Skip to content

Commit 183337d

Browse files
authored
Merge pull request #6438 from thaJeztah/internalize_loader
cli/command/stack: internalize GetConfigDetails, LoadComposefile, RunDeploy, RunRemove
2 parents d62d370 + 26bb688 commit 183337d

File tree

15 files changed

+454
-745
lines changed

15 files changed

+454
-745
lines changed

cli/command/stack/common.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package stack
22

33
import (
4+
"context"
45
"fmt"
56
"strings"
67
"unicode"
78

89
"github.com/docker/cli/cli/compose/convert"
910
"github.com/docker/cli/opts"
1011
"github.com/moby/moby/api/types/filters"
12+
"github.com/moby/moby/api/types/network"
13+
"github.com/moby/moby/api/types/swarm"
14+
"github.com/moby/moby/client"
1115
)
1216

1317
// validateStackName checks if the provided string is a valid stack name (namespace).
@@ -34,6 +38,12 @@ func quotesOrWhitespace(r rune) bool {
3438
return unicode.IsSpace(r) || r == '"' || r == '\''
3539
}
3640

41+
func getStackFilter(namespace string) filters.Args {
42+
filter := filters.NewArgs()
43+
filter.Add("label", convert.LabelNamespace+"="+namespace)
44+
return filter
45+
}
46+
3747
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
3848
filter := opt.Value()
3949
filter.Add("label", convert.LabelNamespace+"="+namespace)
@@ -45,3 +55,23 @@ func getAllStacksFilter() filters.Args {
4555
filter.Add("label", convert.LabelNamespace)
4656
return filter
4757
}
58+
59+
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
60+
return apiclient.ServiceList(ctx, client.ServiceListOptions{Filters: getStackFilter(namespace)})
61+
}
62+
63+
func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]network.Summary, error) {
64+
return apiclient.NetworkList(ctx, client.NetworkListOptions{Filters: getStackFilter(namespace)})
65+
}
66+
67+
func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) {
68+
return apiclient.SecretList(ctx, client.SecretListOptions{Filters: getStackFilter(namespace)})
69+
}
70+
71+
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
72+
return apiclient.ConfigList(ctx, client.ConfigListOptions{Filters: getStackFilter(namespace)})
73+
}
74+
75+
func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
76+
return apiclient.TaskList(ctx, client.TaskListOptions{Filters: getStackFilter(namespace)})
77+
}

cli/command/stack/config.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,32 @@ import (
66

77
"github.com/docker/cli/cli"
88
"github.com/docker/cli/cli/command"
9-
"github.com/docker/cli/cli/command/stack/loader"
10-
"github.com/docker/cli/cli/command/stack/options"
119
composeLoader "github.com/docker/cli/cli/compose/loader"
1210
composetypes "github.com/docker/cli/cli/compose/types"
1311
"github.com/spf13/cobra"
1412
"gopkg.in/yaml.v3"
1513
)
1614

15+
// configOptions holds docker stack config options
16+
type configOptions struct {
17+
composeFiles []string
18+
skipInterpolation bool
19+
}
20+
1721
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
18-
var opts options.Config
22+
var opts configOptions
1923

2024
cmd := &cobra.Command{
2125
Use: "config [OPTIONS]",
2226
Short: "Outputs the final config file, after doing merges and interpolations",
2327
Args: cli.NoArgs,
2428
RunE: func(cmd *cobra.Command, args []string) error {
25-
configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCLI.In())
29+
configDetails, err := getConfigDetails(opts.composeFiles, dockerCLI.In())
2630
if err != nil {
2731
return err
2832
}
2933

30-
cfg, err := outputConfig(configDetails, opts.SkipInterpolation)
34+
cfg, err := outputConfig(configDetails, opts.skipInterpolation)
3135
if err != nil {
3236
return err
3337
}
@@ -40,8 +44,8 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
4044
}
4145

4246
flags := cmd.Flags()
43-
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
44-
flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
47+
flags.StringSliceVarP(&opts.composeFiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
48+
flags.BoolVar(&opts.skipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config")
4549
return cmd
4650
}
4751

cli/command/stack/deploy.go

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
11
package stack
22

33
import (
4+
"context"
5+
"fmt"
6+
47
"github.com/docker/cli/cli"
58
"github.com/docker/cli/cli/command"
6-
"github.com/docker/cli/cli/command/stack/loader"
7-
"github.com/docker/cli/cli/command/stack/options"
8-
"github.com/docker/cli/cli/command/stack/swarm"
9+
"github.com/docker/cli/cli/compose/convert"
10+
composetypes "github.com/docker/cli/cli/compose/types"
11+
"github.com/moby/moby/api/types/swarm"
12+
"github.com/moby/moby/api/types/versions"
13+
"github.com/pkg/errors"
914
"github.com/spf13/cobra"
15+
"github.com/spf13/pflag"
1016
)
1117

18+
// deployOptions holds docker stack deploy options
19+
type deployOptions struct {
20+
composefiles []string
21+
namespace string
22+
resolveImage string
23+
sendRegistryAuth bool
24+
prune bool
25+
detach bool
26+
quiet bool
27+
}
28+
1229
func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
13-
var opts options.Deploy
30+
var opts deployOptions
1431

1532
cmd := &cobra.Command{
1633
Use: "deploy [OPTIONS] STACK",
1734
Aliases: []string{"up"},
1835
Short: "Deploy a new stack or update an existing stack",
1936
Args: cli.ExactArgs(1),
2037
RunE: func(cmd *cobra.Command, args []string) error {
21-
opts.Namespace = args[0]
22-
if err := validateStackName(opts.Namespace); err != nil {
38+
opts.namespace = args[0]
39+
if err := validateStackName(opts.namespace); err != nil {
2340
return err
2441
}
25-
config, err := loader.LoadComposefile(dockerCLI, opts)
42+
config, err := loadComposeFile(dockerCLI, opts)
2643
if err != nil {
2744
return err
2845
}
29-
return swarm.RunDeploy(cmd.Context(), dockerCLI, cmd.Flags(), &opts, config)
46+
return runDeploy(cmd.Context(), dockerCLI, cmd.Flags(), &opts, config)
3047
},
3148
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
3249
return completeNames(dockerCLI)(cmd, args, toComplete)
@@ -35,15 +52,81 @@ func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
3552
}
3653

3754
flags := cmd.Flags()
38-
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
55+
flags.StringSliceVarP(&opts.composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
3956
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
40-
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
41-
flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced")
57+
flags.BoolVar(&opts.sendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
58+
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
4259
flags.SetAnnotation("prune", "version", []string{"1.27"})
43-
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
44-
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`", "`+swarm.ResolveImageChanged+`", "`+swarm.ResolveImageNever+`")`)
60+
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
61+
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`", "`+resolveImageChanged+`", "`+resolveImageNever+`")`)
4562
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
46-
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
47-
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Suppress progress output")
63+
flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
64+
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output")
4865
return cmd
4966
}
67+
68+
// Resolve image constants
69+
const (
70+
resolveImageAlways = "always"
71+
resolveImageChanged = "changed"
72+
resolveImageNever = "never"
73+
)
74+
75+
const defaultNetworkDriver = "overlay"
76+
77+
// runDeploy is the swarm implementation of docker stack deploy
78+
func runDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *deployOptions, cfg *composetypes.Config) error {
79+
switch opts.resolveImage {
80+
case resolveImageAlways, resolveImageChanged, resolveImageNever:
81+
// valid options.
82+
default:
83+
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
84+
}
85+
86+
// client side image resolution should not be done when the supported
87+
// server version is older than 1.30
88+
if versions.LessThan(dockerCLI.Client().ClientVersion(), "1.30") {
89+
// TODO(thaJeztah): should this error if "opts.ResolveImage" is already other (unsupported) values?
90+
opts.resolveImage = resolveImageNever
91+
}
92+
93+
if opts.detach && !flags.Changed("detach") {
94+
_, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
95+
"In a future release, --detach=false will become the default.")
96+
}
97+
98+
return deployCompose(ctx, dockerCLI, opts, cfg)
99+
}
100+
101+
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
102+
// a swarm manager. This is necessary because we must create networks before we
103+
// create services, but the API call for creating a network does not return a
104+
// proper status code when it can't create a network in the "global" scope.
105+
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
106+
info, err := dockerCli.Client().Info(ctx)
107+
if err != nil {
108+
return err
109+
}
110+
if !info.Swarm.ControlAvailable {
111+
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")
112+
}
113+
return nil
114+
}
115+
116+
// pruneServices removes services that are no longer referenced in the source
117+
func pruneServices(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, services map[string]struct{}) {
118+
apiClient := dockerCLI.Client()
119+
120+
oldServices, err := getStackServices(ctx, apiClient, namespace.Name())
121+
if err != nil {
122+
_, _ = fmt.Fprintln(dockerCLI.Err(), "Failed to list services:", err)
123+
}
124+
125+
toRemove := make([]swarm.Service, 0, len(oldServices))
126+
for _, service := range oldServices {
127+
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
128+
toRemove = append(toRemove, service)
129+
}
130+
}
131+
removeServices(ctx, dockerCLI, toRemove)
132+
}

cli/command/stack/swarm/deploy_composefile.go renamed to cli/command/stack/deploy_composefile.go

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package swarm
1+
package stack
22

33
import (
44
"context"
@@ -7,8 +7,7 @@ import (
77

88
"github.com/containerd/errdefs"
99
"github.com/docker/cli/cli/command"
10-
servicecli "github.com/docker/cli/cli/command/service"
11-
"github.com/docker/cli/cli/command/stack/options"
10+
"github.com/docker/cli/cli/command/service"
1211
"github.com/docker/cli/cli/compose/convert"
1312
composetypes "github.com/docker/cli/cli/compose/types"
1413
"github.com/moby/moby/api/types/container"
@@ -17,17 +16,17 @@ import (
1716
"github.com/moby/moby/client"
1817
)
1918

20-
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Deploy, config *composetypes.Config) error {
19+
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *deployOptions, config *composetypes.Config) error {
2120
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
2221
return err
2322
}
2423

25-
namespace := convert.NewNamespace(opts.Namespace)
24+
namespace := convert.NewNamespace(opts.namespace)
2625

27-
if opts.Prune {
26+
if opts.prune {
2827
services := map[string]struct{}{}
29-
for _, service := range config.Services {
30-
services[service.Name] = struct{}{}
28+
for _, svc := range config.Services {
29+
services[svc.Name] = struct{}{}
3130
}
3231
pruneServices(ctx, dockerCli, namespace, services)
3332
}
@@ -62,16 +61,16 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Dep
6261
return err
6362
}
6463

65-
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
64+
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
6665
if err != nil {
6766
return err
6867
}
6968

70-
if opts.Detach {
69+
if opts.detach {
7170
return nil
7271
}
7372

74-
return waitOnServices(ctx, dockerCli, serviceIDs, opts.Quiet)
73+
return waitOnServices(ctx, dockerCli, serviceIDs, opts.quiet)
7574
}
7675

7776
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@@ -196,8 +195,8 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
196195
}
197196

198197
existingServiceMap := make(map[string]swarm.Service)
199-
for _, service := range existingServices {
200-
existingServiceMap[service.Spec.Name] = service
198+
for _, svc := range existingServices {
199+
existingServiceMap[svc.Spec.Name] = svc
201200
}
202201

203202
var serviceIDs []string
@@ -217,42 +216,42 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
217216
}
218217
}
219218

220-
if service, exists := existingServiceMap[name]; exists {
221-
_, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
219+
if svc, exists := existingServiceMap[name]; exists {
220+
_, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, svc.ID)
222221

223222
updateOpts := client.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
224223

225224
switch resolveImage {
226-
case ResolveImageAlways:
225+
case resolveImageAlways:
227226
// image should be updated by the server using QueryRegistry
228227
updateOpts.QueryRegistry = true
229-
case ResolveImageChanged:
230-
if image != service.Spec.Labels[convert.LabelImage] {
228+
case resolveImageChanged:
229+
if image != svc.Spec.Labels[convert.LabelImage] {
231230
// Query the registry to resolve digest for the updated image
232231
updateOpts.QueryRegistry = true
233232
} else {
234233
// image has not changed; update the serviceSpec with the
235234
// existing information that was set by QueryRegistry on the
236235
// previous deploy. Otherwise this will trigger an incorrect
237236
// service update.
238-
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
237+
serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image
239238
}
240239
default:
241-
if image == service.Spec.Labels[convert.LabelImage] {
240+
if image == svc.Spec.Labels[convert.LabelImage] {
242241
// image has not changed; update the serviceSpec with the
243242
// existing information that was set by QueryRegistry on the
244243
// previous deploy. Otherwise this will trigger an incorrect
245244
// service update.
246-
serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image
245+
serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image
247246
}
248247
}
249248

250249
// Stack deploy does not have a `--force` option. Preserve existing
251250
// ForceUpdate value so that tasks are not re-deployed if not updated.
252251
// TODO move this to API client?
253-
serviceSpec.TaskTemplate.ForceUpdate = service.Spec.TaskTemplate.ForceUpdate
252+
serviceSpec.TaskTemplate.ForceUpdate = svc.Spec.TaskTemplate.ForceUpdate
254253

255-
response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
254+
response, err := apiClient.ServiceUpdate(ctx, svc.ID, svc.Version, serviceSpec, updateOpts)
256255
if err != nil {
257256
return nil, fmt.Errorf("failed to update service %s: %w", name, err)
258257
}
@@ -261,12 +260,12 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
261260
_, _ = fmt.Fprintln(dockerCLI.Err(), warning)
262261
}
263262

264-
serviceIDs = append(serviceIDs, service.ID)
263+
serviceIDs = append(serviceIDs, svc.ID)
265264
} else {
266265
_, _ = fmt.Fprintln(out, "Creating service", name)
267266

268267
// query registry if flag disabling it was not set
269-
queryRegistry := resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged
268+
queryRegistry := resolveImage == resolveImageAlways || resolveImage == resolveImageChanged
270269

271270
response, err := apiClient.ServiceCreate(ctx, serviceSpec, client.ServiceCreateOptions{
272271
EncodedRegistryAuth: encodedAuth,
@@ -286,7 +285,7 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
286285
func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error {
287286
var errs []error
288287
for _, serviceID := range serviceIDs {
289-
if err := servicecli.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
288+
if err := service.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
290289
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
291290
}
292291
}

0 commit comments

Comments
 (0)