mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(docker): migrate dashboard to react [EE-2191] (#11574)
Some checks are pending
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
Some checks are pending
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
This commit is contained in:
parent
2669a44d79
commit
014a590704
54 changed files with 1297 additions and 507 deletions
|
@ -70,7 +70,10 @@ func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
|
|||
}
|
||||
|
||||
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService { return nil }
|
||||
func (tx *StoreTx) Stack() dataservices.StackService { return nil }
|
||||
|
||||
func (tx *StoreTx) Stack() dataservices.StackService {
|
||||
return tx.store.StackService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Tag() dataservices.TagService {
|
||||
return tx.store.TagService.Tx(tx.tx)
|
||||
|
@ -80,7 +83,8 @@ func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService {
|
|||
return tx.store.TeamMembershipService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
|
||||
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
|
||||
|
||||
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }
|
||||
|
||||
func (tx *StoreTx) User() dataservices.UserService {
|
||||
|
|
|
@ -5,4 +5,5 @@ const (
|
|||
SwarmStackNameLabel = "com.docker.stack.namespace"
|
||||
SwarmServiceIdLabel = "com.docker.swarm.service.id"
|
||||
SwarmNodeIdLabel = "com.docker.swarm.node.id"
|
||||
HideStackLabel = "io.portainer.hideStack"
|
||||
)
|
||||
|
|
37
api/docker/container_stats.go
Normal file
37
api/docker/container_stats.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package docker
|
||||
|
||||
import "github.com/docker/docker/api/types"
|
||||
|
||||
type ContainerStats struct {
|
||||
Running int `json:"running"`
|
||||
Stopped int `json:"stopped"`
|
||||
Healthy int `json:"healthy"`
|
||||
Unhealthy int `json:"unhealthy"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func CalculateContainerStats(containers []types.Container) ContainerStats {
|
||||
var running, stopped, healthy, unhealthy int
|
||||
for _, container := range containers {
|
||||
switch container.State {
|
||||
case "running":
|
||||
running++
|
||||
case "healthy":
|
||||
running++
|
||||
healthy++
|
||||
case "unhealthy":
|
||||
running++
|
||||
unhealthy++
|
||||
case "exited", "stopped":
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
|
||||
return ContainerStats{
|
||||
Running: running,
|
||||
Stopped: stopped,
|
||||
Healthy: healthy,
|
||||
Unhealthy: unhealthy,
|
||||
Total: len(containers),
|
||||
}
|
||||
}
|
27
api/docker/container_stats_test.go
Normal file
27
api/docker/container_stats_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCalculateContainerStats(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{State: "running"},
|
||||
{State: "running"},
|
||||
{State: "exited"},
|
||||
{State: "stopped"},
|
||||
{State: "healthy"},
|
||||
{State: "unhealthy"},
|
||||
}
|
||||
|
||||
stats := CalculateContainerStats(containers)
|
||||
|
||||
assert.Equal(t, 4, stats.Running)
|
||||
assert.Equal(t, 2, stats.Stopped)
|
||||
assert.Equal(t, 1, stats.Healthy)
|
||||
assert.Equal(t, 1, stats.Unhealthy)
|
||||
assert.Equal(t, 6, stats.Total)
|
||||
}
|
|
@ -153,19 +153,11 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
|||
return err
|
||||
}
|
||||
|
||||
runningContainers := 0
|
||||
stoppedContainers := 0
|
||||
healthyContainers := 0
|
||||
unhealthyContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
gpuUseSet := make(map[string]struct{})
|
||||
gpuUseAll := false
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" || container.State == "stopped" {
|
||||
stoppedContainers++
|
||||
} else if container.State == "running" {
|
||||
runningContainers++
|
||||
|
||||
if container.State == "running" {
|
||||
// snapshot GPUs
|
||||
response, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
|
@ -202,15 +194,6 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
|||
}
|
||||
}
|
||||
|
||||
if container.State == "healthy" {
|
||||
runningContainers++
|
||||
healthyContainers++
|
||||
}
|
||||
|
||||
if container.State == "unhealthy" {
|
||||
unhealthyContainers++
|
||||
}
|
||||
|
||||
for k, v := range container.Labels {
|
||||
if k == consts.ComposeStackNameLabel {
|
||||
stacks[v] = struct{}{}
|
||||
|
@ -226,11 +209,13 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
|||
snapshot.GpuUseAll = gpuUseAll
|
||||
snapshot.GpuUseList = gpuUseList
|
||||
|
||||
snapshot.ContainerCount = len(containers)
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.HealthyContainerCount = healthyContainers
|
||||
snapshot.UnhealthyContainerCount = unhealthyContainers
|
||||
stats := CalculateContainerStats(containers)
|
||||
|
||||
snapshot.ContainerCount = stats.Total
|
||||
snapshot.RunningContainerCount = stats.Running
|
||||
snapshot.StoppedContainerCount = stats.Stopped
|
||||
snapshot.HealthyContainerCount = stats.Healthy
|
||||
snapshot.UnhealthyContainerCount = stats.Unhealthy
|
||||
snapshot.StackCount += len(stacks)
|
||||
for _, container := range containers {
|
||||
snapshot.SnapshotRaw.Containers = append(snapshot.SnapshotRaw.Containers, portainer.DockerContainerSnapshot{Container: container})
|
||||
|
|
164
api/http/handler/docker/dashboard.go
Normal file
164
api/http/handler/docker/dashboard.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/utils"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type imagesCounters struct {
|
||||
Total int `json:"total"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
Containers docker.ContainerStats `json:"containers"`
|
||||
Services int `json:"services"`
|
||||
Images imagesCounters `json:"images"`
|
||||
Volumes int `json:"volumes"`
|
||||
Networks int `json:"networks"`
|
||||
Stacks int `json:"stacks"`
|
||||
}
|
||||
|
||||
// @id dockerDashboard
|
||||
// @summary Get counters for the dashboard
|
||||
// @description **Access policy**: restricted
|
||||
// @tags docker
|
||||
// @security jwt
|
||||
// @param environmentId path int true "Environment identifier"
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {object} dashboardResponse "Success"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 500 "Internal server error"
|
||||
// @router /docker/{environmentId}/dashboard [post]
|
||||
func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var resp dashboardResponse
|
||||
err := h.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
cli, httpErr := utils.GetClient(r, h.dockerClientFactory)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
context, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve user details from request context", err)
|
||||
}
|
||||
|
||||
containers, err := cli.ContainerList(r.Context(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
|
||||
}
|
||||
|
||||
containers, err = utils.FilterByResourceControl(tx, containers, portainer.ContainerResourceControl, context, func(c types.Container) string {
|
||||
return c.ID
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
images, err := cli.ImageList(r.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker images", err)
|
||||
}
|
||||
|
||||
var totalSize int64
|
||||
for _, image := range images {
|
||||
totalSize += image.Size
|
||||
}
|
||||
|
||||
info, err := cli.Info(r.Context())
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker info", err)
|
||||
}
|
||||
|
||||
isSwarmManager := info.Swarm.ControlAvailable && info.Swarm.NodeID != ""
|
||||
|
||||
var services []swarm.Service
|
||||
if isSwarmManager {
|
||||
servicesRes, err := cli.ServiceList(r.Context(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker services", err)
|
||||
}
|
||||
|
||||
filteredServices, err := utils.FilterByResourceControl(tx, servicesRes, portainer.ServiceResourceControl, context, func(c swarm.Service) string {
|
||||
return c.ID
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services = filteredServices
|
||||
}
|
||||
|
||||
volumesRes, err := cli.VolumeList(r.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker volumes", err)
|
||||
}
|
||||
|
||||
volumes, err := utils.FilterByResourceControl(tx, volumesRes.Volumes, portainer.NetworkResourceControl, context, func(c *volume.Volume) string {
|
||||
return c.Name
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
networks, err := cli.NetworkList(r.Context(), types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker networks", err)
|
||||
}
|
||||
|
||||
networks, err = utils.FilterByResourceControl(tx, networks, portainer.NetworkResourceControl, context, func(c types.NetworkResource) string {
|
||||
return c.Name
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environment, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment", err)
|
||||
}
|
||||
|
||||
stackCount := 0
|
||||
if environment.SecuritySettings.AllowStackManagementForRegularUsers || context.IsAdmin {
|
||||
stacks, err := utils.GetDockerStacks(tx, context, environment.ID, containers, services)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve stacks", err)
|
||||
}
|
||||
|
||||
stackCount = len(stacks)
|
||||
}
|
||||
|
||||
resp = dashboardResponse{
|
||||
Images: imagesCounters{
|
||||
Total: len(images),
|
||||
Size: totalSize,
|
||||
},
|
||||
Services: len(services),
|
||||
Containers: docker.CalculateContainerStats(containers),
|
||||
Networks: len(networks),
|
||||
Volumes: len(volumes),
|
||||
Stacks: stackCount,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return errors.TxResponse(err, func() *httperror.HandlerError {
|
||||
return response.JSON(w, resp)
|
||||
})
|
||||
}
|
|
@ -41,8 +41,10 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
|||
|
||||
// endpoints
|
||||
endpointRouter := h.PathPrefix("/docker/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
endpointRouter.Use(dockerOnlyMiddleware)
|
||||
endpointRouter.Use(bouncer.AuthenticatedAccess)
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"), dockerOnlyMiddleware)
|
||||
|
||||
endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.dashboard)).Methods(http.MethodGet)
|
||||
|
||||
containersHandler := containers.NewHandler("/docker/{id}/containers", bouncer, dataStore, dockerClientFactory, containerService)
|
||||
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
|
||||
|
|
36
api/http/handler/docker/utils/filter_by_uac.go
Normal file
36
api/http/handler/docker/utils/filter_by_uac.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
)
|
||||
|
||||
// filterByResourceControl filters a list of items based on the user's role and the resource control associated to the item.
|
||||
func FilterByResourceControl[T any](tx dataservices.DataStoreTx, items []T, rcType portainer.ResourceControlType, securityContext *security.RestrictedRequestContext, idGetter func(T) string) ([]T, error) {
|
||||
if securityContext.IsAdmin {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
userTeamIDs := slices.Map(securityContext.UserMemberships, func(membership portainer.TeamMembership) portainer.TeamID {
|
||||
return membership.TeamID
|
||||
})
|
||||
|
||||
filteredItems := make([]T, 0)
|
||||
for _, item := range items {
|
||||
resourceControl, err := tx.ResourceControl().ResourceControlByResourceIDAndType(idGetter(item), portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to retrieve resource control: %w", err)
|
||||
}
|
||||
|
||||
if resourceControl == nil || authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
|
||||
}
|
||||
return filteredItems, nil
|
||||
}
|
83
api/http/handler/docker/utils/get_stacks.go
Normal file
83
api/http/handler/docker/utils/get_stacks.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerconsts "github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type StackViewModel struct {
|
||||
InternalStack *portaineree.Stack
|
||||
|
||||
ID portainer.StackID
|
||||
Name string
|
||||
IsExternal bool
|
||||
Type portainer.StackType
|
||||
}
|
||||
|
||||
// GetDockerStacks retrieves all the stacks associated to a specific environment filtered by the user's access.
|
||||
func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.RestrictedRequestContext, environmentID portainer.EndpointID, containers []types.Container, services []swarm.Service) ([]StackViewModel, error) {
|
||||
|
||||
stacks, err := tx.Stack().ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to retrieve stacks: %w", err)
|
||||
}
|
||||
|
||||
stacksNameSet := map[string]*StackViewModel{}
|
||||
|
||||
for i := range stacks {
|
||||
stack := stacks[i]
|
||||
if stack.EndpointID == environmentID {
|
||||
stacksNameSet[stack.Name] = &StackViewModel{
|
||||
InternalStack: &stack,
|
||||
ID: stack.ID,
|
||||
Name: stack.Name,
|
||||
IsExternal: false,
|
||||
Type: stack.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
name := container.Labels[dockerconsts.ComposeStackNameLabel]
|
||||
|
||||
if name != "" && stacksNameSet[name] == nil && !isHiddenStack(container.Labels) {
|
||||
stacksNameSet[name] = &StackViewModel{
|
||||
Name: name,
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerComposeStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
name := service.Spec.Labels[dockerconsts.SwarmStackNameLabel]
|
||||
|
||||
if name != "" && stacksNameSet[name] == nil && !isHiddenStack(service.Spec.Labels) {
|
||||
stacksNameSet[name] = &StackViewModel{
|
||||
Name: name,
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stacksList := make([]StackViewModel, 0)
|
||||
for _, stack := range stacksNameSet {
|
||||
stacksList = append(stacksList, *stack)
|
||||
}
|
||||
|
||||
return FilterByResourceControl(tx, stacksList, portainer.StackResourceControl, securityContext, func(c StackViewModel) string {
|
||||
return c.Name
|
||||
})
|
||||
}
|
||||
|
||||
func isHiddenStack(labels map[string]string) bool {
|
||||
return labels[dockerconsts.HideStackLabel] != ""
|
||||
}
|
96
api/http/handler/docker/utils/get_stacks_test.go
Normal file
96
api/http/handler/docker/utils/get_stacks_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
dockerconsts "github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandler_getDockerStacks(t *testing.T) {
|
||||
environment := &portaineree.Endpoint{
|
||||
ID: 1,
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
containers := []types.Container{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
dockerconsts.ComposeStackNameLabel: "stack1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: map[string]string{
|
||||
dockerconsts.ComposeStackNameLabel: "stack2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
services := []swarm.Service{
|
||||
{
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Labels: map[string]string{
|
||||
dockerconsts.SwarmStackNameLabel: "stack3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stack1 := portaineree.Stack{
|
||||
ID: 1,
|
||||
Name: "stack1",
|
||||
EndpointID: 1,
|
||||
Type: portainer.DockerComposeStack,
|
||||
}
|
||||
|
||||
datastore := testhelpers.NewDatastore(
|
||||
testhelpers.WithEndpoints([]portaineree.Endpoint{*environment}),
|
||||
testhelpers.WithStacks([]portaineree.Stack{
|
||||
stack1,
|
||||
{
|
||||
ID: 2,
|
||||
Name: "stack2",
|
||||
EndpointID: 2,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
stacksList, err := GetDockerStacks(datastore, &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}, environment.ID, containers, services)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, stacksList, 3)
|
||||
|
||||
expectedStacks := []StackViewModel{
|
||||
{
|
||||
InternalStack: &stack1,
|
||||
ID: 1,
|
||||
Name: "stack1",
|
||||
IsExternal: false,
|
||||
Type: portainer.DockerComposeStack,
|
||||
},
|
||||
{
|
||||
Name: "stack2",
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerComposeStack,
|
||||
},
|
||||
{
|
||||
Name: "stack3",
|
||||
IsExternal: true,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
},
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedStacks, stacksList)
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
@ -197,7 +198,7 @@ func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoin
|
|||
}
|
||||
|
||||
for _, container := range containers {
|
||||
containerNS, ok := container.Labels["com.docker.compose.project"]
|
||||
containerNS, ok := container.Labels[consts.ComposeStackNameLabel]
|
||||
|
||||
if ok && containerNS == name {
|
||||
return false, nil
|
||||
|
|
|
@ -13,7 +13,11 @@ func IsAdmin(request *http.Request) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
return tokenData.Role == portainer.AdministratorRole, nil
|
||||
return IsAdminRole(tokenData.Role), nil
|
||||
}
|
||||
|
||||
func IsAdminRole(role portainer.UserRole) bool {
|
||||
return role == portainer.AdministratorRole
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -49,3 +50,17 @@ func RetrieveRestrictedRequestContext(request *http.Request) (*RestrictedRequest
|
|||
requestContext := contextData.(*RestrictedRequestContext)
|
||||
return requestContext, nil
|
||||
}
|
||||
|
||||
func RetrieveUserFromRequest(r *http.Request, tx dataservices.DataStoreTx) (*portainer.User, error) {
|
||||
rrc, err := RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := tx.User().Read(rrc.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
|
|
@ -337,3 +337,96 @@ func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
|
|||
d.endpoint = &stubEndpointService{endpoints: endpoints}
|
||||
}
|
||||
}
|
||||
|
||||
type stubStacksService struct {
|
||||
stacks []portainer.Stack
|
||||
}
|
||||
|
||||
func (s *stubStacksService) BucketName() string { return "stacks" }
|
||||
|
||||
func (s *stubStacksService) Create(stack *portainer.Stack) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) Update(ID portainer.StackID, stack *portainer.Stack) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) Delete(ID portainer.StackID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) Read(ID portainer.StackID) (*portainer.Stack, error) {
|
||||
for _, stack := range s.stacks {
|
||||
if stack.ID == ID {
|
||||
return &stack, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
func (s *stubStacksService) ReadAll() ([]portainer.Stack, error) {
|
||||
return s.stacks, nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) StacksByEndpointID(endpointID portainer.EndpointID) ([]portainer.Stack, error) {
|
||||
result := make([]portainer.Stack, 0)
|
||||
|
||||
for _, stack := range s.stacks {
|
||||
if stack.EndpointID == endpointID {
|
||||
result = append(result, stack)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
result := make([]portainer.Stack, 0)
|
||||
|
||||
for _, stack := range s.stacks {
|
||||
if stack.AutoUpdate != nil {
|
||||
result = append(result, stack)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) StackByName(name string) (*portainer.Stack, error) {
|
||||
for _, stack := range s.stacks {
|
||||
if stack.Name == name {
|
||||
return &stack, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
func (s *stubStacksService) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
result := make([]portainer.Stack, 0)
|
||||
|
||||
for _, stack := range s.stacks {
|
||||
if stack.Name == name {
|
||||
result = append(result, stack)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *stubStacksService) StackByWebhookID(webhookID string) (*portainer.Stack, error) {
|
||||
for _, stack := range s.stacks {
|
||||
if stack.AutoUpdate != nil && stack.AutoUpdate.Webhook == webhookID {
|
||||
return &stack, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
func (s *stubStacksService) GetNextIdentifier() int {
|
||||
return len(s.stacks)
|
||||
}
|
||||
|
||||
// WithStacks option will instruct testDatastore to return provided stacks
|
||||
func WithStacks(stacks []portainer.Stack) datastoreOption {
|
||||
return func(d *testDatastore) {
|
||||
d.stack = &stubStacksService{stacks: stacks}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue