From 59d6ed0a4da8646ad87e29ab819512b7ae40a528 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Mon, 22 May 2017 14:06:36 -0700 Subject: [PATCH 1/3] Enable client side digest pinning for stack deploy Signed-off-by: Nishant Totla (cherry picked from commit 9f1bea2657e1830313ebe4d82e0037bc660a7f73) Signed-off-by: Nishant Totla --- components/cli/cli/command/stack/deploy.go | 9 +++++++ .../cli/command/stack/deploy_bundlefile.go | 2 +- .../cli/command/stack/deploy_composefile.go | 24 ++++++++++++------- components/cli/cli/compose/convert/service.go | 6 +++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/components/cli/cli/command/stack/deploy.go b/components/cli/cli/command/stack/deploy.go index d18a43484d..a5edd2bd67 100644 --- a/components/cli/cli/command/stack/deploy.go +++ b/components/cli/cli/command/stack/deploy.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/compose/convert" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -21,6 +22,7 @@ type deployOptions struct { composefile string namespace string sendRegistryAuth bool + noResolveImage bool prune bool } @@ -44,12 +46,19 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command { addRegistryAuthFlag(&opts.sendRegistryAuth, flags) flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") flags.SetAnnotation("prune", "version", []string{"1.27"}) + flags.BoolVar(&opts.noResolveImage, "no-resolve-image", false, "Do not query the registry to resolve image digest and supported platforms") + flags.SetAnnotation("no-resolve-image", "version", []string{"1.30"}) return cmd } func runDeploy(dockerCli command.Cli, opts deployOptions) error { ctx := context.Background() + // image resolution should not happen for clients older than v1.30 + if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") { + opts.noResolveImage = true + } + switch { case opts.bundlefile == "" && opts.composefile == "": return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") diff --git a/components/cli/cli/command/stack/deploy_bundlefile.go b/components/cli/cli/command/stack/deploy_bundlefile.go index 2f2a9aa042..9c3ba25954 100644 --- a/components/cli/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/cli/command/stack/deploy_bundlefile.go @@ -87,5 +87,5 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.noResolveImage) } diff --git a/components/cli/cli/command/stack/deploy_composefile.go b/components/cli/cli/command/stack/deploy_composefile.go index 642867cba9..602f194b2b 100644 --- a/components/cli/cli/command/stack/deploy_composefile.go +++ b/components/cli/cli/command/stack/deploy_composefile.go @@ -92,7 +92,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption if err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.noResolveImage) } func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { @@ -282,6 +282,7 @@ func deployServices( services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, + noResolveImage bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() @@ -300,9 +301,9 @@ func deployServices( name := namespace.Scope(internalName) encodedAuth := "" + image := serviceSpec.TaskTemplate.ContainerSpec.Image if sendAuth { // Retrieve encoded auth token from the image reference - image := serviceSpec.TaskTemplate.ContainerSpec.Image encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) if err != nil { return err @@ -312,10 +313,14 @@ func deployServices( if service, exists := existingServiceMap[name]; exists { fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) - updateOpts := types.ServiceUpdateOptions{} - if sendAuth { - updateOpts.EncodedRegistryAuth = encodedAuth + updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} + + if image != service.Spec.Labels["com.docker.stack.image"] { + if !noResolveImage { + updateOpts.QueryRegistry = true + } } + response, err := apiClient.ServiceUpdate( ctx, service.ID, @@ -333,10 +338,13 @@ func deployServices( } else { fmt.Fprintf(out, "Creating service %s\n", name) - createOpts := types.ServiceCreateOptions{} - if sendAuth { - createOpts.EncodedRegistryAuth = encodedAuth + createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} + + // query registry if flag disabling it was not set + if !noResolveImage { + createOpts.QueryRegistry = true } + if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { return err } diff --git a/components/cli/cli/compose/convert/service.go b/components/cli/cli/compose/convert/service.go index 233a376293..9228f67ed8 100644 --- a/components/cli/cli/compose/convert/service.go +++ b/components/cli/cli/compose/convert/service.go @@ -46,6 +46,12 @@ func Services( if err != nil { return nil, errors.Wrapf(err, "service %s", service.Name) } + // add an image label to serviceSpec + if serviceSpec.Labels == nil { + serviceSpec.Labels = make(map[string]string) + } + serviceSpec.Labels["com.docker.stack.image"] = service.Image + result[service.Name] = serviceSpec } From 66d168dd559df0378b37d21193ef650206292b28 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Fri, 2 Jun 2017 16:21:41 -0700 Subject: [PATCH 2/3] Change --no-resolve-image flag to --resolve-image string flag Signed-off-by: Nishant Totla (cherry picked from commit f790e839fc7d669acafa6365ca7a83cbedfe9e2d) Signed-off-by: Nishant Totla --- components/cli/cli/command/stack/deploy.go | 29 +++++++++++++++---- .../cli/command/stack/deploy_bundlefile.go | 2 +- .../cli/command/stack/deploy_composefile.go | 12 ++++---- components/cli/cli/compose/convert/service.go | 15 +++++----- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/components/cli/cli/command/stack/deploy.go b/components/cli/cli/command/stack/deploy.go index a5edd2bd67..8e14b70d2f 100644 --- a/components/cli/cli/command/stack/deploy.go +++ b/components/cli/cli/command/stack/deploy.go @@ -15,14 +15,17 @@ import ( const ( defaultNetworkDriver = "overlay" + resolveImageAlways = "always" + resolveImageChanged = "changed" + resolveImageNever = "never" ) type deployOptions struct { bundlefile string composefile string namespace string + resolveImage string sendRegistryAuth bool - noResolveImage bool prune bool } @@ -46,17 +49,17 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command { addRegistryAuthFlag(&opts.sendRegistryAuth, flags) flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") flags.SetAnnotation("prune", "version", []string{"1.27"}) - flags.BoolVar(&opts.noResolveImage, "no-resolve-image", false, "Do not query the registry to resolve image digest and supported platforms") - flags.SetAnnotation("no-resolve-image", "version", []string{"1.30"}) + 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"}) return cmd } func runDeploy(dockerCli command.Cli, opts deployOptions) error { ctx := context.Background() - // image resolution should not happen for clients older than v1.30 - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") { - opts.noResolveImage = true + if err := validateResolveImageFlag(dockerCli, &opts); err != nil { + return err } switch { @@ -71,6 +74,20 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error { } } +// validateResolveImageFlag validates the opts.resolveImage command line option +// and also turns image resolution off if the version is older than 1.30 +func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error { + if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever { + 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") { + opts.resolveImage = resolveImageNever + } + return nil +} + // 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 diff --git a/components/cli/cli/command/stack/deploy_bundlefile.go b/components/cli/cli/command/stack/deploy_bundlefile.go index 9c3ba25954..1074210e97 100644 --- a/components/cli/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/cli/command/stack/deploy_bundlefile.go @@ -87,5 +87,5 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.noResolveImage) + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage) } diff --git a/components/cli/cli/command/stack/deploy_composefile.go b/components/cli/cli/command/stack/deploy_composefile.go index 602f194b2b..1cafca58e7 100644 --- a/components/cli/cli/command/stack/deploy_composefile.go +++ b/components/cli/cli/command/stack/deploy_composefile.go @@ -92,7 +92,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption if err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.noResolveImage) + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage) } func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { @@ -282,7 +282,7 @@ func deployServices( services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, - noResolveImage bool, + resolveImage string, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() @@ -315,10 +315,8 @@ func deployServices( updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} - if image != service.Spec.Labels["com.docker.stack.image"] { - if !noResolveImage { - updateOpts.QueryRegistry = true - } + if resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]) { + updateOpts.QueryRegistry = true } response, err := apiClient.ServiceUpdate( @@ -341,7 +339,7 @@ func deployServices( createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} // query registry if flag disabling it was not set - if !noResolveImage { + if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged { createOpts.QueryRegistry = true } diff --git a/components/cli/cli/compose/convert/service.go b/components/cli/cli/compose/convert/service.go index 9228f67ed8..2280d57c79 100644 --- a/components/cli/cli/compose/convert/service.go +++ b/components/cli/cli/compose/convert/service.go @@ -18,7 +18,11 @@ import ( "github.com/pkg/errors" ) -const defaultNetwork = "default" +const ( + defaultNetwork = "default" + // LabelImage is the label used to store image name provided in the compose file + LabelImage = "com.docker.stack.image" +) // Services from compose-file types to engine API types func Services( @@ -46,12 +50,6 @@ func Services( if err != nil { return nil, errors.Wrapf(err, "service %s", service.Name) } - // add an image label to serviceSpec - if serviceSpec.Labels == nil { - serviceSpec.Labels = make(map[string]string) - } - serviceSpec.Labels["com.docker.stack.image"] = service.Image - result[service.Name] = serviceSpec } @@ -164,6 +162,9 @@ func convertService( UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } + // add an image label to serviceSpec + serviceSpec.Labels[LabelImage] = service.Image + // ServiceSpec.Networks is deprecated and should not have been used by // this package. It is possible to update TaskTemplate.Networks, but it // is not possible to update ServiceSpec.Networks. Unfortunately, we From f15c4fd16020d1fe48cff5aca240507b1d040bfa Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Thu, 8 Jun 2017 15:20:09 -0700 Subject: [PATCH 3/3] Fixing stack deploy tests to not contact registry Because of cherry-pick from commit f790e839fc7d669acafa6365ca7a83cbedfe9e2d Signed-off-by: Nishant Totla --- .../engine/hack/integration-cli-on-swarm/host/dockercmd.go | 1 + components/engine/integration-cli/docker_cli_stack_test.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/components/engine/hack/integration-cli-on-swarm/host/dockercmd.go b/components/engine/hack/integration-cli-on-swarm/host/dockercmd.go index 10ea0ecc24..c539876213 100644 --- a/components/engine/hack/integration-cli-on-swarm/host/dockercmd.go +++ b/components/engine/hack/integration-cli-on-swarm/host/dockercmd.go @@ -37,6 +37,7 @@ func deployStack(unusedCli *client.Client, stackName, composeFilePath string) er {"docker", "stack", "deploy", "--compose-file", composeFilePath, "--with-registry-auth", + "--resolve-image", "never", stackName}, }) } diff --git a/components/engine/integration-cli/docker_cli_stack_test.go b/components/engine/integration-cli/docker_cli_stack_test.go index 91fe4d75c0..51b98ff187 100644 --- a/components/engine/integration-cli/docker_cli_stack_test.go +++ b/components/engine/integration-cli/docker_cli_stack_test.go @@ -62,6 +62,7 @@ func (s *DockerSwarmSuite) TestStackDeployComposeFile(c *check.C) { testStackName := "testdeploy" stackArgs := []string{ "stack", "deploy", + "--resolve-image", "never", "--compose-file", "fixtures/deploy/default.yaml", testStackName, } @@ -88,6 +89,7 @@ func (s *DockerSwarmSuite) TestStackDeployWithSecretsTwice(c *check.C) { testStackName := "testdeploy" stackArgs := []string{ "stack", "deploy", + "--resolve-image", "never", "--compose-file", "fixtures/deploy/secrets.yaml", testStackName, } @@ -120,6 +122,7 @@ func (s *DockerSwarmSuite) TestStackRemove(c *check.C) { stackName := "testdeploy" stackArgs := []string{ "stack", "deploy", + "--resolve-image", "never", "--compose-file", "fixtures/deploy/remove.yaml", stackName, } @@ -179,6 +182,7 @@ func (s *DockerSwarmSuite) TestStackDeployWithDAB(c *check.C) { // deploy stackArgs := []string{ "stack", "deploy", + "--resolve-image", "never", "--bundle-file", testDABFileName, testStackName, }