chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func ConfigResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item swarm.Config) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Config]{
|
||||
RCType: portainer.ConfigResourceControl,
|
||||
IDGetter: ConfigResourceControlID,
|
||||
LabelsGetter: ConfigLabels,
|
||||
})
|
||||
}
|
||||
|
||||
func ConfigResourceControlID(item swarm.Config) string {
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func ConfigLabels(item swarm.Config) map[string]string {
|
||||
return item.Spec.Labels
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigResourceControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
envID := portainer.EndpointID(1)
|
||||
configID := "config"
|
||||
stackName := "stack"
|
||||
stackRCID := stackutils.ResourceControlID(envID, stackName)
|
||||
serviceID := "service"
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(configID, portainer.ConfigResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// by direct ID
|
||||
rc, err := ConfigResourceControlGetter(tx, envID)(swarm.Config{ID: configID})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(configID, rc.ResourceID)
|
||||
|
||||
// by compose stack label
|
||||
rc, err = ConfigResourceControlGetter(tx, envID)(
|
||||
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.ComposeStackNameLabel: stackName}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by swarm stack label
|
||||
rc, err = ConfigResourceControlGetter(tx, envID)(
|
||||
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmStackNameLabel: stackName}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by service ID
|
||||
rc, err = ConfigResourceControlGetter(tx, envID)(
|
||||
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package uac
|
||||
|
||||
// TODO: implement UAC rules for Container groups
|
||||
// See usage of portainer.ContainerGroupResourceControl
|
||||
@@ -0,0 +1,30 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func ContainerResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item container.Summary) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[container.Summary]{
|
||||
RCType: portainer.ContainerResourceControl,
|
||||
IDGetter: ContainerResourceControlID,
|
||||
LabelsGetter: ContainerLabels,
|
||||
})
|
||||
}
|
||||
|
||||
func ContainerResourceControlID(item container.Summary) string {
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func ContainerLabels(item container.Summary) map[string]string {
|
||||
return item.Labels
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainerResourceControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
envID := portainer.EndpointID(1)
|
||||
containerID := "container"
|
||||
stackName := "stack"
|
||||
stackRCID := stackutils.ResourceControlID(envID, stackName)
|
||||
serviceID := "service"
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(containerID, portainer.ContainerResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// by direct ID
|
||||
rc, err := ContainerResourceControlGetter(tx, envID)(container.Summary{ID: containerID})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(containerID, rc.ResourceID)
|
||||
|
||||
// by compose stack label
|
||||
rc, err = ContainerResourceControlGetter(tx, envID)(
|
||||
container.Summary{ID: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by swarm stack label
|
||||
rc, err = ContainerResourceControlGetter(tx, envID)(
|
||||
container.Summary{ID: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by service ID
|
||||
rc, err = ContainerResourceControlGetter(tx, envID)(
|
||||
container.Summary{ID: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package uac
|
||||
|
||||
// TODO: implement UAC rules for Custom templates
|
||||
// See usage of portainer.CustomTemplateResourceControl
|
||||
@@ -0,0 +1,184 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type rcServiceLike interface {
|
||||
ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error)
|
||||
}
|
||||
|
||||
type teamServiceLike interface {
|
||||
TeamByName(name string) (*portainer.Team, error)
|
||||
}
|
||||
|
||||
type userServiceLike interface {
|
||||
UserIDByUsername(name string) (portainer.UserID, error)
|
||||
}
|
||||
|
||||
type txLike[
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
] interface {
|
||||
ResourceControl() RCS
|
||||
Team() TS
|
||||
User() US
|
||||
}
|
||||
|
||||
type ResourceContext[T any] struct {
|
||||
RCType portainer.ResourceControlType
|
||||
IDGetter func(T) string
|
||||
LabelsGetter func(T) map[string]string
|
||||
}
|
||||
|
||||
func genericResourcControlGetter[
|
||||
T any,
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
context ResourceContext[T],
|
||||
) func(T) (*portainer.ResourceControl, error) {
|
||||
return func(item T) (*portainer.ResourceControl, error) {
|
||||
resourceID := context.IDGetter(item)
|
||||
resourceType := context.RCType
|
||||
|
||||
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
|
||||
resourceID, resourceType,
|
||||
); err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
} else if rc != nil {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
if context.LabelsGetter == nil {
|
||||
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
|
||||
}
|
||||
|
||||
labels := context.LabelsGetter(item)
|
||||
if labels == nil {
|
||||
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
|
||||
}
|
||||
|
||||
if serviceId, ok := labels[consts.SwarmServiceIDLabel]; ok {
|
||||
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
|
||||
ServiceResourceControlID(swarm.Service{ID: serviceId}),
|
||||
portainer.ServiceResourceControl,
|
||||
); err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
} else if rc != nil {
|
||||
return rc, nil
|
||||
}
|
||||
}
|
||||
|
||||
if stackName, ok := labels[consts.SwarmStackNameLabel]; ok {
|
||||
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
|
||||
StackResourceControlID(endpointID, stackName),
|
||||
portainer.StackResourceControl,
|
||||
); err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
} else if rc != nil {
|
||||
return rc, nil
|
||||
}
|
||||
}
|
||||
|
||||
if stackName, ok := labels[consts.ComposeStackNameLabel]; ok {
|
||||
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
|
||||
StackResourceControlID(endpointID, stackName),
|
||||
portainer.StackResourceControl,
|
||||
); err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
} else if rc != nil {
|
||||
return rc, nil
|
||||
}
|
||||
}
|
||||
|
||||
return rcFromPortainerLabels(tx, labels, resourceID, resourceType)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
publicRCLabel = "io.portainer.accesscontrol.public"
|
||||
userRCLabel = "io.portainer.accesscontrol.users"
|
||||
teamRCLabel = "io.portainer.accesscontrol.teams"
|
||||
)
|
||||
|
||||
// translation of rules from transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
|
||||
func rcFromPortainerLabels[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX, labels map[string]string, resourceID string, resourceType portainer.ResourceControlType,
|
||||
) (*portainer.ResourceControl, error) {
|
||||
if _, ok := labels[publicRCLabel]; ok {
|
||||
return authorization.NewPublicResourceControl(resourceID, resourceType), nil
|
||||
}
|
||||
|
||||
teamNames := make([]string, 0)
|
||||
userNames := make([]string, 0)
|
||||
|
||||
if teams, ok := labels[teamRCLabel]; ok {
|
||||
teamNames = getUniqueElements(teams)
|
||||
}
|
||||
|
||||
if users, ok := labels[userRCLabel]; ok {
|
||||
userNames = getUniqueElements(users)
|
||||
}
|
||||
|
||||
if len(teamNames) == 0 && len(userNames) == 0 {
|
||||
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
|
||||
}
|
||||
|
||||
teamIDs := make([]portainer.TeamID, 0)
|
||||
userIDs := make([]portainer.UserID, 0)
|
||||
for _, name := range teamNames {
|
||||
team, err := tx.Team().TeamByName(name)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Str("name", name).
|
||||
Str("resource_id", resourceID).
|
||||
Msg("unknown team name in access control label, ignoring access control rule for this team")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
teamIDs = append(teamIDs, team.ID)
|
||||
}
|
||||
|
||||
for _, name := range userNames {
|
||||
userID, err := tx.User().UserIDByUsername(name)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Str("name", name).
|
||||
Str("resource_id", resourceID).
|
||||
Msg("unknown user name in access control label, ignoring access control rule for this user")
|
||||
continue
|
||||
}
|
||||
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
|
||||
return authorization.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs), nil
|
||||
}
|
||||
|
||||
func getUniqueElements(items string) []string {
|
||||
xs := strings.Split(items, ",")
|
||||
xs = slicesx.Map(xs, strings.TrimSpace)
|
||||
xs = slicesx.FilterInPlace(xs, func(x string) bool { return len(x) > 0 })
|
||||
|
||||
return slicesx.Unique(xs)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenericResourcControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
endpointID := portainer.EndpointID(1)
|
||||
composeStackName := "compose-stack"
|
||||
composeStackRCID := StackResourceControlID(endpointID, composeStackName)
|
||||
swarmStackName := "swarm-stack"
|
||||
swarmStackRCID := StackResourceControlID(endpointID, swarmStackName)
|
||||
serviceID := "service"
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
// created on container create
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl("container", portainer.ContainerResourceControl)))
|
||||
// created on compose stack create
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(composeStackRCID, portainer.StackResourceControl)))
|
||||
// created on swarm stack create
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(swarmStackRCID, portainer.StackResourceControl)))
|
||||
// created a swarm service create
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
})
|
||||
is.NoError(err)
|
||||
|
||||
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
context := ResourceContext[container.Summary]{RCType: portainer.ContainerResourceControl, IDGetter: ContainerResourceControlID, LabelsGetter: ContainerLabels}
|
||||
|
||||
// trying to get UAC for a container created through Portainer
|
||||
rc, err := genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "container"})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
|
||||
// trying to get UAC for an external container
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "unknown"})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
// trying to get UAC for a container from a compose stack
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-compose-stack-name-label", Labels: map[string]string{consts.ComposeStackNameLabel: composeStackName}})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(composeStackRCID, rc.ResourceID)
|
||||
|
||||
// trying to get UAC for a container from a swarm stack
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-swarm-stack-name-label", Labels: map[string]string{consts.SwarmStackNameLabel: swarmStackName}})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(swarmStackRCID, rc.ResourceID)
|
||||
|
||||
// trying to get UAC for a container from a swarm service
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-swarm-service-id-label", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
}
|
||||
|
||||
func TestGenericResourcControlGetterWithPortainerLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.User().Create(&portainer.User{Role: portainer.AdministratorRole, Username: "admin"}))
|
||||
is.NoError(tx.User().Create(&portainer.User{Role: portainer.AdministratorRole, Username: "user"}))
|
||||
is.NoError(tx.Team().Create(&portainer.Team{Name: "team"}))
|
||||
is.NoError(tx.TeamMembership().Create(&portainer.TeamMembership{UserID: 2, TeamID: 1}))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
context := ResourceContext[container.Summary]{RCType: portainer.ContainerResourceControl, IDGetter: ContainerResourceControlID, LabelsGetter: ContainerLabels}
|
||||
|
||||
rc, err := genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "public",
|
||||
Labels: map[string]string{publicRCLabel: "true"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.True(rc.Public)
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "team",
|
||||
Labels: map[string]string{teamRCLabel: "team"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Contains(rc.TeamAccesses, portainer.TeamResourceAccess{TeamID: 1, AccessLevel: portainer.ReadWriteAccessLevel})
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "no-team",
|
||||
Labels: map[string]string{teamRCLabel: "team2"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "user",
|
||||
Labels: map[string]string{userRCLabel: "user"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Contains(rc.UserAccesses, portainer.UserResourceAccess{UserID: 2, AccessLevel: portainer.ReadWriteAccessLevel})
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "no-user",
|
||||
Labels: map[string]string{userRCLabel: "user2"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "warn-on-unknown",
|
||||
Labels: map[string]string{userRCLabel: "user2,user3", teamRCLabel: "team2,team3"},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "empty-when-no-label",
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
|
||||
ID: "empty-when-empty-labels",
|
||||
Labels: map[string]string{},
|
||||
})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Empty(rc.UserAccesses)
|
||||
is.Empty(rc.TeamAccesses)
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func NetworkResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item network.Summary) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[network.Summary]{
|
||||
RCType: portainer.NetworkResourceControl,
|
||||
IDGetter: NetworkResourceControlID,
|
||||
LabelsGetter: NetworkLabels,
|
||||
})
|
||||
}
|
||||
|
||||
func NetworkResourceControlID(item network.Summary) string {
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func NetworkLabels(item network.Summary) map[string]string {
|
||||
return item.Labels
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/network"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkResourceControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
envID := portainer.EndpointID(1)
|
||||
networkID := "network"
|
||||
stackName := "stack"
|
||||
stackRCID := stackutils.ResourceControlID(envID, stackName)
|
||||
serviceID := "service"
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(networkID, portainer.NetworkResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// by direct ID
|
||||
rc, err := NetworkResourceControlGetter(tx, envID)(network.Inspect{ID: networkID})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(networkID, rc.ResourceID)
|
||||
|
||||
// by compose stack label
|
||||
rc, err = NetworkResourceControlGetter(tx, envID)(
|
||||
network.Inspect{ID: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by swarm stack label
|
||||
rc, err = NetworkResourceControlGetter(tx, envID)(
|
||||
network.Inspect{ID: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by service ID
|
||||
rc, err = NetworkResourceControlGetter(tx, envID)(
|
||||
network.Inspect{ID: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func SecretResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item swarm.Secret) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Secret]{
|
||||
RCType: portainer.SecretResourceControl,
|
||||
IDGetter: SecretResourceControlID,
|
||||
LabelsGetter: SecretLabels,
|
||||
})
|
||||
}
|
||||
|
||||
func SecretResourceControlID(item swarm.Secret) string {
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func SecretLabels(item swarm.Secret) map[string]string {
|
||||
return item.Spec.Labels
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSecretResourceControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
envID := portainer.EndpointID(1)
|
||||
secretID := "secret"
|
||||
stackName := "stack"
|
||||
stackRCID := stackutils.ResourceControlID(envID, stackName)
|
||||
serviceID := "service"
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(secretID, portainer.SecretResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// by direct ID
|
||||
rc, err := SecretResourceControlGetter(tx, envID)(swarm.Secret{ID: secretID})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(secretID, rc.ResourceID)
|
||||
|
||||
// by compose stack label
|
||||
rc, err = SecretResourceControlGetter(tx, envID)(
|
||||
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.ComposeStackNameLabel: stackName}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by swarm stack label
|
||||
rc, err = SecretResourceControlGetter(tx, envID)(
|
||||
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmStackNameLabel: stackName}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by service ID
|
||||
rc, err = SecretResourceControlGetter(tx, envID)(
|
||||
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}}}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func ServiceResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item swarm.Service) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Service]{
|
||||
RCType: portainer.ServiceResourceControl,
|
||||
IDGetter: ServiceResourceControlID,
|
||||
LabelsGetter: ServiceLabels,
|
||||
})
|
||||
}
|
||||
|
||||
func ServiceResourceControlID(item swarm.Service) string {
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func ServiceLabels(item swarm.Service) map[string]string {
|
||||
return item.Spec.Labels
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
func StackResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item portainer.Stack) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[portainer.Stack]{
|
||||
RCType: portainer.StackResourceControl,
|
||||
IDGetter: func(s portainer.Stack) string { return StackResourceControlID(s.EndpointID, s.Name) },
|
||||
// stacks don't have labels so we don't pass a getter
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: replace usage of stackutils function to this package
|
||||
func StackResourceControlID(endpointID portainer.EndpointID, name string) string {
|
||||
return stackutils.ResourceControlID(endpointID, name)
|
||||
}
|
||||
|
||||
type ExternalStack struct {
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// External stacks are indirectly detected either via containers or services labels
|
||||
// Any UAC applied to them can only be fetched from containers'/services' labels
|
||||
func ExternalStackResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID) func(item ExternalStack) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[ExternalStack]{
|
||||
RCType: portainer.StackResourceControl,
|
||||
IDGetter: func(s ExternalStack) string { return "0" },
|
||||
LabelsGetter: func(es ExternalStack) map[string]string { return es.Labels },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
)
|
||||
|
||||
// FilterByResourceControl filters a list of items based on the user's role and the resource control associated to the item.
|
||||
// UAC rules
|
||||
// * UAC in DB (direct)
|
||||
// * UAC inherited (from swarm service | swarm stack | compose stack)
|
||||
// * UAC defined in labels (UAC on external resources)
|
||||
func FilterByResourceControl[T any](
|
||||
items []T,
|
||||
user *portainer.User,
|
||||
userMemberships []portainer.TeamMembership,
|
||||
resourceControlGetter func(item T) (*portainer.ResourceControl, error),
|
||||
) ([]T, error) {
|
||||
filteredItems := make([]T, 0)
|
||||
|
||||
if user == nil {
|
||||
return filteredItems, nil
|
||||
}
|
||||
|
||||
if canBypassUAC(user) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
userTeamIDs := slicesx.Map(userMemberships, func(membership portainer.TeamMembership) portainer.TeamID {
|
||||
return membership.TeamID
|
||||
})
|
||||
|
||||
for _, item := range items {
|
||||
rc, err := resourceControlGetter(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to retrieve resource control: %w", err)
|
||||
}
|
||||
|
||||
// TODO: move UserCanAccessResource function to the UAC package
|
||||
if authorization.UserCanAccessResource(user.ID, userTeamIDs, rc) {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredItems, nil
|
||||
}
|
||||
|
||||
func canBypassUAC(user *portainer.User) bool {
|
||||
return user != nil && user.Role == portainer.AdministratorRole
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCanBypassUAC(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
admin := &portainer.User{Role: portainer.AdministratorRole}
|
||||
user := &portainer.User{Role: portainer.StandardUserRole}
|
||||
|
||||
is.False(canBypassUAC(nil))
|
||||
is.False(canBypassUAC(user))
|
||||
is.True(canBypassUAC(admin))
|
||||
}
|
||||
|
||||
func TestFilterByResourceControl(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
endpointID := portainer.EndpointID(1)
|
||||
stackRCID := StackResourceControlID(endpointID, "stack")
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPrivateResourceControl(stackRCID, portainer.StackResourceControl, 2)))
|
||||
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewAdministratorsOnlyResourceControl("admin", portainer.ContainerResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPrivateResourceControl("private", portainer.ContainerResourceControl, 2)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl("public", portainer.ContainerResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewSystemResourceControl("system", portainer.ContainerResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
admin := &portainer.User{ID: 1, Role: portainer.AdministratorRole}
|
||||
std := &portainer.User{ID: 2, Role: portainer.StandardUserRole}
|
||||
std2 := &portainer.User{ID: 3, Role: portainer.StandardUserRole}
|
||||
|
||||
containers := []container.Summary{{ID: "admin"}, {ID: "private"}, {ID: "public"}, {ID: "system"},
|
||||
{ID: "bylabel", Labels: map[string]string{consts.ComposeStackNameLabel: "stack"}},
|
||||
}
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
c, err := FilterByResourceControl(containers, admin, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
|
||||
is.NoError(err)
|
||||
is.Len(c, len(containers))
|
||||
|
||||
c, err = FilterByResourceControl(containers, std, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
|
||||
is.NoError(err)
|
||||
is.Len(c, 4)
|
||||
is.Contains(c, container.Summary{ID: "private"})
|
||||
is.Contains(c, container.Summary{ID: "public"})
|
||||
is.Contains(c, container.Summary{ID: "system"})
|
||||
is.Contains(c, container.Summary{ID: "bylabel", Labels: map[string]string{consts.ComposeStackNameLabel: "stack"}})
|
||||
|
||||
c, err = FilterByResourceControl(containers, std2, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
|
||||
is.NoError(err)
|
||||
is.Len(c, 2)
|
||||
is.Contains(c, container.Summary{ID: "public"})
|
||||
is.Contains(c, container.Summary{ID: "system"})
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func VolumeResourceControlGetter[
|
||||
TX txLike[RCS, TS, US],
|
||||
RCS rcServiceLike,
|
||||
TS teamServiceLike,
|
||||
US userServiceLike,
|
||||
](
|
||||
tx TX,
|
||||
endpointID portainer.EndpointID,
|
||||
) func(item volume.Volume) (*portainer.ResourceControl, error) {
|
||||
return genericResourcControlGetter(tx, endpointID, ResourceContext[volume.Volume]{
|
||||
RCType: portainer.VolumeResourceControl,
|
||||
IDGetter: VolumeResourceControlID,
|
||||
LabelsGetter: VolumeLabels,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: use a copy of getVolumeResourceID()
|
||||
// or change the key (+ migrate) to not require using the dockerID/swarm nodeID
|
||||
func VolumeResourceControlID(item volume.Volume) string {
|
||||
return item.Name
|
||||
}
|
||||
|
||||
func VolumeLabels(item volume.Volume) map[string]string {
|
||||
return item.Labels
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVolumeResourceControlGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := require.New(t)
|
||||
|
||||
ok, store := datastore.MustNewTestStore(t, true, false)
|
||||
is.True(ok)
|
||||
is.NotNil(store)
|
||||
|
||||
envID := portainer.EndpointID(1)
|
||||
volumeName := "volume"
|
||||
stackName := "stack"
|
||||
stackRCID := stackutils.ResourceControlID(envID, stackName)
|
||||
serviceID := "service"
|
||||
|
||||
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(volumeName, portainer.VolumeResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
|
||||
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
|
||||
return nil
|
||||
}))
|
||||
|
||||
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
// by direct ID
|
||||
rc, err := VolumeResourceControlGetter(tx, envID)(volume.Volume{Name: volumeName})
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(volumeName, rc.ResourceID)
|
||||
|
||||
// by compose stack label
|
||||
rc, err = VolumeResourceControlGetter(tx, envID)(
|
||||
volume.Volume{Name: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by swarm stack label
|
||||
rc, err = VolumeResourceControlGetter(tx, envID)(
|
||||
volume.Volume{Name: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(stackRCID, rc.ResourceID)
|
||||
|
||||
// by service ID
|
||||
rc, err = VolumeResourceControlGetter(tx, envID)(
|
||||
volume.Volume{Name: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
|
||||
)
|
||||
is.NoError(err)
|
||||
is.NotNil(rc)
|
||||
is.Equal(serviceID, rc.ResourceID)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user