diff --git a/pkg/skaffold/build/local/bazel.go b/pkg/skaffold/build/local/bazel.go index c3a4950cedd..0717a8e6baa 100644 --- a/pkg/skaffold/build/local/bazel.go +++ b/pkg/skaffold/build/local/bazel.go @@ -25,7 +25,6 @@ import ( "path/filepath" "strings" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" "github.com/pkg/errors" @@ -56,18 +55,14 @@ func (b *Builder) buildBazel(ctx context.Context, out io.Writer, workspace strin } defer imageTar.Close() - resp, err := b.api.ImageLoad(ctx, imageTar, false) - if err != nil { - return "", errors.Wrap(err, "loading image into docker daemon") - } - defer resp.Body.Close() + ref := buildImageTag(a.BuildTarget) - err = docker.StreamDockerMessages(out, resp.Body, nil) + imageID, err := b.localDocker.Load(ctx, out, imageTar, ref) if err != nil { - return "", errors.Wrap(err, "reading from image load response") + return "", errors.Wrap(err, "loading image into docker daemon") } - return docker.ImageID(ctx, b.api, buildImageTag(a.BuildTarget)) + return imageID, nil } func bazelBin(ctx context.Context, workspace string) (string, error) { diff --git a/pkg/skaffold/build/local/docker.go b/pkg/skaffold/build/local/docker.go index 7bb44802e28..e828e6ff366 100644 --- a/pkg/skaffold/build/local/docker.go +++ b/pkg/skaffold/build/local/docker.go @@ -51,8 +51,8 @@ func (b *Builder) buildDocker(ctx context.Context, out io.Writer, workspace stri return "", errors.Wrap(err, "running build") } - return docker.ImageID(ctx, b.api, initialTag) + return b.localDocker.ImageID(ctx, initialTag) } - return docker.Build(ctx, out, b.api, workspace, a, initialTag) + return b.localDocker.Build(ctx, out, workspace, a, initialTag) } diff --git a/pkg/skaffold/build/local/jib_gradle.go b/pkg/skaffold/build/local/jib_gradle.go index 5b29de209ac..0726f7198c3 100644 --- a/pkg/skaffold/build/local/jib_gradle.go +++ b/pkg/skaffold/build/local/jib_gradle.go @@ -44,7 +44,7 @@ func (b *Builder) buildJibGradleToDocker(ctx context.Context, out io.Writer, wor return "", err } - return docker.ImageID(ctx, b.api, skaffoldImage) + return b.localDocker.ImageID(ctx, skaffoldImage) } func (b *Builder) buildJibGradleToRegistry(ctx context.Context, out io.Writer, workspace string, artifact *latest.Artifact) (string, error) { diff --git a/pkg/skaffold/build/local/jib_maven.go b/pkg/skaffold/build/local/jib_maven.go index 1149cd42ddf..ccd46e0708b 100644 --- a/pkg/skaffold/build/local/jib_maven.go +++ b/pkg/skaffold/build/local/jib_maven.go @@ -52,7 +52,7 @@ func (b *Builder) buildJibMavenToDocker(ctx context.Context, out io.Writer, work return "", err } - return docker.ImageID(ctx, b.api, skaffoldImage) + return b.localDocker.ImageID(ctx, skaffoldImage) } func (b *Builder) buildJibMavenToRegistry(ctx context.Context, out io.Writer, workspace string, artifact *latest.Artifact) (string, error) { diff --git a/pkg/skaffold/build/local/local.go b/pkg/skaffold/build/local/local.go index 298d867d2cc..cbe075f407d 100644 --- a/pkg/skaffold/build/local/local.go +++ b/pkg/skaffold/build/local/local.go @@ -35,7 +35,7 @@ func (b *Builder) Build(ctx context.Context, out io.Writer, tagger tag.Tagger, a if b.localCluster { color.Default.Fprintf(out, "Found [%s] context, using local docker daemon.\n", b.kubeContext) } - defer b.api.Close() + defer b.localDocker.Close() // TODO(dgageot): parallel builds return build.InSequence(ctx, out, tagger, artifacts, b.buildArtifact) @@ -98,12 +98,12 @@ func (b *Builder) retagAndPush(ctx context.Context, out io.Writer, initialTag st return nil } - if err := b.api.ImageTag(ctx, initialTag, newTag); err != nil { + if err := b.localDocker.Tag(ctx, initialTag, newTag); err != nil { return err } if b.pushImages { - if _, err := docker.RunPush(ctx, out, b.api, newTag); err != nil { + if _, err := b.localDocker.Push(ctx, out, newTag); err != nil { return errors.Wrap(err, "pushing") } } diff --git a/pkg/skaffold/build/local/local_test.go b/pkg/skaffold/build/local/local_test.go index aee0cf20832..63594cf5acf 100644 --- a/pkg/skaffold/build/local/local_test.go +++ b/pkg/skaffold/build/local/local_test.go @@ -147,7 +147,7 @@ func TestLocalRun(t *testing.T) { t.Run(test.description, func(t *testing.T) { l := Builder{ cfg: &latest.LocalBuild{}, - api: &test.api, + localDocker: docker.NewLocalDaemon(&test.api), localCluster: test.localCluster, } diff --git a/pkg/skaffold/build/local/types.go b/pkg/skaffold/build/local/types.go index 244ae7a145c..61a5b311dcd 100644 --- a/pkg/skaffold/build/local/types.go +++ b/pkg/skaffold/build/local/types.go @@ -31,7 +31,7 @@ import ( type Builder struct { cfg *latest.LocalBuild - api docker.APIClient + localDocker docker.LocalDaemon localCluster bool pushImages bool kubeContext string @@ -41,7 +41,7 @@ type Builder struct { // NewBuilder returns an new instance of a local Builder. func NewBuilder(cfg *latest.LocalBuild, kubeContext string) (*Builder, error) { - api, err := docker.NewAPIClient() + localDocker, err := docker.NewAPIClient() if err != nil { return nil, errors.Wrap(err, "getting docker client") } @@ -58,7 +58,7 @@ func NewBuilder(cfg *latest.LocalBuild, kubeContext string) (*Builder, error) { return &Builder{ cfg: cfg, kubeContext: kubeContext, - api: api, + localDocker: localDocker, localCluster: localCluster, pushImages: pushImages, }, nil @@ -70,7 +70,7 @@ func (b *Builder) Labels() map[string]string { constants.Labels.Builder: "local", } - v, err := b.api.ServerVersion(context.Background()) + v, err := b.localDocker.ServerVersion(context.Background()) if err == nil { labels[constants.Labels.DockerAPIVersion] = fmt.Sprintf("%v", v.APIVersion) } diff --git a/pkg/skaffold/docker/auth.go b/pkg/skaffold/docker/auth.go index c8afc850cd7..bf9f511a058 100644 --- a/pkg/skaffold/docker/auth.go +++ b/pkg/skaffold/docker/auth.go @@ -82,7 +82,7 @@ func (credsHelper) GetAllAuthConfigs() (map[string]types.AuthConfig, error) { return cf.GetCredentialsStore("").GetAll() } -func encodedRegistryAuth(ctx context.Context, cli APIClient, a AuthConfigHelper, image string) (string, error) { +func (l *localDaemon) encodedRegistryAuth(ctx context.Context, a AuthConfigHelper, image string) (string, error) { ref, err := reference.ParseNormalizedNamed(image) if err != nil { return "", errors.Wrap(err, "parsing image name for registry") @@ -96,7 +96,7 @@ func encodedRegistryAuth(ctx context.Context, cli APIClient, a AuthConfigHelper, index := repoInfo.Index configKey := index.Name if index.Official { - configKey = officialRegistry(ctx, cli) + configKey = l.officialRegistry(ctx) } ac, err := a.GetAuthConfig(configKey) @@ -112,11 +112,11 @@ func encodedRegistryAuth(ctx context.Context, cli APIClient, a AuthConfigHelper, return base64.URLEncoding.EncodeToString(buf), nil } -func officialRegistry(ctx context.Context, cli APIClient) string { +func (l *localDaemon) officialRegistry(ctx context.Context) string { serverAddress := registry.IndexServer // The daemon `/info` endpoint informs us of the default registry being used. - info, err := cli.Info(ctx) + info, err := l.apiClient.Info(ctx) switch { case err != nil: logrus.Warnf("failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) diff --git a/pkg/skaffold/docker/auth_test.go b/pkg/skaffold/docker/auth_test.go index 89c054f2977..c1c95536bc0 100644 --- a/pkg/skaffold/docker/auth_test.go +++ b/pkg/skaffold/docker/auth_test.go @@ -78,7 +78,9 @@ func TestGetEncodedRegistryAuth(t *testing.T) { defer func(h AuthConfigHelper) { DefaultAuthHelper = h }(DefaultAuthHelper) DefaultAuthHelper = test.authType - out, err := encodedRegistryAuth(context.Background(), nil, test.authType, test.image) + l := &localDaemon{} + out, err := l.encodedRegistryAuth(context.Background(), test.authType, test.image) + testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expected, out) }) } diff --git a/pkg/skaffold/docker/client.go b/pkg/skaffold/docker/client.go index 236646548a5..8ed37f0b5a5 100644 --- a/pkg/skaffold/docker/client.go +++ b/pkg/skaffold/docker/client.go @@ -28,11 +28,10 @@ import ( "strings" "sync" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" - "github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants" kubectx "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" "github.com/docker/docker/api" "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" @@ -40,18 +39,14 @@ import ( "github.com/sirupsen/logrus" ) -type APIClient interface { - client.CommonAPIClient -} - var ( dockerAPIClientOnce sync.Once - dockerAPIClient APIClient + dockerAPIClient LocalDaemon dockerAPIClientErr error ) // NewAPIClient guesses the docker client to use based on current kubernetes context. -func NewAPIClient() (APIClient, error) { +func NewAPIClient() (LocalDaemon, error) { dockerAPIClientOnce.Do(func() { kubeContext, err := kubectx.CurrentContext() if err != nil { @@ -59,14 +54,16 @@ func NewAPIClient() (APIClient, error) { return } - dockerAPIClient, dockerAPIClientErr = newAPIClient(kubeContext) + apiClient, err := newAPIClient(kubeContext) + dockerAPIClient = NewLocalDaemon(apiClient) + dockerAPIClientErr = err }) return dockerAPIClient, dockerAPIClientErr } // newAPIClient guesses the docker client to use based on current kubernetes context. -func newAPIClient(kubeContext string) (APIClient, error) { +func newAPIClient(kubeContext string) (client.CommonAPIClient, error) { if kubeContext == constants.DefaultMinikubeContext { return newMinikubeAPIClient() } @@ -76,7 +73,7 @@ func newAPIClient(kubeContext string) (APIClient, error) { // newEnvAPIClient returns a docker client based on the environment variables set. // It will "negotiate" the highest possible API version supported by both the client // and the server if there is a mismatch. -func newEnvAPIClient() (APIClient, error) { +func newEnvAPIClient() (client.CommonAPIClient, error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHTTPHeaders(getUserAgentHeader())) if err != nil { return nil, fmt.Errorf("error getting docker client: %s", err) @@ -88,7 +85,7 @@ func newEnvAPIClient() (APIClient, error) { // newMinikubeAPIClient returns a docker client using the environment variables // provided by minikube. -func newMinikubeAPIClient() (APIClient, error) { +func newMinikubeAPIClient() (client.CommonAPIClient, error) { env, err := getMinikubeDockerEnv() if err != nil { logrus.Warnf("Could not get minikube docker env, falling back to local docker daemon: %s", err) diff --git a/pkg/skaffold/docker/client_test.go b/pkg/skaffold/docker/client_test.go index c475d3c360e..76280dede97 100644 --- a/pkg/skaffold/docker/client_test.go +++ b/pkg/skaffold/docker/client_test.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" "github.com/GoogleContainerTools/skaffold/testutil" + "github.com/docker/docker/client" ) func TestNewEnvClient(t *testing.T) { @@ -61,7 +62,7 @@ func TestNewMinikubeImageAPIClient(t *testing.T) { description string cmd util.Command - expected APIClient + expected client.CommonAPIClient shouldErr bool }{ { diff --git a/pkg/skaffold/docker/image.go b/pkg/skaffold/docker/image.go index c8296c4942d..3128aa42bb3 100644 --- a/pkg/skaffold/docker/image.go +++ b/pkg/skaffold/docker/image.go @@ -21,8 +21,8 @@ import ( "encoding/json" "fmt" "io" - "net/http" "sort" + "sync" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" "github.com/docker/docker/api/types" @@ -31,15 +31,35 @@ import ( "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/term" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +// LocalDaemon talks to a local Docker API. +type LocalDaemon interface { + Close() error + ServerVersion(ctx context.Context) (types.Version, error) + ConfigFile(ctx context.Context, image string) (*v1.ConfigFile, error) + Build(ctx context.Context, out io.Writer, workspace string, a *latest.DockerArtifact, ref string) (string, error) + Push(ctx context.Context, out io.Writer, ref string) (string, error) + Load(ctx context.Context, out io.Writer, input io.Reader, ref string) (string, error) + Tag(ctx context.Context, image, ref string) error + ImageID(ctx context.Context, ref string) (string, error) +} + +type localDaemon struct { + apiClient client.CommonAPIClient + imageCache sync.Map +} + +// NewLocalDaemon creates a new LocalDaemon. +func NewLocalDaemon(apiClient client.CommonAPIClient) LocalDaemon { + return &localDaemon{ + apiClient: apiClient, + } +} + // PushResult gives the information on an image that has been pushed. type PushResult struct { Digest string @@ -50,8 +70,44 @@ type BuildResult struct { ID string } +// Close closes the connection with the local daemon. +func (l *localDaemon) Close() error { + return l.apiClient.Close() +} + +// ServerVersion retrieves the version information from the server. +func (l *localDaemon) ServerVersion(ctx context.Context) (types.Version, error) { + return l.apiClient.ServerVersion(ctx) +} + +// ConfigFile retrieves and caches image configurations. +func (l *localDaemon) ConfigFile(ctx context.Context, image string) (*v1.ConfigFile, error) { + cachedCfg, present := l.imageCache.Load(image) + if present { + return cachedCfg.(*v1.ConfigFile), nil + } + + cfg := &v1.ConfigFile{} + + _, raw, err := l.apiClient.ImageInspectWithRaw(ctx, image) + if err == nil { + if err := json.Unmarshal(raw, cfg); err != nil { + return nil, err + } + } else { + cfg, err = retrieveRemoteConfig(image) + if err != nil { + return nil, errors.Wrap(err, "getting remote config") + } + } + + l.imageCache.Store(image, cfg) + + return cfg, nil +} + // Build performs a docker build and returns the imageID. -func Build(ctx context.Context, out io.Writer, cli APIClient, workspace string, a *latest.DockerArtifact, initialTag string) (string, error) { +func (l *localDaemon) Build(ctx context.Context, out io.Writer, workspace string, a *latest.DockerArtifact, ref string) (string, error) { logrus.Debugf("Running docker build: context: %s, dockerfile: %s", workspace, a.DockerfilePath) // Like `docker build`, we ignore the errors @@ -71,8 +127,8 @@ func Build(ctx context.Context, out io.Writer, cli APIClient, workspace string, progressOutput := streamformatter.NewProgressOutput(out) body := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") - resp, err := cli.ImageBuild(ctx, body, types.ImageBuildOptions{ - Tags: []string{initialTag}, + resp, err := l.apiClient.ImageBuild(ctx, body, types.ImageBuildOptions{ + Tags: []string{ref}, Dockerfile: a.DockerfilePath, BuildArgs: a.BuildArgs, CacheFrom: a.CacheFrom, @@ -98,14 +154,14 @@ func Build(ctx context.Context, out io.Writer, cli APIClient, workspace string, imageID = result.ID } - if err := StreamDockerMessages(out, resp.Body, auxCallback); err != nil { + if err := streamDockerMessages(out, resp.Body, auxCallback); err != nil { return "", err } if imageID == "" { // Maybe this version of Docker doesn't return the digest of the image // that has been built. - imageID, err = ImageID(ctx, cli, initialTag) + imageID, err = l.ImageID(ctx, ref) if err != nil { return "", errors.Wrap(err, "getting digest") } @@ -114,21 +170,21 @@ func Build(ctx context.Context, out io.Writer, cli APIClient, workspace string, return imageID, nil } -// StreamDockerMessages streams formatted json output from the docker daemon +// streamDockerMessages streams formatted json output from the docker daemon // TODO(@r2d4): Make this output much better, this is the bare minimum -func StreamDockerMessages(dst io.Writer, src io.Reader, auxCallback func(jsonmessage.JSONMessage)) error { +func streamDockerMessages(dst io.Writer, src io.Reader, auxCallback func(jsonmessage.JSONMessage)) error { fd, _ := term.GetFdInfo(dst) return jsonmessage.DisplayJSONMessagesStream(src, dst, fd, false, auxCallback) } -// RunPush pushes an image reference to a registry. Returns the image digest. -func RunPush(ctx context.Context, out io.Writer, cli APIClient, ref string) (string, error) { - registryAuth, err := encodedRegistryAuth(ctx, cli, DefaultAuthHelper, ref) +// Push pushes an image reference to a registry. Returns the image digest. +func (l *localDaemon) Push(ctx context.Context, out io.Writer, ref string) (string, error) { + registryAuth, err := l.encodedRegistryAuth(ctx, DefaultAuthHelper, ref) if err != nil { return "", errors.Wrapf(err, "getting auth config for %s", ref) } - rc, err := cli.ImagePush(ctx, ref, types.ImagePushOptions{ + rc, err := l.apiClient.ImagePush(ctx, ref, types.ImagePushOptions{ RegistryAuth: registryAuth, }) if err != nil { @@ -150,7 +206,7 @@ func RunPush(ctx context.Context, out io.Writer, cli APIClient, ref string) (str digest = result.Digest } - if err := StreamDockerMessages(out, rc, auxCallback); err != nil { + if err := streamDockerMessages(out, rc, auxCallback); err != nil { return "", err } @@ -166,42 +222,30 @@ func RunPush(ctx context.Context, out io.Writer, cli APIClient, ref string) (str return digest, nil } -func AddTag(src, target string) error { - srcRef, err := name.ParseReference(src, name.WeakValidation) - if err != nil { - return errors.Wrap(err, "getting source reference") - } - - auth, err := authn.DefaultKeychain.Resolve(srcRef.Context().Registry) +// Load loads an image from a tar file. Returns the imageID for the loaded image. +func (l *localDaemon) Load(ctx context.Context, out io.Writer, input io.Reader, ref string) (string, error) { + resp, err := l.apiClient.ImageLoad(ctx, input, false) if err != nil { - return err + return "", errors.Wrap(err, "loading image into docker daemon") } + defer resp.Body.Close() - targetRef, err := name.ParseReference(target, name.WeakValidation) + err = streamDockerMessages(out, resp.Body, nil) if err != nil { - return errors.Wrap(err, "getting target reference") + return "", errors.Wrap(err, "reading from image load response") } - return addTag(srcRef, targetRef, auth, http.DefaultTransport) + return l.ImageID(ctx, ref) } -func addTag(ref name.Reference, targetRef name.Reference, auth authn.Authenticator, t http.RoundTripper) error { - tr, err := transport.New(ref.Context().Registry, auth, t, []string{targetRef.Scope(transport.PushScope)}) - if err != nil { - return err - } - - img, err := remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(tr)) - if err != nil { - return err - } - - return remote.Write(targetRef, img, auth, t) +// Tag adds a tag to an image. +func (l *localDaemon) Tag(ctx context.Context, image, ref string) error { + return l.apiClient.ImageTag(ctx, image, ref) } // ImageID returns the image ID for a corresponding reference. -func ImageID(ctx context.Context, cli APIClient, ref string) (string, error) { - image, _, err := cli.ImageInspectWithRaw(ctx, ref) +func (l *localDaemon) ImageID(ctx context.Context, ref string) (string, error) { + image, _, err := l.apiClient.ImageInspectWithRaw(ctx, ref) if err != nil { if client.IsErrNotFound(err) { return "", nil @@ -212,34 +256,6 @@ func ImageID(ctx context.Context, cli APIClient, ref string) (string, error) { return image.ID, nil } -func remoteImage(identifier string) (v1.Image, error) { - ref, err := name.ParseReference(identifier, name.WeakValidation) - if err != nil { - return nil, errors.Wrap(err, "parsing initial ref") - } - - auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) - if err != nil { - return nil, errors.Wrap(err, "getting default keychain auth") - } - - return remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport)) -} - -func RemoteDigest(identifier string) (string, error) { - img, err := remoteImage(identifier) - if err != nil { - return "", errors.Wrap(err, "getting image") - } - - h, err := img.Digest() - if err != nil { - return "", errors.Wrap(err, "getting digest") - } - - return h.String(), nil -} - // GetBuildArgs gives the build args flags for docker build. func GetBuildArgs(a *latest.DockerArtifact) []string { var args []string diff --git a/pkg/skaffold/docker/image_test.go b/pkg/skaffold/docker/image_test.go index 8456afead8b..3f872bd2fda 100644 --- a/pkg/skaffold/docker/image_test.go +++ b/pkg/skaffold/docker/image_test.go @@ -36,7 +36,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestRunPush(t *testing.T) { +func TestPush(t *testing.T) { var tests = []struct { description string imageName string @@ -73,7 +73,11 @@ func TestRunPush(t *testing.T) { } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - digest, err := RunPush(context.Background(), ioutil.Discard, &test.api, test.imageName) + localDocker := &localDaemon{ + apiClient: &test.api, + } + + digest, err := localDocker.Push(context.Background(), ioutil.Discard, test.imageName) testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expectedDigest, digest) }) @@ -108,7 +112,11 @@ func TestRunBuild(t *testing.T) { } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - _, err := Build(context.Background(), ioutil.Discard, &test.api, ".", &latest.DockerArtifact{}, "finalimage") + localDocker := &localDaemon{ + apiClient: &test.api, + } + + _, err := localDocker.Build(context.Background(), ioutil.Discard, ".", &latest.DockerArtifact{}, "finalimage") testutil.CheckError(t, test.shouldErr, err) }) @@ -149,7 +157,11 @@ func TestImageID(t *testing.T) { } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - imageID, err := ImageID(context.Background(), &test.api, test.ref) + localDocker := &localDaemon{ + apiClient: &test.api, + } + + imageID, err := localDocker.ImageID(context.Background(), test.ref) testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expected, imageID) }) diff --git a/pkg/skaffold/docker/parse.go b/pkg/skaffold/docker/parse.go index e0a8638e5bb..221940adf94 100644 --- a/pkg/skaffold/docker/parse.go +++ b/pkg/skaffold/docker/parse.go @@ -18,13 +18,11 @@ package docker import ( "context" - "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" - "sync" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" @@ -369,53 +367,13 @@ func GetDependencies(ctx context.Context, workspace string, a *latest.DockerArti return dependencies, nil } -var imageCache sync.Map - func retrieveImage(image string) (*v1.ConfigFile, error) { - cachedCfg, present := imageCache.Load(image) - if present { - return cachedCfg.(*v1.ConfigFile), nil - } - - client, err := NewAPIClient() - if err != nil { - return nil, err - } - - cfg := &v1.ConfigFile{} - raw, err := retrieveLocalImage(client, image) - if err == nil { - if err := json.Unmarshal(raw, cfg); err != nil { - return nil, err - } - } else { - cfg, err = retrieveRemoteConfig(image) - if err != nil { - return nil, errors.Wrap(err, "getting remote config") - } - } - - imageCache.Store(image, cfg) - - return cfg, nil -} - -func retrieveLocalImage(client APIClient, image string) ([]byte, error) { - _, raw, err := client.ImageInspectWithRaw(context.Background(), image) - if err != nil { - return nil, err - } - - return raw, nil -} - -func retrieveRemoteConfig(identifier string) (*v1.ConfigFile, error) { - img, err := remoteImage(identifier) + localDaemon, err := NewAPIClient() // Cached after first call if err != nil { - return nil, errors.Wrap(err, "getting image") + return nil, errors.Wrap(err, "getting docker client") } - return img.ConfigFile() + return localDaemon.ConfigFile(context.Background(), image) } func processCopy(value *parser.Node, envs map[string]string) ([]string, error) { diff --git a/pkg/skaffold/docker/remote.go b/pkg/skaffold/docker/remote.go new file mode 100644 index 00000000000..ef029deff11 --- /dev/null +++ b/pkg/skaffold/docker/remote.go @@ -0,0 +1,98 @@ +/* +Copyright 2018 The Skaffold Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +import ( + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/pkg/errors" +) + +func AddTag(src, target string) error { + srcRef, err := name.ParseReference(src, name.WeakValidation) + if err != nil { + return errors.Wrap(err, "getting source reference") + } + + auth, err := authn.DefaultKeychain.Resolve(srcRef.Context().Registry) + if err != nil { + return err + } + + targetRef, err := name.ParseReference(target, name.WeakValidation) + if err != nil { + return errors.Wrap(err, "getting target reference") + } + + return addTag(srcRef, targetRef, auth, http.DefaultTransport) +} + +func addTag(ref name.Reference, targetRef name.Reference, auth authn.Authenticator, t http.RoundTripper) error { + tr, err := transport.New(ref.Context().Registry, auth, t, []string{targetRef.Scope(transport.PushScope)}) + if err != nil { + return err + } + + img, err := remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(tr)) + if err != nil { + return err + } + + return remote.Write(targetRef, img, auth, t) +} + +func RemoteDigest(identifier string) (string, error) { + img, err := remoteImage(identifier) + if err != nil { + return "", errors.Wrap(err, "getting image") + } + + h, err := img.Digest() + if err != nil { + return "", errors.Wrap(err, "getting digest") + } + + return h.String(), nil +} + +func retrieveRemoteConfig(identifier string) (*v1.ConfigFile, error) { + img, err := remoteImage(identifier) + if err != nil { + return nil, errors.Wrap(err, "getting image") + } + + return img.ConfigFile() +} + +func remoteImage(identifier string) (v1.Image, error) { + ref, err := name.ParseReference(identifier, name.WeakValidation) + if err != nil { + return nil, errors.Wrap(err, "parsing initial ref") + } + + auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) + if err != nil { + return nil, errors.Wrap(err, "getting default keychain auth") + } + + return remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport)) +} diff --git a/testutil/fake_image_api.go b/testutil/fake_image_api.go index 239f92eca3c..8b1bc893438 100644 --- a/testutil/fake_image_api.go +++ b/testutil/fake_image_api.go @@ -30,7 +30,7 @@ import ( ) type FakeAPIClient struct { - *client.Client + client.CommonAPIClient TagToImageID map[string]string ErrImageBuild bool