fix(compose-unpacker): port swarm commands to use libstack [BE-12915] (#2890)

This commit is contained in:
Devon Steenberg
2026-06-15 11:43:29 +12:00
committed by GitHub
parent d37f3aa504
commit ef807950f1
2 changed files with 111 additions and 2 deletions
+11 -2
View File
@@ -49,6 +49,9 @@ type DeployOptions struct {
// true - query the registry (ResolveImageAlways)
// false - never contact the registry; reuse the existing digest (ResolveImageNever)
PullImage bool
// ForceRecreate increments ForceUpdate on every existing service so that
// Docker re-schedules all tasks even when nothing in the spec has changed.
ForceRecreate bool
}
// RemoveOptions extends Options with removal settings.
@@ -220,6 +223,7 @@ func deployStack(ctx context.Context, dockerCLI *command.DockerCli, filePaths []
services,
namespace,
options.PullImage,
options.ForceRecreate,
)
}
@@ -410,6 +414,7 @@ func deployServices(
services map[string]swarm.ServiceSpec,
namespace convert.Namespace,
pullImage bool,
forceRecreate bool,
) error {
existingServices, err := getStackServices(ctx, apiClient, namespace.Name())
if err != nil {
@@ -446,8 +451,12 @@ func deployServices(
}
}
// Preserve ForceUpdate so that tasks are not re-deployed if nothing changed.
serviceSpec.TaskTemplate.ForceUpdate = existing.Spec.TaskTemplate.ForceUpdate
if forceRecreate {
serviceSpec.TaskTemplate.ForceUpdate = existing.Spec.TaskTemplate.ForceUpdate + 1
} else {
// Preserve ForceUpdate so that tasks are not re-deployed if nothing changed.
serviceSpec.TaskTemplate.ForceUpdate = existing.Spec.TaskTemplate.ForceUpdate
}
response, err := apiClient.ServiceUpdate(ctx, existing.ID, existing.Version, serviceSpec, updateOpts)
if err != nil {
+100
View File
@@ -1,13 +1,16 @@
package swarm
import (
"context"
"os"
"slices"
"testing"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
dockerregistry "github.com/docker/docker/registry"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/pkg/libstack"
@@ -462,3 +465,100 @@ services:
})
}
}
type mockAPIClient struct {
client.APIClient
serviceListFn func(context.Context, swarm.ServiceListOptions) ([]swarm.Service, error)
serviceUpdateFn func(
context.Context,
string,
swarm.Version,
swarm.ServiceSpec,
swarm.ServiceUpdateOptions,
) (swarm.ServiceUpdateResponse, error)
}
func (m *mockAPIClient) ServiceList(ctx context.Context, opts swarm.ServiceListOptions) ([]swarm.Service, error) {
if m.serviceListFn == nil {
return nil, nil
}
return m.serviceListFn(ctx, opts)
}
func (m *mockAPIClient) ServiceUpdate(
ctx context.Context,
id string,
ver swarm.Version,
spec swarm.ServiceSpec,
opts swarm.ServiceUpdateOptions,
) (swarm.ServiceUpdateResponse, error) {
if m.serviceUpdateFn == nil {
return swarm.ServiceUpdateResponse{}, nil
}
return m.serviceUpdateFn(ctx, id, ver, spec, opts)
}
func Test_deployServices_forceRecreate(t *testing.T) {
t.Parallel()
const initialForceUpdate = uint64(3)
tests := []struct {
name string
forceRecreate bool
expectedForceUpdate uint64
}{
{"true increments ForceUpdate", true, initialForceUpdate + 1},
{"false preserves ForceUpdate", false, initialForceUpdate},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
existingSvc := swarm.Service{
ID: "svc-id-1",
Meta: swarm.Meta{Version: swarm.Version{Index: 10}},
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "mystack_web"},
TaskTemplate: swarm.TaskSpec{
ForceUpdate: initialForceUpdate,
ContainerSpec: &swarm.ContainerSpec{Image: "nginx:latest"},
},
},
}
var capturedForceUpdate uint64
mock := &mockAPIClient{
serviceListFn: func(_ context.Context, _ swarm.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{existingSvc}, nil
},
serviceUpdateFn: func(
_ context.Context,
_ string,
_ swarm.Version,
spec swarm.ServiceSpec,
_ swarm.ServiceUpdateOptions,
) (swarm.ServiceUpdateResponse, error) {
capturedForceUpdate = spec.TaskTemplate.ForceUpdate
return swarm.ServiceUpdateResponse{}, nil
},
}
services := map[string]swarm.ServiceSpec{
"web": {
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{Image: "nginx:latest"},
},
},
}
namespace := convert.NewNamespace("mystack")
err := deployServices(context.Background(), mock, nil, services, namespace, false, tt.forceRecreate)
require.NoError(t, err)
require.Equal(t, tt.expectedForceUpdate, capturedForceUpdate)
})
}
}