-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Enable client side digest pinning for stack deploy #121
Conversation
305a611
to
9b9ae49
Compare
I think we need this for 17.06 because if we didn't have it, we'd lose digest pinning from |
@dnephin @tiborvass what can we do to make sure this makes it into 17.06? Also opened moby/moby#33386 to ensure tests don't contact registry because of this. |
@@ -320,6 +322,13 @@ func deployServices( | |||
if sendAuth { | |||
updateOpts.EncodedRegistryAuth = encodedAuth | |||
} | |||
|
|||
if image != service.Spec.TaskTemplate.ContainerSpec.Image { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this check makes sense. It means that if the digest wasn't pinned before, we won't pin the digest (or add platform information) going forward. Something like a temporary registry error would cause pinning to be turned off for future runs as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aaronlehmann you're right, that would be a problem.
For service update
, it's pretty simple to judge because we have a --image
flag, but I couldn't find a way of deciding whether an image was updated in an updated stack file. Is there a way of discovering this at the CLI level? cc @dnephin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be best to just remove the check, so the digest and platform information are always updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little uncomfortable with that idea, because if the stack file contains multiple images, then it's possibly too much overhead each time any update is done.
Although it's easy to avoid that given the --no-resolve-image
flag, but the some might find that frustrating UI. Anyhow, if there's no other way, then I agree that removing the check is the right thing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a better way, unless we want to change the UX to make digest pinning a manual operation, rather than getting the latest digest on every deploy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds reasonable to me.
We would need to make sure there is a way to enable digest pinning if the service was originally deployed with --no-resolve-image
, or vice versa.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect --no-resolve-image
to persist? I expect that it would only apply to the current deploy.
For any service with a missing label we can treat that as an image name mismatch. The first deploy will cause an update to include the service image label.
Otherwise I wouldn't expect --no-resolve-image
to change the label value (it will always contain the string from the config, not a digest), so the flag acts as an override.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't expect it to persist, but just saying that storing the original name in a label and comparing with that when deciding whether to update the digest could cause us not to add a digest if there wasn't one originally. We just need to make sure to take that account in the logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't very clear to me how having a label solves the problem. If the label contains the image that was originally deployed (without a digest because say pinning failed), then later when the stack file is updated (without --no-resolve-image
flag, but the same image), how do we decide to contact the registry?
To me, the --image
flag in service create
and service update
also conveys "user intent", something that I'm not able to grasp in stack files. Please correct me if I've misunderstood.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The label can give us the same signal as the --image
flag on service commands.
Here's how it works:
- in
convert.Services()
we add a label to each service with a key ofcom.docker.stack.image
(or something like this) and a value of the literal string from the compose file (composetypes.Service.Image
) - when we enter this
deployServices()
code we compare the existing label value against the new value from the compose file
image := serviceSpec.TaskTemplate.ContainerSpec.Image
// has the image value changed in the compose file?
if service.Spec.Labels["com.docker.stack.image"] != image {
if !noResolveImage {
updateOpts.QueryRegistry = true
}
}
This is different from the current code because we don't know if service.Spec.TaskTemplate.ContainerSpec.Image
was modified or not, but we do know the label is never modified by other code.
Does that make sense?
@@ -320,6 +322,13 @@ func deployServices( | |||
if sendAuth { | |||
updateOpts.EncodedRegistryAuth = encodedAuth | |||
} | |||
|
|||
if image != service.Spec.TaskTemplate.ContainerSpec.Image { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a better way, unless we want to change the UX to make digest pinning a manual operation, rather than getting the latest digest on every deploy.
@@ -320,6 +322,13 @@ func deployServices( | |||
if sendAuth { | |||
updateOpts.EncodedRegistryAuth = encodedAuth | |||
} | |||
|
|||
if image != service.Spec.TaskTemplate.ContainerSpec.Image { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm understanding the issue correctly, I think the right way to handle this is by setting a label on each service with the original image name.
If the label of the existing service doesn't match the string from the config, then we know the image has changed and we should set QueryRegistry = true
.
I don't think we should force update the digest on every deploy.
// query registry if flag disabling it was not set | ||
if !noResolveImage && versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") { | ||
createOpts.QueryRegistry = true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking the version twice I think we should handle the version check in runDeploy()
by setting the option accordingly (set the option to true
for older versions). That way noResolveImage
will always be correct inside deployServices()
and we won't need to check version twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnephin I addressed this comment, and the one about refactoring.
cli/command/stack/opts.go
Outdated
var flagNoResolveImage = "no-resolve-image" | ||
flags.BoolVar(opt, flagNoResolveImage, false, "Do not query the registry to resolve image digest and supported platforms") | ||
flags.SetAnnotation(flagNoResolveImage, "version", []string{"1.30"}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor nit: typically we only use these functions for flags that are shared between commands. I think it's fine to keep this as a separate function, but maybe move it to the single file where it's used?
543b0a6
to
0db4de3
Compare
cli/command/stack/deploy.go
Outdated
return cmd | ||
} | ||
|
||
func runDeploy(dockerCli command.Cli, opts deployOptions) error { | ||
ctx := context.Background() | ||
|
||
// image resolution should not happen for clients older than v1.30 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though this says "ClientVersion()" it's important to note it actually reflects the supported version of the server. The comment might be a bit misleading in that sense, but I guess it's fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree with changing this comment.
The label can give us the same signal as the --image flag on service commands.
Here's how it works:
in convert.Services() we add a label to each service with a key of com.docker.stack.image (or something like this) and a value of the literal string from the compose file (composetypes.Service.Image)
when we enter this deployServices() code we compare the existing label value against the new value from the compose file
What about the case where digest pinning fails, and I want to retry it? I would expect rerunning docker stack deploy without --no-resolve-image to do this, but if I understand the proposal correctly, it would detect that the image has not changed in the compose file, and avoid digest pinning.
|
If a service create/update is performed with with digest pinning enabled, but it failed to pin, I would expect the service create/update call to fail, not for the command to return success anyway. Is that not the case? |
It completes successfully but warns the user that warns the user that the image is referenced by name, not by digest. Otherwise, you would need to use a special flag to create a service using a local or pre-pulled image, which would break backward compat. |
I see. In that case I guess we need a way to say "force pinning". We should still add the label for the default case. For the force case I think we should change the flag to |
SGTM
|
I've added the label part, and adding the As far as labels go though, consider this example.
Is this the expected UI? |
0db4de3
to
f158368
Compare
This is why I think it's much better to (at least by default) always query the registry. Doing so matches the old behavior when this was done daemon-side, assuming |
I'm really not sure what someone would expect when they use Maybe the default should be |
@dnephin I'm possibly restating what's been said above, but just for clarity: changing the default to With this proposal, digest pinning always happens, unless the user chooses I'd be satisfied with this sort of behavior, if we all agree on this. (I'll change the option names too) |
Yes, I think that sounds right.
I don't understand this part. Why would this be the case? I think using a label fixes this problem? |
@dnephin my comment above explains in more detail what I mean: #121 (comment) |
4ad7796
to
29b22c4
Compare
@dnephin added validation for the CLI option, and moved label addition to |
cli/command/stack/deploy.go
Outdated
@@ -62,6 +71,18 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error { | |||
} | |||
} | |||
|
|||
// checkImageResolutionAllowed validates the opts.resolveImage command line option | |||
// and also turns image resolution off if the version is older than 1.30 | |||
func checkImageResolutionAllowed(dockerCli command.Cli, opts *deployOptions) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validateResolveImageFlag
?
cli/command/stack/deploy.go
Outdated
// checkImageResolutionAllowed validates the opts.resolveImage command line option | ||
// and also turns image resolution off if the version is older than 1.30 | ||
func checkImageResolutionAllowed(dockerCli command.Cli, opts *deployOptions) error { | ||
if opts.resolveImage != "always" && opts.resolveImage != "changed" && opts.resolveImage != "never" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a switch
, but it's fine this way too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest declaring constants for the possible values, so it is harder for this to be out of sync with the code that interprets the flag values, or for typos to creep into the string literals in some places. Again, not a big deal either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM
It would be nice to have test coverage, but I'm not immediately familiar with how tests work for this portion of the code, so I'll let Daniel advise on what makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can write unit tests for these using a fakeClient
.
cli/compose/convert/service.go
Outdated
// add an image label to serviceSpec | ||
if serviceSpec.Labels == nil { | ||
serviceSpec.Labels = make(map[string]string) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition is already handled by AddStackLabel()
.
2c53999
to
fb37e1e
Compare
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
fb37e1e
to
f790e83
Compare
Codecov Report
@@ Coverage Diff @@
## master #121 +/- ##
==========================================
- Coverage 44.79% 44.72% -0.07%
==========================================
Files 171 171
Lines 11454 11467 +13
==========================================
- Hits 5131 5129 -2
- Misses 6027 6043 +16
+ Partials 296 295 -1 |
I've addressed comments and fixed the merge conflict. For the test, it seems like Alternatively, @dnephin would you suggest a different unit test? Could we make this a follow-up? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There is a FakeCli
in cli/internal/test
that is used for testing things that depend on command.Cli
.
If this is time sensitive tests in a follow up are fine with me.
@dnephin it is time sensitive since it must be cherry picked for rc3, so I can add the test later. |
I've confirmed these changes work as desired in local (manual) testing, and opened a cherry-pick PR here: docker-archive/docker-ce#44 |
This PR is a follow-up to #30.
It also adds a
--resolve-image
flag tostack deploy
to provide digest pinning options.cc @dnephin @tiborvass @thaJeztah