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

Fix local images when daemon uses containerd storage #222

Merged
merged 3 commits into from
Oct 30, 2023

Conversation

natalieparellano
Copy link
Member

@natalieparellano natalieparellano commented Oct 25, 2023

In service of:

On Mac: Docker > Settings > Features in development > Use containerd for pulling and storing images
Tests pass for me locally with both configurations.

Local image creation works with these changes, but is unsatisfactory as we can no longer omit base layers that are already present in the daemon. Omitting the layers creates a runnable image, but if you try to docker push that image to a registry the daemon will crash, I'm not sure why. But if you do a docker save on that image you will get a base layer that is 0 in size. It's possible this is something that will be fixed before containerd storage becomes the default configuration. We should keep an eye on it (or maybe there's another way around it).

Signed-off-by: Natalie Arellano <narellano@vmware.com>
@natalieparellano natalieparellano requested a review from a team as a code owner October 25, 2023 18:18
Comment on lines +168 to +169
cfg1.DockerVersion = ""
cfg2.DockerVersion = ""
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried explicitly setting this about here:

config = v1.Config{
but it seems to have no effect.

ref, err := name.ParseReference(refStr, name.WeakValidation)
AssertNil(t, err)
t.Helper()
Run(t, exec.Command("docker", "push", refStr)) // #nosec G204
Copy link
Member Author

@natalieparellano natalieparellano Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was interesting. dockerCli.ImagePush returns a 401 error, but docker image push works fine. I didn't look too deeply into it, but this is another rough edge that could be worth investigating. Our test registry is from github.com/google/go-containerregistry/pkg/registry - maybe another registry would behave differently, but I didn't try it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is very weird. Maybe we should open a bug on this one. I captured the flow a bit.

// our docker.Push wrapper
	fmt.Println("DEBUG (DockerPush)", authConfig.Username, authConfig.Password)

	rc, err := dockerCli.ImagePush(context.Background(), refStr, dockertypes.ImagePushOptions{
		RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
		PrivilegeFunc: func() (string, error) {
			fmt.Println("DEBUG (DockerPush)", "PrivilegeFunc")
			return "bob", nil
		},
	})

// the registry bit
func BasicAuth(handler http.Handler, username, password, realm string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, pass, ok := r.BasicAuth()
		fmt.Println("DEBUG (RegistryBasicAuth)", "user:", user, "pass:", pass, "ok:", ok)

// relevant debug output
DEBUG (DockerPush) vvqijndzyz wiqldsvicu
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user:  pass:  ok: false

The generated auth is passed into ImagePushOptions. The first empty user/pass at the registry auth check is typical but the subsequent ones are not. I tried adding the PrivilegeFunc` to see if the 401 would trigger an auth send but the logs show it isn't executed in either situation. Like the auth is ignored entirely 😞

Here are the logs when using the overlayfs for comparison.

DEBUG (DockerPush) zitozndflk dhlzgeqnku
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user: zitozndflk pass: dhlzgeqnku ok: true
DEBUG (RegistryBasicAuth) user: zitozndflk pass: dhlzgeqnku ok: true

}

return nil
_, err = i.addImageToTar(tw, repoName)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reduces some duplication but also without the refactor I got an "unexpected format" error when trying to load the image in the daemon. Which is weird because the code looks basically the same...

@@ -154,13 +123,16 @@ func (i *Image) doSaveAs(name string) (types.ImageInspect, error) {
return types.ImageInspect{}, errors.Wrapf(err, "loading image %q. first error", i.repoName)
}

inspect, _, err := i.docker.ImageInspectWithRaw(context.Background(), id)
inspect, _, err := i.docker.ImageInspectWithRaw(context.Background(), i.repoName)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With containerd storage the image ID is the sha of the manifest, which technically we could compute but it would be hard. To avoid collision problems we can validate that the data in inspect matches the config that we expect.

// The docker daemon allows this if the given set of layers already exists in the daemon in the given order.
inspect, err = i.doSaveAs(name)
}
if !canOmitBaseLayers || err != nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the sorriest part... it's going to lead to a huge performance penalty unless we can work around it. Maybe with containerd storage we could get individual layers from the daemon, IDK. If anyone has further context here it would be much appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also couldn't find a way around this. The trick of giving a tar header with no bytes does not work so we always have to pay the price it seems. At least until moby/moby#44369 ships

Signed-off-by: Natalie Arellano <narellano@vmware.com>
Comment on lines +170 to +171
cfg1.Config.Image = ""
cfg2.Config.Image = ""
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unspec'd so IDK if we care.

Copy link
Contributor

@jabrown85 jabrown85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! This seems shippable for me since the storage system is in beta on the docker side.

// The docker daemon allows this if the given set of layers already exists in the daemon in the given order.
inspect, err = i.doSaveAs(name)
}
if !canOmitBaseLayers || err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also couldn't find a way around this. The trick of giving a tar header with no bytes does not work so we always have to pay the price it seems. At least until moby/moby#44369 ships

ref, err := name.ParseReference(refStr, name.WeakValidation)
AssertNil(t, err)
t.Helper()
Run(t, exec.Command("docker", "push", refStr)) // #nosec G204
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is very weird. Maybe we should open a bug on this one. I captured the flow a bit.

// our docker.Push wrapper
	fmt.Println("DEBUG (DockerPush)", authConfig.Username, authConfig.Password)

	rc, err := dockerCli.ImagePush(context.Background(), refStr, dockertypes.ImagePushOptions{
		RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
		PrivilegeFunc: func() (string, error) {
			fmt.Println("DEBUG (DockerPush)", "PrivilegeFunc")
			return "bob", nil
		},
	})

// the registry bit
func BasicAuth(handler http.Handler, username, password, realm string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, pass, ok := r.BasicAuth()
		fmt.Println("DEBUG (RegistryBasicAuth)", "user:", user, "pass:", pass, "ok:", ok)

// relevant debug output
DEBUG (DockerPush) vvqijndzyz wiqldsvicu
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user:  pass:  ok: false

The generated auth is passed into ImagePushOptions. The first empty user/pass at the registry auth check is typical but the subsequent ones are not. I tried adding the PrivilegeFunc` to see if the 401 would trigger an auth send but the logs show it isn't executed in either situation. Like the auth is ignored entirely 😞

Here are the logs when using the overlayfs for comparison.

DEBUG (DockerPush) zitozndflk dhlzgeqnku
DEBUG (RegistryBasicAuth) user:  pass:  ok: false
DEBUG (RegistryBasicAuth) user: zitozndflk pass: dhlzgeqnku ok: true
DEBUG (RegistryBasicAuth) user: zitozndflk pass: dhlzgeqnku ok: true

Signed-off-by: Natalie Arellano <narellano@vmware.com>
@natalieparellano natalieparellano merged commit 1aff6e5 into main Oct 30, 2023
4 checks passed
@natalieparellano natalieparellano deleted the fix/containerd-hack branch October 30, 2023 17:52
natalieparellano added a commit that referenced this pull request Jan 17, 2024
Before #222, we calculated the sha of the config file that we sent to the daemon,
and verified that we could inspect an image with that ID.

After #222, since the image ID may be the sha of the config file or the sha of the manifest file
(depending on whether containerd storage is enabled),
we still calculated the sha of the config file that we sent to the daemon,
but instead of trying to inspect an image with that ID,
we inspected the image by name, derived the config file from the data we got back from inspect,
and then verified that the sha of the derived config file matches the sha of the config file that we sent in.

This check turned out to be brittle
(see buildpacks/pack#2000 and https://cloud-native.slack.com/archives/C0331B61A1Y/p1701976103265489)
and we agreed that it should be safe to remove this check.

Signed-off-by: Natalie Arellano <narellano@vmware.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants