From 5cb5ccbf5327eb004d1e4f7bc4ca87a7d3769299 Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Fri, 29 Mar 2024 15:35:50 -0700 Subject: [PATCH] Fix ECR auth --- examples/tests/ecr/.dockerignore | 1 + examples/tests/ecr/Pulumi.yaml | 36 ++++++++++++++++++++++++++++++++ examples/yaml_test.go | 15 +++++++++++++ go.mod | 4 ++-- provider/internal/cli.go | 19 ++++++++++++----- provider/internal/image.go | 14 ++++++++----- provider/internal/index.go | 8 +++++-- 7 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 examples/tests/ecr/.dockerignore create mode 100644 examples/tests/ecr/Pulumi.yaml diff --git a/examples/tests/ecr/.dockerignore b/examples/tests/ecr/.dockerignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/examples/tests/ecr/.dockerignore @@ -0,0 +1 @@ +* diff --git a/examples/tests/ecr/Pulumi.yaml b/examples/tests/ecr/Pulumi.yaml new file mode 100644 index 0000000..13636af --- /dev/null +++ b/examples/tests/ecr/Pulumi.yaml @@ -0,0 +1,36 @@ +name: ecr +description: Push to AWS ECR with caching +outputs: + ref: ${my-image.ref} +resources: + ecr-repository: + type: aws:ecr:Repository + properties: + forceDelete: true + my-image: + type: dockerbuild:Image + properties: + tags: + - ${ecr-repository.repositoryUrl}:tag-name + push: true + context: + location: . + dockerfile: + inline: FROM alpine + cacheFrom: + - registry: + ref: ${ecr-repository.repositoryUrl}:cache + cacheTo: + - registry: + ref: ${ecr-repository.repositoryUrl}:cache + imageManifest: true + ociMediaTypes: true + registries: + - username: ${auth-token.userName} + password: ${auth-token.password} + address: ${ecr-repository.repositoryUrl} +runtime: yaml +variables: + auth-token: + fn::aws:ecr:getAuthorizationToken: + registryId: ${ecr-repository.registryId} diff --git a/examples/yaml_test.go b/examples/yaml_test.go index 23dc623..3a503fc 100644 --- a/examples/yaml_test.go +++ b/examples/yaml_test.go @@ -25,3 +25,18 @@ func TestYAMLExample(t *testing.T) { integration.ProgramTest(t, &test) } + +func TestECR(t *testing.T) { + if os.Getenv("AWS_SESSION_TOKEN") == "" { + t.Skip("Missing AWS credentials") + } + + cwd, err := os.Getwd() + require.NoError(t, err) + + test := integration.ProgramTestOptions{ + Dir: path.Join(cwd, "tests/ecr"), + } + + integration.ProgramTest(t, &test) +} diff --git a/go.mod b/go.mod index f4f7436..fe54861 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,9 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.9.0 - github.com/theupdateframework/notary v0.7.0 github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5 go.uber.org/mock v0.3.0 + golang.org/x/crypto v0.21.0 google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -392,6 +392,7 @@ require ( github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.16 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/theupdateframework/notary v0.7.0 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect github.com/timonwong/loggercheck v0.9.4 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -446,7 +447,6 @@ require ( go.uber.org/zap v1.26.0 // indirect gocloud.dev v0.36.0 // indirect gocloud.dev/secrets/hashivault v0.28.0 // indirect - golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/mod v0.16.0 // indirect diff --git a/provider/internal/cli.go b/provider/internal/cli.go index 7457b8e..a0d743c 100644 --- a/provider/internal/cli.go +++ b/provider/internal/cli.go @@ -30,6 +30,7 @@ import ( "github.com/docker/buildx/commands" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/credentials" cfgtypes "github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/streams" "github.com/moby/buildkit/client" @@ -66,7 +67,8 @@ type Cli interface { } // wrap creates a new cli client with auth configs layered on top of our host's -// auth. +// auth. Repeated auth for the same host will take precedence over earlier +// credentials. func wrap(host *host, registries ...Registry) (*cli, error) { // We need to create a new DockerCLI instance because we don't want the // auth changes we make to the ConfigFile to leak to the host. @@ -81,10 +83,17 @@ func wrap(host *host, registries ...Registry) (*cli, error) { } for _, r := range registries { - // HostNewName takes care of DockerHub's special-casing for us. - h := config.HostNewName(r.Address) - auths[h.CredHost] = cfgtypes.AuthConfig{ - ServerAddress: h.Hostname, + // Special handling for legacy DockerHub domains. The OCI-compliant + // registry is registry-1.docker.io but this is stored in config under the + // legacy name. + // https://github.com/docker/cli/issues/3793#issuecomment-1269051403 + key := credentials.ConvertToHostname(r.Address) + if key == "registry-1.docker.io" || key == "index.docker.io" || key == "docker.io" { + key = "https://index.docker.io/v1/" + } + + auths[key] = cfgtypes.AuthConfig{ + ServerAddress: r.Address, Username: r.Username, Password: r.Password, } diff --git a/provider/internal/image.go b/provider/internal/image.go index aa5589f..91e8f9c 100644 --- a/provider/internal/image.go +++ b/provider/internal/image.go @@ -338,16 +338,20 @@ func (is *ImageState) Annotate(a infer.Annotator) { // client produces a CLI client with scoped to this resource and layered on top // of any host-level credentials. -func (i *Image) client(ctx provider.Context, state ImageState, args ImageArgs) (Client, error) { - cfg := infer.GetConfig[Config](ctx) +func (i *Image) client(pctx provider.Context, state ImageState, args ImageArgs) (Client, error) { + ctx := context.Context(pctx) + + cfg := infer.GetConfig[Config](pctx) if cli, ok := ctx.Value(_mockClientKey).(Client); ok { return cli, nil } - // Layer auth from args, state, and the provider in that order. - auths := cfg.Registries - auths = append(auths, state.Registries...) + // We prefer auth from args, the provider, and state in that order. We + // build a slice in reverse order because wrap() will overwrite earlier + // entries with later ones. + auths := state.Registries + auths = append(auths, cfg.Registries...) auths = append(auths, args.Registries...) return wrap(cfg.host, auths...) diff --git a/provider/internal/index.go b/provider/internal/index.go index ce93a3d..e7c0de9 100644 --- a/provider/internal/index.go +++ b/provider/internal/index.go @@ -298,8 +298,12 @@ func (i *Index) client( return cli, nil } - auths := cfg.Registries - auths = append(auths, state.Registry, args.Registry) + // We prefer auth from args, the provider, and state in that order. We + // build a slice in reverse order because wrap() will overwrite earlier + // entries with later ones. + auths := []Registry{state.Registry} + auths = append(auths, cfg.Registries...) + auths = append(auths, args.Registry) return wrap(cfg.host, auths...) }