diff --git a/api/kubernetes/cli/resource_quota.go b/api/kubernetes/cli/resource_quota.go index 407a78f49..374065c56 100644 --- a/api/kubernetes/cli/resource_quota.go +++ b/api/kubernetes/cli/resource_quota.go @@ -49,7 +49,7 @@ func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]core func (kcl *KubeClient) fetchResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) { resourceQuotas, err := kcl.cli.CoreV1().ResourceQuotas(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { - return nil, fmt.Errorf("an error occured, failed to list resource quotas for the admin user: %w", err) + return nil, fmt.Errorf("an error occurred, failed to list resource quotas for the admin user: %w", err) } return &resourceQuotas.Items, nil diff --git a/pkg/libstack/compose/composeplugin.go b/pkg/libstack/compose/composeplugin.go index 1990a95ed..1aaf90d5b 100644 --- a/pkg/libstack/compose/composeplugin.go +++ b/pkg/libstack/compose/composeplugin.go @@ -2,13 +2,16 @@ package compose import ( "context" + "errors" "fmt" "maps" "path/filepath" "slices" + "strconv" "strings" "sync" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/pkg/libstack" "github.com/compose-spec/compose-go/v2/dotenv" @@ -22,6 +25,8 @@ import ( "github.com/rs/zerolog/log" ) +const PortainerEdgeStackLabel = "io.portainer.edge_stack_id" + var mu sync.Mutex func withCli( @@ -125,7 +130,7 @@ func withComposeService( // Deploy creates and starts containers func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, options libstack.DeployOptions) error { return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error { - addServiceLabels(project, false) + addServiceLabels(project, false, options.EdgeStackID) project = project.WithoutUnnecessaryResources() @@ -154,7 +159,7 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option // Run runs the given service just once, without considering dependencies func (c *ComposeDeployer) Run(ctx context.Context, filePaths []string, serviceName string, options libstack.RunOptions) error { return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error { - addServiceLabels(project, true) + addServiceLabels(project, true, 0) for name, service := range project.Services { if name == serviceName { @@ -242,7 +247,51 @@ func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, option return payload, nil } -func addServiceLabels(project *types.Project, oneOff bool) { +func (c *ComposeDeployer) GetExistingEdgeStacks(ctx context.Context) ([]libstack.EdgeStack, error) { + m := make(map[int]libstack.EdgeStack) + + if err := withComposeService(ctx, nil, libstack.Options{}, func(composeService api.Service, project *types.Project) error { + stacks, err := composeService.List(ctx, api.ListOptions{ + All: true, + }) + if err != nil { + return err + } + + for _, s := range stacks { + summary, err := composeService.Ps(ctx, s.Name, api.PsOptions{All: true}) + if err != nil { + return err + } + + for _, cs := range summary { + if sid, ok := cs.Labels[PortainerEdgeStackLabel]; ok { + id, err := strconv.Atoi(sid) + if err != nil { + return err + } + + if cs.Labels[api.ProjectLabel] == "" { + return errors.New("invalid project label") + } + + m[id] = libstack.EdgeStack{ + ID: id, + Name: cs.Labels[api.ProjectLabel], + } + } + } + } + + return nil + }); err != nil { + return nil, err + } + + return slices.Collect(maps.Values(m)), nil +} + +func addServiceLabels(project *types.Project, oneOff bool, edgeStackID portainer.EdgeStackID) { oneOffLabel := "False" if oneOff { oneOffLabel = "True" @@ -257,6 +306,11 @@ func addServiceLabels(project *types.Project, oneOff bool) { api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","), api.OneoffLabel: oneOffLabel, } + + if edgeStackID > 0 { + s.CustomLabels.Add(PortainerEdgeStackLabel, strconv.Itoa(int(edgeStackID))) + } + project.Services[i] = s } } diff --git a/pkg/libstack/libstack.go b/pkg/libstack/libstack.go index 5315db601..5b9b6effb 100644 --- a/pkg/libstack/libstack.go +++ b/pkg/libstack/libstack.go @@ -3,6 +3,8 @@ package libstack import ( "context" + portainer "github.com/portainer/portainer/api" + configtypes "github.com/docker/cli/cli/config/types" ) @@ -18,6 +20,7 @@ type Deployer interface { Validate(ctx context.Context, filePaths []string, options Options) error WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult Config(ctx context.Context, filePaths []string, options Options) ([]byte, error) + GetExistingEdgeStacks(ctx context.Context) ([]EdgeStack, error) } type Status string @@ -65,6 +68,7 @@ type DeployOptions struct { // When this is set, docker compose will output its logs to stdout AbortOnContainerExit bool RemoveOrphans bool + EdgeStackID portainer.EdgeStackID } type RunOptions struct { @@ -82,3 +86,8 @@ type RemoveOptions struct { Volumes bool } + +type EdgeStack struct { + ID int + Name string +}