Merge pull request #6438 from thaJeztah/internalize_loader

cli/command/stack: internalize GetConfigDetails, LoadComposefile, RunDeploy, RunRemove
This commit is contained in:
Sebastiaan van Stijn
2025-09-08 17:31:28 +02:00
committed by GitHub
15 changed files with 454 additions and 745 deletions

View File

@ -1,6 +1,7 @@
package stack
import (
"context"
"fmt"
"strings"
"unicode"
@ -8,6 +9,9 @@ import (
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
// validateStackName checks if the provided string is a valid stack name (namespace).
@ -34,6 +38,12 @@ func quotesOrWhitespace(r rune) bool {
return unicode.IsSpace(r) || r == '"' || r == '\''
}
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
filter := opt.Value()
filter.Add("label", convert.LabelNamespace+"="+namespace)
@ -45,3 +55,23 @@ func getAllStacksFilter() filters.Args {
filter.Add("label", convert.LabelNamespace)
return filter
}
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
return apiclient.ServiceList(ctx, client.ServiceListOptions{Filters: getStackFilter(namespace)})
}
func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]network.Summary, error) {
return apiclient.NetworkList(ctx, client.NetworkListOptions{Filters: getStackFilter(namespace)})
}
func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) {
return apiclient.SecretList(ctx, client.SecretListOptions{Filters: getStackFilter(namespace)})
}
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
return apiclient.ConfigList(ctx, client.ConfigListOptions{Filters: getStackFilter(namespace)})
}
func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
return apiclient.TaskList(ctx, client.TaskListOptions{Filters: getStackFilter(namespace)})
}

View File

@ -6,28 +6,32 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/loader"
"github.com/docker/cli/cli/command/stack/options"
composeLoader "github.com/docker/cli/cli/compose/loader"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
// configOptions holds docker stack config options
type configOptions struct {
composeFiles []string
skipInterpolation bool
}
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
var opts options.Config
var opts configOptions
cmd := &cobra.Command{
Use: "config [OPTIONS]",
Short: "Outputs the final config file, after doing merges and interpolations",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCLI.In())
configDetails, err := getConfigDetails(opts.composeFiles, dockerCLI.In())
if err != nil {
return err
}
cfg, err := outputConfig(configDetails, opts.SkipInterpolation)
cfg, err := outputConfig(configDetails, opts.skipInterpolation)
if err != nil {
return err
}
@ -40,8 +44,8 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
flags.StringSliceVarP(&opts.composeFiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
flags.BoolVar(&opts.skipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
return cmd
}

View File

@ -1,16 +1,33 @@
package stack
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/loader"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/stack/swarm"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// deployOptions holds docker stack deploy options
type deployOptions struct {
composefiles []string
namespace string
resolveImage string
sendRegistryAuth bool
prune bool
detach bool
quiet bool
}
func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
var opts options.Deploy
var opts deployOptions
cmd := &cobra.Command{
Use: "deploy [OPTIONS] STACK",
@ -18,15 +35,15 @@ func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Deploy a new stack or update an existing stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Namespace = args[0]
if err := validateStackName(opts.Namespace); err != nil {
opts.namespace = args[0]
if err := validateStackName(opts.namespace); err != nil {
return err
}
config, err := loader.LoadComposefile(dockerCLI, opts)
config, err := loadComposeFile(dockerCLI, opts)
if err != nil {
return err
}
return swarm.RunDeploy(cmd.Context(), dockerCLI, cmd.Flags(), &opts, config)
return runDeploy(cmd.Context(), dockerCLI, cmd.Flags(), &opts, config)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCLI)(cmd, args, toComplete)
@ -35,15 +52,81 @@ func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
flags.StringSliceVarP(&opts.composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced")
flags.BoolVar(&opts.sendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
flags.SetAnnotation("prune", "version", []string{"1.27"})
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`", "`+swarm.ResolveImageChanged+`", "`+swarm.ResolveImageNever+`")`)
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`", "`+resolveImageChanged+`", "`+resolveImageNever+`")`)
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Suppress progress output")
flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output")
return cmd
}
// Resolve image constants
const (
resolveImageAlways = "always"
resolveImageChanged = "changed"
resolveImageNever = "never"
)
const defaultNetworkDriver = "overlay"
// runDeploy is the swarm implementation of docker stack deploy
func runDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *deployOptions, cfg *composetypes.Config) error {
switch opts.resolveImage {
case resolveImageAlways, resolveImageChanged, resolveImageNever:
// valid options.
default:
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions.LessThan(dockerCLI.Client().ClientVersion(), "1.30") {
// TODO(thaJeztah): should this error if "opts.ResolveImage" is already other (unsupported) values?
opts.resolveImage = resolveImageNever
}
if opts.detach && !flags.Changed("detach") {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
"In a future release, --detach=false will become the default.")
}
return deployCompose(ctx, dockerCLI, opts, cfg)
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
info, err := dockerCli.Client().Info(ctx)
if err != nil {
return err
}
if !info.Swarm.ControlAvailable {
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
}
return nil
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, services map[string]struct{}) {
apiClient := dockerCLI.Client()
oldServices, err := getStackServices(ctx, apiClient, namespace.Name())
if err != nil {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Failed to list services:", err)
}
toRemove := make([]swarm.Service, 0, len(oldServices))
for _, service := range oldServices {
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
toRemove = append(toRemove, service)
}
}
removeServices(ctx, dockerCLI, toRemove)
}

View File

@ -1,4 +1,4 @@
package swarm
package stack
import (
"context"
@ -7,8 +7,7 @@ import (
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/command"
servicecli "github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/service"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/moby/moby/api/types/container"
@ -17,17 +16,17 @@ import (
"github.com/moby/moby/client"
)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Deploy, config *composetypes.Config) error {
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *deployOptions, config *composetypes.Config) error {
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
return err
}
namespace := convert.NewNamespace(opts.Namespace)
namespace := convert.NewNamespace(opts.namespace)
if opts.Prune {
if opts.prune {
services := map[string]struct{}{}
for _, service := range config.Services {
services[service.Name] = struct{}{}
for _, svc := range config.Services {
services[svc.Name] = struct{}{}
}
pruneServices(ctx, dockerCli, namespace, services)
}
@ -62,16 +61,16 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Dep
return err
}
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
if err != nil {
return err
}
if opts.Detach {
if opts.detach {
return nil
}
return waitOnServices(ctx, dockerCli, serviceIDs, opts.Quiet)
return waitOnServices(ctx, dockerCli, serviceIDs, opts.quiet)
}
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@ -196,8 +195,8 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
}
existingServiceMap := make(map[string]swarm.Service)
for _, service := range existingServices {
existingServiceMap[service.Spec.Name] = service
for _, svc := range existingServices {
existingServiceMap[svc.Spec.Name] = svc
}
var serviceIDs []string
@ -217,17 +216,17 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
}
}
if service, exists := existingServiceMap[name]; exists {
_, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
if svc, exists := existingServiceMap[name]; exists {
_, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, svc.ID)
updateOpts := client.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
switch resolveImage {
case ResolveImageAlways:
case resolveImageAlways:
// image should be updated by the server using QueryRegistry
updateOpts.QueryRegistry = true
case ResolveImageChanged:
if image != service.Spec.Labels[convert.LabelImage] {
case resolveImageChanged:
if image != svc.Spec.Labels[convert.LabelImage] {
// Query the registry to resolve digest for the updated image
updateOpts.QueryRegistry = true
} else {
@ -235,24 +234,24 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image
}
default:
if image == service.Spec.Labels[convert.LabelImage] {
if image == svc.Spec.Labels[convert.LabelImage] {
// image has not changed; update the serviceSpec with the
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image
}
}
// Stack deploy does not have a `--force` option. Preserve existing
// ForceUpdate value so that tasks are not re-deployed if not updated.
// TODO move this to API client?
serviceSpec.TaskTemplate.ForceUpdate = service.Spec.TaskTemplate.ForceUpdate
serviceSpec.TaskTemplate.ForceUpdate = svc.Spec.TaskTemplate.ForceUpdate
response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
response, err := apiClient.ServiceUpdate(ctx, svc.ID, svc.Version, serviceSpec, updateOpts)
if err != nil {
return nil, fmt.Errorf("failed to update service %s: %w", name, err)
}
@ -261,12 +260,12 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
_, _ = fmt.Fprintln(dockerCLI.Err(), warning)
}
serviceIDs = append(serviceIDs, service.ID)
serviceIDs = append(serviceIDs, svc.ID)
} else {
_, _ = fmt.Fprintln(out, "Creating service", name)
// query registry if flag disabling it was not set
queryRegistry := resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged
queryRegistry := resolveImage == resolveImageAlways || resolveImage == resolveImageChanged
response, err := apiClient.ServiceCreate(ctx, serviceSpec, client.ServiceCreateOptions{
EncodedRegistryAuth: encodedAuth,
@ -286,7 +285,7 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error {
var errs []error
for _, serviceID := range serviceIDs {
if err := servicecli.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
if err := service.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
}
}

View File

@ -1,4 +1,4 @@
package swarm
package stack
import (
"context"

View File

@ -1,11 +1,16 @@
package stack
import (
"context"
"io"
"testing"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestDeployWithEmptyName(t *testing.T) {
@ -16,3 +21,101 @@ func TestDeployWithEmptyName(t *testing.T) {
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
}
func TestPruneServices(t *testing.T) {
ctx := context.Background()
namespace := convert.NewNamespace("foo")
services := map[string]struct{}{
"new": {},
"keep": {},
}
apiClient := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}}
dockerCli := test.NewFakeCli(apiClient)
pruneServices(ctx, dockerCli, namespace, services)
assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), apiClient.removedServices))
}
// TestServiceUpdateResolveImageChanged tests that the service's
// image digest, and "ForceUpdate" is preserved if the image did not change in
// the compose file
func TestServiceUpdateResolveImageChanged(t *testing.T) {
namespace := convert.NewNamespace("mystack")
var (
receivedOptions client.ServiceUpdateOptions
receivedService swarm.ServiceSpec
)
fakeCli := test.NewFakeCli(&fakeClient{
serviceListFunc: func(options client.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{
{
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: namespace.Name() + "_myservice",
Labels: map[string]string{"com.docker.stack.image": "foobar:1.2.3"},
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: "foobar:1.2.3@sha256:deadbeef",
},
ForceUpdate: 123,
},
},
},
}, nil
},
serviceUpdateFunc: func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options client.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
receivedOptions = options
receivedService = service
return swarm.ServiceUpdateResponse{}, nil
},
})
testcases := []struct {
image string
expectedQueryRegistry bool
expectedImage string
expectedForceUpdate uint64
}{
// Image not changed
{
image: "foobar:1.2.3",
expectedQueryRegistry: false,
expectedImage: "foobar:1.2.3@sha256:deadbeef",
expectedForceUpdate: 123,
},
// Image changed
{
image: "foobar:1.2.4",
expectedQueryRegistry: true,
expectedImage: "foobar:1.2.4",
expectedForceUpdate: 123,
},
}
ctx := context.Background()
for _, tc := range testcases {
t.Run(tc.image, func(t *testing.T) {
spec := map[string]swarm.ServiceSpec{
"myservice": {
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: tc.image,
},
},
},
}
_, err := deployServices(ctx, fakeCli, spec, namespace, false, resolveImageChanged)
assert.NilError(t, err)
assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry))
assert.Check(t, is.Equal(receivedService.TaskTemplate.ContainerSpec.Image, tc.expectedImage))
assert.Check(t, is.Equal(receivedService.TaskTemplate.ForceUpdate, tc.expectedForceUpdate))
receivedService = swarm.ServiceSpec{}
receivedOptions = client.ServiceUpdateOptions{}
})
}
}

View File

@ -1,7 +1,7 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package loader
package stack
import (
"fmt"
@ -14,18 +14,15 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
//
// Deprecated: this function was for internal use and will be removed in the next release.
func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) {
configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In())
// loadComposeFile parse the composefile specified in the cli and returns its configOptions and version.
func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes.Config, error) {
configDetails, err := getConfigDetails(opts.composefiles, streams.In())
if err != nil {
return nil, err
}
@ -43,13 +40,13 @@ func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
_, _ = fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
_, _ = fmt.Fprintf(streams.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
_, _ = fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
_, _ = fmt.Fprintf(streams.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
@ -85,10 +82,8 @@ func propertyWarnings(properties map[string]string) string {
return strings.Join(msgs, "\n\n")
}
// GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails
//
// Deprecated: this function was for internal use and will be removed in the next release.
func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
// getConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails
func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
if len(composefiles) == 0 {

View File

@ -1,4 +1,4 @@
package loader
package stack
import (
"os"
@ -22,7 +22,7 @@ services:
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
defer file.Remove()
details, err := GetConfigDetails([]string{file.Path()}, nil)
details, err := getConfigDetails([]string{file.Path()}, nil)
assert.NilError(t, err)
assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir))
assert.Assert(t, is.Len(details.ConfigFiles, 1))
@ -37,7 +37,7 @@ services:
foo:
image: alpine:3.5
`
details, err := GetConfigDetails([]string{"-"}, strings.NewReader(content))
details, err := getConfigDetails([]string{"-"}, strings.NewReader(content))
assert.NilError(t, err)
cwd, err := os.Getwd()
assert.NilError(t, err)

View File

@ -1,30 +0,0 @@
package options
// Deploy holds docker stack deploy options
//
// Deprecated: this type was for internal use and will be removed in the next release.
type Deploy struct {
Composefiles []string
Namespace string
ResolveImage string
SendRegistryAuth bool
Prune bool
Detach bool
Quiet bool
}
// Config holds docker stack config options
//
// Deprecated: this type was for internal use and will be removed in the next release.
type Config struct {
Composefiles []string
SkipInterpolation bool
}
// Remove holds docker stack remove options
//
// Deprecated: this type was for internal use and will be removed in the next release.
type Remove struct {
Namespaces []string
Detach bool
}

View File

@ -1,15 +1,28 @@
package stack
import (
"context"
"errors"
"fmt"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/stack/swarm"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
// removeOptions holds docker stack remove options
type removeOptions struct {
namespaces []string
detach bool
}
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
var opts options.Remove
var opts removeOptions
cmd := &cobra.Command{
Use: "rm [OPTIONS] STACK [STACK...]",
@ -17,11 +30,11 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove one or more stacks",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Namespaces = args
if err := validateStackNames(opts.Namespaces); err != nil {
opts.namespaces = args
if err := validateStackNames(opts.namespaces); err != nil {
return err
}
return swarm.RunRemove(cmd.Context(), dockerCLI, opts)
return runRemove(cmd.Context(), dockerCLI, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCLI)(cmd, args, toComplete)
@ -30,6 +43,161 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Do not wait for stack removal")
flags.BoolVarP(&opts.detach, "detach", "d", true, "Do not wait for stack removal")
return cmd
}
// runRemove is the swarm implementation of docker stack remove.
func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions) error {
apiClient := dockerCli.Client()
var errs []error
for _, namespace := range opts.namespaces {
services, err := getStackServices(ctx, apiClient, namespace)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, apiClient, namespace)
if err != nil {
return err
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, apiClient, namespace)
if err != nil {
return err
}
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, apiClient, namespace)
if err != nil {
return err
}
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
_, _ = fmt.Fprintln(dockerCli.Err(), "Nothing found in stack:", namespace)
continue
}
// TODO(thaJeztah): change this "hasError" boolean to return a (multi-)error for each of these functions instead.
hasError := removeServices(ctx, dockerCli, services)
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
if hasError {
errs = append(errs, errors.New("failed to remove some resources from stack: "+namespace))
continue
}
if !opts.detach {
err = waitOnTasks(ctx, apiClient, namespace)
if err != nil {
errs = append(errs, fmt.Errorf("failed to wait on tasks of stack: %s: %w", namespace, err))
}
}
}
return errors.Join(errs...)
}
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
return func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
}
}
func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool {
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name)
if err := dockerCLI.Client().ServiceRemove(ctx, service.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove service %s: %s", service.ID, err)
}
}
return hasError
}
func removeNetworks(ctx context.Context, dockerCLI command.Cli, networks []network.Summary) bool {
var hasError bool
for _, nw := range networks {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing network", nw.Name)
if err := dockerCLI.Client().NetworkRemove(ctx, nw.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove network %s: %s", nw.ID, err)
}
}
return hasError
}
func removeSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.Secret) bool {
var hasError bool
for _, secret := range secrets {
_, _ = fmt.Fprintln(dockerCli.Out(), "Removing secret", secret.Spec.Name)
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
}
}
return hasError
}
func removeConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.Config) bool {
var hasError bool
for _, config := range configs {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing config", config.Spec.Name)
if err := dockerCLI.Client().ConfigRemove(ctx, config.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove config %s: %s", config.ID, err)
}
}
return hasError
}
var numberedStates = map[swarm.TaskState]int64{
swarm.TaskStateNew: 1,
swarm.TaskStateAllocated: 2,
swarm.TaskStatePending: 3,
swarm.TaskStateAssigned: 4,
swarm.TaskStateAccepted: 5,
swarm.TaskStatePreparing: 6,
swarm.TaskStateReady: 7,
swarm.TaskStateStarting: 8,
swarm.TaskStateRunning: 9,
swarm.TaskStateComplete: 10,
swarm.TaskStateShutdown: 11,
swarm.TaskStateFailed: 12,
swarm.TaskStateRejected: 13,
}
func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
}
func waitOnTasks(ctx context.Context, apiClient client.APIClient, namespace string) error {
terminalStatesReached := 0
for {
tasks, err := getStackTasks(ctx, apiClient, namespace)
if err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for _, task := range tasks {
if terminalState(task.Status.State) {
terminalStatesReached++
break
}
}
if terminalStatesReached == len(tasks) {
break
}
}
return nil
}

View File

@ -1,241 +0,0 @@
package swarm
import (
"context"
"strings"
"github.com/docker/cli/cli/compose/convert"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
type fakeClient struct {
client.Client
version string
services []string
networks []string
secrets []string
configs []string
removedServices []string
removedNetworks []string
removedSecrets []string
removedConfigs []string
serviceListFunc func(options client.ServiceListOptions) ([]swarm.Service, error)
networkListFunc func(options client.NetworkListOptions) ([]network.Summary, error)
secretListFunc func(options client.SecretListOptions) ([]swarm.Secret, error)
configListFunc func(options client.ConfigListOptions) ([]swarm.Config, error)
nodeListFunc func(options client.NodeListOptions) ([]swarm.Node, error)
taskListFunc func(options client.TaskListOptions) ([]swarm.Task, error)
nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error)
serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options client.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error)
serviceRemoveFunc func(serviceID string) error
networkRemoveFunc func(networkID string) error
secretRemoveFunc func(secretID string) error
configRemoveFunc func(configID string) error
}
func (*fakeClient) ServerVersion(context.Context) (types.Version, error) {
return types.Version{
Version: "docker-dev",
APIVersion: client.MaxAPIVersion,
}, nil
}
func (cli *fakeClient) ClientVersion() string {
return cli.version
}
func (cli *fakeClient) ServiceList(_ context.Context, options client.ServiceListOptions) ([]swarm.Service, error) {
if cli.serviceListFunc != nil {
return cli.serviceListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
servicesList := []swarm.Service{}
for _, name := range cli.services {
if belongToNamespace(name, namespace) {
servicesList = append(servicesList, serviceFromName(name))
}
}
return servicesList, nil
}
func (cli *fakeClient) NetworkList(_ context.Context, options client.NetworkListOptions) ([]network.Summary, error) {
if cli.networkListFunc != nil {
return cli.networkListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
networksList := []network.Summary{}
for _, name := range cli.networks {
if belongToNamespace(name, namespace) {
networksList = append(networksList, networkFromName(name))
}
}
return networksList, nil
}
func (cli *fakeClient) SecretList(_ context.Context, options client.SecretListOptions) ([]swarm.Secret, error) {
if cli.secretListFunc != nil {
return cli.secretListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
secretsList := []swarm.Secret{}
for _, name := range cli.secrets {
if belongToNamespace(name, namespace) {
secretsList = append(secretsList, secretFromName(name))
}
}
return secretsList, nil
}
func (cli *fakeClient) ConfigList(_ context.Context, options client.ConfigListOptions) ([]swarm.Config, error) {
if cli.configListFunc != nil {
return cli.configListFunc(options)
}
namespace := namespaceFromFilters(options.Filters)
configsList := []swarm.Config{}
for _, name := range cli.configs {
if belongToNamespace(name, namespace) {
configsList = append(configsList, configFromName(name))
}
}
return configsList, nil
}
func (cli *fakeClient) TaskList(_ context.Context, options client.TaskListOptions) ([]swarm.Task, error) {
if cli.taskListFunc != nil {
return cli.taskListFunc(options)
}
return []swarm.Task{}, nil
}
func (cli *fakeClient) NodeList(_ context.Context, options client.NodeListOptions) ([]swarm.Node, error) {
if cli.nodeListFunc != nil {
return cli.nodeListFunc(options)
}
return []swarm.Node{}, nil
}
func (cli *fakeClient) NodeInspectWithRaw(_ context.Context, ref string) (swarm.Node, []byte, error) {
if cli.nodeInspectWithRaw != nil {
return cli.nodeInspectWithRaw(ref)
}
return swarm.Node{}, nil, nil
}
func (cli *fakeClient) ServiceUpdate(_ context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options client.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
if cli.serviceUpdateFunc != nil {
return cli.serviceUpdateFunc(serviceID, version, service, options)
}
return swarm.ServiceUpdateResponse{}, nil
}
func (cli *fakeClient) ServiceRemove(_ context.Context, serviceID string) error {
if cli.serviceRemoveFunc != nil {
return cli.serviceRemoveFunc(serviceID)
}
cli.removedServices = append(cli.removedServices, serviceID)
return nil
}
func (cli *fakeClient) NetworkRemove(_ context.Context, networkID string) error {
if cli.networkRemoveFunc != nil {
return cli.networkRemoveFunc(networkID)
}
cli.removedNetworks = append(cli.removedNetworks, networkID)
return nil
}
func (cli *fakeClient) SecretRemove(_ context.Context, secretID string) error {
if cli.secretRemoveFunc != nil {
return cli.secretRemoveFunc(secretID)
}
cli.removedSecrets = append(cli.removedSecrets, secretID)
return nil
}
func (cli *fakeClient) ConfigRemove(_ context.Context, configID string) error {
if cli.configRemoveFunc != nil {
return cli.configRemoveFunc(configID)
}
cli.removedConfigs = append(cli.removedConfigs, configID)
return nil
}
func serviceFromName(name string) swarm.Service {
return swarm.Service{
ID: "ID-" + name,
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: name},
},
}
}
func networkFromName(name string) network.Summary {
return network.Summary{
Network: network.Network{
ID: "ID-" + name,
Name: name,
},
}
}
func secretFromName(name string) swarm.Secret {
return swarm.Secret{
ID: "ID-" + name,
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{Name: name},
},
}
}
func configFromName(name string) swarm.Config {
return swarm.Config{
ID: "ID-" + name,
Spec: swarm.ConfigSpec{
Annotations: swarm.Annotations{Name: name},
},
}
}
func namespaceFromFilters(fltrs filters.Args) string {
label := fltrs.Get("label")[0]
return strings.TrimPrefix(label, convert.LabelNamespace+"=")
}
func belongToNamespace(id, namespace string) bool {
return strings.HasPrefix(id, namespace+"_")
}
func objectName(namespace, name string) string {
return namespace + "_" + name
}
func objectID(name string) string {
return "ID-" + name
}
func buildObjectIDs(objectNames []string) []string {
IDs := make([]string, len(objectNames))
for i, name := range objectNames {
IDs[i] = objectID(name)
}
return IDs
}

View File

@ -1,37 +0,0 @@
package swarm
import (
"context"
"github.com/docker/cli/cli/compose/convert"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", convert.LabelNamespace+"="+namespace)
return filter
}
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
return apiclient.ServiceList(ctx, client.ServiceListOptions{Filters: getStackFilter(namespace)})
}
func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]network.Summary, error) {
return apiclient.NetworkList(ctx, client.NetworkListOptions{Filters: getStackFilter(namespace)})
}
func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) {
return apiclient.SecretList(ctx, client.SecretListOptions{Filters: getStackFilter(namespace)})
}
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
return apiclient.ConfigList(ctx, client.ConfigListOptions{Filters: getStackFilter(namespace)})
}
func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
return apiclient.TaskList(ctx, client.TaskListOptions{Filters: getStackFilter(namespace)})
}

View File

@ -1,82 +0,0 @@
package swarm
import (
"context"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
// Resolve image constants
const (
defaultNetworkDriver = "overlay"
ResolveImageAlways = "always"
ResolveImageChanged = "changed"
ResolveImageNever = "never"
)
// RunDeploy is the swarm implementation of docker stack deploy
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *options.Deploy, cfg *composetypes.Config) error {
switch opts.ResolveImage {
case ResolveImageAlways, ResolveImageChanged, ResolveImageNever:
// valid options.
default:
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage)
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions.LessThan(dockerCLI.Client().ClientVersion(), "1.30") {
// TODO(thaJeztah): should this error if "opts.ResolveImage" is already other (unsupported) values?
opts.ResolveImage = ResolveImageNever
}
if opts.Detach && !flags.Changed("detach") {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
"In a future release, --detach=false will become the default.")
}
return deployCompose(ctx, dockerCLI, opts, cfg)
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
info, err := dockerCli.Client().Info(ctx)
if err != nil {
return err
}
if !info.Swarm.ControlAvailable {
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
}
return nil
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, services map[string]struct{}) {
apiClient := dockerCLI.Client()
oldServices, err := getStackServices(ctx, apiClient, namespace.Name())
if err != nil {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Failed to list services:", err)
}
toRemove := make([]swarm.Service, 0, len(oldServices))
for _, service := range oldServices {
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
toRemove = append(toRemove, service)
}
}
removeServices(ctx, dockerCLI, toRemove)
}

View File

@ -1,111 +0,0 @@
package swarm
import (
"context"
"testing"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestPruneServices(t *testing.T) {
ctx := context.Background()
namespace := convert.NewNamespace("foo")
services := map[string]struct{}{
"new": {},
"keep": {},
}
apiClient := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}}
dockerCli := test.NewFakeCli(apiClient)
pruneServices(ctx, dockerCli, namespace, services)
assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), apiClient.removedServices))
}
// TestServiceUpdateResolveImageChanged tests that the service's
// image digest, and "ForceUpdate" is preserved if the image did not change in
// the compose file
func TestServiceUpdateResolveImageChanged(t *testing.T) {
namespace := convert.NewNamespace("mystack")
var (
receivedOptions client.ServiceUpdateOptions
receivedService swarm.ServiceSpec
)
fakeCli := test.NewFakeCli(&fakeClient{
serviceListFunc: func(options client.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{
{
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: namespace.Name() + "_myservice",
Labels: map[string]string{"com.docker.stack.image": "foobar:1.2.3"},
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: "foobar:1.2.3@sha256:deadbeef",
},
ForceUpdate: 123,
},
},
},
}, nil
},
serviceUpdateFunc: func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options client.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
receivedOptions = options
receivedService = service
return swarm.ServiceUpdateResponse{}, nil
},
})
testcases := []struct {
image string
expectedQueryRegistry bool
expectedImage string
expectedForceUpdate uint64
}{
// Image not changed
{
image: "foobar:1.2.3",
expectedQueryRegistry: false,
expectedImage: "foobar:1.2.3@sha256:deadbeef",
expectedForceUpdate: 123,
},
// Image changed
{
image: "foobar:1.2.4",
expectedQueryRegistry: true,
expectedImage: "foobar:1.2.4",
expectedForceUpdate: 123,
},
}
ctx := context.Background()
for _, tc := range testcases {
t.Run(tc.image, func(t *testing.T) {
spec := map[string]swarm.ServiceSpec{
"myservice": {
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: tc.image,
},
},
},
}
_, err := deployServices(ctx, fakeCli, spec, namespace, false, ResolveImageChanged)
assert.NilError(t, err)
assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry))
assert.Check(t, is.Equal(receivedService.TaskTemplate.ContainerSpec.Image, tc.expectedImage))
assert.Check(t, is.Equal(receivedService.TaskTemplate.ForceUpdate, tc.expectedForceUpdate))
receivedService = swarm.ServiceSpec{}
receivedOptions = client.ServiceUpdateOptions{}
})
}
}

View File

@ -1,172 +0,0 @@
package swarm
import (
"context"
"errors"
"fmt"
"sort"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
)
// RunRemove is the swarm implementation of docker stack remove
//
// Deprecated: this function was for internal use and will be removed in the next release.
func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) error {
apiClient := dockerCli.Client()
var errs []error
for _, namespace := range opts.Namespaces {
services, err := getStackServices(ctx, apiClient, namespace)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, apiClient, namespace)
if err != nil {
return err
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, apiClient, namespace)
if err != nil {
return err
}
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, apiClient, namespace)
if err != nil {
return err
}
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
_, _ = fmt.Fprintln(dockerCli.Err(), "Nothing found in stack:", namespace)
continue
}
// TODO(thaJeztah): change this "hasError" boolean to return a (multi-)error for each of these functions instead.
hasError := removeServices(ctx, dockerCli, services)
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
if hasError {
errs = append(errs, errors.New("failed to remove some resources from stack: "+namespace))
continue
}
if !opts.Detach {
err = waitOnTasks(ctx, apiClient, namespace)
if err != nil {
errs = append(errs, fmt.Errorf("failed to wait on tasks of stack: %s: %w", namespace, err))
}
}
}
return errors.Join(errs...)
}
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
return func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
}
}
func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool {
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name)
if err := dockerCLI.Client().ServiceRemove(ctx, service.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove service %s: %s", service.ID, err)
}
}
return hasError
}
func removeNetworks(ctx context.Context, dockerCLI command.Cli, networks []network.Summary) bool {
var hasError bool
for _, nw := range networks {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing network", nw.Name)
if err := dockerCLI.Client().NetworkRemove(ctx, nw.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove network %s: %s", nw.ID, err)
}
}
return hasError
}
func removeSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.Secret) bool {
var hasError bool
for _, secret := range secrets {
_, _ = fmt.Fprintln(dockerCli.Out(), "Removing secret", secret.Spec.Name)
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
}
}
return hasError
}
func removeConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.Config) bool {
var hasError bool
for _, config := range configs {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Removing config", config.Spec.Name)
if err := dockerCLI.Client().ConfigRemove(ctx, config.ID); err != nil {
hasError = true
_, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove config %s: %s", config.ID, err)
}
}
return hasError
}
var numberedStates = map[swarm.TaskState]int64{
swarm.TaskStateNew: 1,
swarm.TaskStateAllocated: 2,
swarm.TaskStatePending: 3,
swarm.TaskStateAssigned: 4,
swarm.TaskStateAccepted: 5,
swarm.TaskStatePreparing: 6,
swarm.TaskStateReady: 7,
swarm.TaskStateStarting: 8,
swarm.TaskStateRunning: 9,
swarm.TaskStateComplete: 10,
swarm.TaskStateShutdown: 11,
swarm.TaskStateFailed: 12,
swarm.TaskStateRejected: 13,
}
func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
}
func waitOnTasks(ctx context.Context, apiClient client.APIClient, namespace string) error {
terminalStatesReached := 0
for {
tasks, err := getStackTasks(ctx, apiClient, namespace)
if err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for _, task := range tasks {
if terminalState(task.Status.State) {
terminalStatesReached++
break
}
}
if terminalStatesReached == len(tasks) {
break
}
}
return nil
}