Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable client side digest pinning for stack deploy #121

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cli/command/stack/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ 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"
)

const (
defaultNetworkDriver = "overlay"
resolveImageAlways = "always"
resolveImageChanged = "changed"
resolveImageNever = "never"
)

type deployOptions struct {
bundlefile string
composefile string
namespace string
resolveImage string
sendRegistryAuth bool
prune bool
}
Expand All @@ -44,12 +49,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.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()

if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
return err
}

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).")
Expand All @@ -62,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
Expand Down
2 changes: 1 addition & 1 deletion cli/command/stack/deploy_bundlefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.resolveImage)
}
19 changes: 16 additions & 3 deletions cli/command/stack/deploy_composefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.resolveImage)
}

func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
Expand Down Expand Up @@ -283,6 +283,7 @@ func deployServices(
services map[string]swarm.ServiceSpec,
namespace convert.Namespace,
sendAuth bool,
resolveImage string,
) error {
apiClient := dockerCli.Client()
out := dockerCli.Out()
Expand All @@ -301,9 +302,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
Expand All @@ -313,12 +314,18 @@ func deployServices(
if service, exists := existingServiceMap[name]; exists {
fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)

updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}

if resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]) {
updateOpts.QueryRegistry = true
}

response, err := apiClient.ServiceUpdate(
ctx,
service.ID,
service.Version,
serviceSpec,
types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth},
updateOpts,
)
if err != nil {
return errors.Wrapf(err, "failed to update service %s", name)
Expand All @@ -331,6 +338,12 @@ func deployServices(
fmt.Fprintf(out, "Creating service %s\n", name)

createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}

// query registry if flag disabling it was not set
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
createOpts.QueryRegistry = true
}

if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
return errors.Wrapf(err, "failed to create service %s", name)
}
Expand Down
9 changes: 8 additions & 1 deletion cli/compose/convert/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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(
Expand Down Expand Up @@ -157,6 +161,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
Expand Down