From f4bfed2f1e79e1e351e277b6692c053080a0bd45 Mon Sep 17 00:00:00 2001 From: Xu Deng Date: Mon, 11 Sep 2023 16:36:57 +0000 Subject: [PATCH] Add ecr-cred-injector --- Dockerfile | 5 +- .../templates/deployment.yaml | 6 + cmd/server.go | 7 + ecrtokenrefresher/Dockerfile | 24 ++ .../secrets/registrymirror/registrymirror.go | 14 +- go.mod | 21 ++ go.sum | 31 +++ pkg/authenticator/ecrsecret.go | 5 +- pkg/registry/ecr_cred_injector.go | 255 ++++++++++++++++++ pkg/registry/ecr_cred_injector_test.go | 50 ++++ 10 files changed, 406 insertions(+), 12 deletions(-) create mode 100644 ecrtokenrefresher/Dockerfile create mode 100644 pkg/registry/ecr_cred_injector.go create mode 100644 pkg/registry/ecr_cred_injector_test.go diff --git a/Dockerfile b/Dockerfile index ecf8ae73..f05b467b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.17 as builder +FROM golang:1.20 as builder WORKDIR /workspace # Copy the Go Modules manifests @@ -15,13 +15,14 @@ COPY api/ api/ COPY controllers/ controllers/ COPY config/ config/ COPY pkg/ pkg/ +COPY cmd/ cmd/ # Build RUN GOPROXY=direct CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o package-manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot +FROM gcr.io/distroless/static:debug-nonroot WORKDIR / COPY --from=builder /workspace/package-manager . USER 65532:65532 diff --git a/charts/eks-anywhere-packages/templates/deployment.yaml b/charts/eks-anywhere-packages/templates/deployment.yaml index ac9f2c13..c18b69b7 100644 --- a/charts/eks-anywhere-packages/templates/deployment.yaml +++ b/charts/eks-anywhere-packages/templates/deployment.yaml @@ -101,6 +101,8 @@ spec: - mountPath: /tmp/ecr-token name: ecr-token readOnly: true + - name: aws-secret + mountPath: /tmp/aws-secret initContainers: - name: init-job image: {{.Values.sourceRegistry}}{{ template "template.image" .Values.cronjob }} @@ -146,6 +148,10 @@ spec: defaultMode: 420 secretName: ecr-token optional: true + - name: aws-secret + secret: + secretName: aws-secret + optional: true {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cmd/server.go b/cmd/server.go index 50f242fe..fd5138bd 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -31,6 +31,7 @@ import ( "github.com/aws/eks-anywhere-packages/api/v1alpha1" "github.com/aws/eks-anywhere-packages/controllers" pkgConfig "github.com/aws/eks-anywhere-packages/pkg/config" + "github.com/aws/eks-anywhere-packages/pkg/registry" "github.com/aws/eks-anywhere-packages/pkg/webhook" ) @@ -87,6 +88,12 @@ func server() error { return fmt.Errorf("unable to start manager: %v", err) } + ecrCredAdapter, err := registry.NewECRCredInjector(rootCmd.Context(), mgr.GetClient(), packageLog) + if err != nil { + return fmt.Errorf("unable to create ecrCredAdapter: %v", err) + } + go ecrCredAdapter.Run(rootCmd.Context()) + if err = controllers.RegisterPackageBundleReconciler(mgr); err != nil { return fmt.Errorf("unable to register package bundle controller: %v", err) } diff --git a/ecrtokenrefresher/Dockerfile b/ecrtokenrefresher/Dockerfile new file mode 100644 index 00000000..caa404f1 --- /dev/null +++ b/ecrtokenrefresher/Dockerfile @@ -0,0 +1,24 @@ +# Build the manager binary +FROM golang:1.20 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY pkg/ pkg/ +COPY cmd/ cmd/ + +# Build +RUN GOPROXY=direct CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o ecr-token-refresh ./cmd/ecr-token-refresher + +FROM gcr.io/distroless/static:debug-nonroot +WORKDIR / +COPY --from=builder /workspace/ecr-token-refresh . +USER 65532:65532 + +ENTRYPOINT ["/ecr-token-refresh"] diff --git a/ecrtokenrefresher/pkg/secrets/registrymirror/registrymirror.go b/ecrtokenrefresher/pkg/secrets/registrymirror/registrymirror.go index 32735217..e81e887d 100644 --- a/ecrtokenrefresher/pkg/secrets/registrymirror/registrymirror.go +++ b/ecrtokenrefresher/pkg/secrets/registrymirror/registrymirror.go @@ -66,12 +66,8 @@ func (mirror *RegistryMirrorSecret) GetClusterCredentials(clientSets secrets.Clu CA: string(secret.Data[caKey]), Insecure: string(secret.Data[insecureKey]), } - if credential.Registry != "" && credential.Username != "" && credential.Password != "" { - clusterCredentials[clusterName] = []*secrets.Credential{credential} - utils.InfoLogger.Println("success.") - } else { - utils.InfoLogger.Println("empty credential.") - } + clusterCredentials[clusterName] = []*secrets.Credential{credential} + utils.InfoLogger.Println("success.") } else { utils.ErrorLogger.Println(err) return nil, err @@ -94,7 +90,9 @@ func (mirror *RegistryMirrorSecret) BroadcastCredentials() error { if clusterName == mirror.mgmtClusterName { data[corev1.DockerConfigJsonKey] = configJson } - data[clusterName+"_ca.crt"] = []byte(creds[0].CA) + if len(creds[0].CA) > 0 { // when "" ca is used, no tls verification will succeed + data[clusterName+"_ca.crt"] = []byte(creds[0].CA) + } data["config.json"] = configJson if creds[0].Insecure == "true" { data[clusterName+"_insecure"] = []byte(creds[0].Insecure) @@ -109,11 +107,13 @@ func (mirror *RegistryMirrorSecret) BroadcastCredentials() error { } secret, _ := k8s.GetSecret(defaultClientSet, credName, constants.PackagesNamespace) if secret == nil { + utils.InfoLogger.Printf("Create secret %s in namespace %s", credName, constants.PackagesNamespace) _, err := k8s.CreateSecret(defaultClientSet, credName, constants.PackagesNamespace, data) if err != nil { return err } } else { + utils.InfoLogger.Printf("Update secret %s in namespace %s", credName, constants.PackagesNamespace) _, err := k8s.UpdateSecret(defaultClientSet, constants.PackagesNamespace, secret, data) if err != nil { return err diff --git a/go.mod b/go.mod index 953251d9..6feae088 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/squirrel v1.5.3 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/aws/aws-sdk-go-v2 v1.21.0 github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect @@ -163,3 +164,23 @@ require ( sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) + +require ( + github.com/aws/aws-sdk-go-v2/config v1.18.39 + github.com/aws/smithy-go v1.14.2 // indirect +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.13.37 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.19.5 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect +) + +require github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 9e28d70e..98cd86b7 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,32 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= +github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= +github.com/aws/aws-sdk-go-v2/config v1.18.39 h1:oPVyh6fuu/u4OiW4qcuQyEtk7U7uuNBmHmJSLg1AJsQ= +github.com/aws/aws-sdk-go-v2/config v1.18.39/go.mod h1:+NH/ZigdPckFpgB1TRcRuWCB/Kbbvkxc/iNAKTq5RhE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.37 h1:BvEdm09+ZEh2XtN+PVHPcYwKY3wIeB6pw7vPRM4M9/U= +github.com/aws/aws-sdk-go-v2/credentials v1.13.37/go.mod h1:ACLrdkd4CLZyXOghZ8IYumQbcooAcp2jo/s2xsFH8IM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= +github.com/aws/aws-sdk-go-v2/service/ecr v1.19.5 h1:hg2/a7rE9dwYr+/DPNzHQ+IsHXLNt1NsQVUecBtA8os= +github.com/aws/aws-sdk-go-v2/service/ecr v1.19.5/go.mod h1:pGwmNL8hN0jpBfKfTbmu+Rl0bJkDhaGl+9PQLrZ4KLo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 h1:2PylFCfKCEDv6PeSN09pC/VUiRd10wi1VfHG5FrW0/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6 h1:pSB560BbVj9ZlJZF4WYj5zsytWHWKxg+NgyGV4B2L58= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.6/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= +github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= +github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -262,6 +288,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -340,6 +367,10 @@ github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItq github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= diff --git a/pkg/authenticator/ecrsecret.go b/pkg/authenticator/ecrsecret.go index 563a045e..81a623f3 100644 --- a/pkg/authenticator/ecrsecret.go +++ b/pkg/authenticator/ecrsecret.go @@ -21,7 +21,7 @@ const ( ecrTokenName = "ecr-token" cronJobName = "cron-ecr-renew" jobExecName = "eksa-auth-refresher-" - mirrorCredName = "registry-mirror-cred" + MirrorCredName = "registry-mirror-cred" ) type ecrSecret struct { @@ -136,7 +136,7 @@ func (s *ecrSecret) GetSecretValues(ctx context.Context, namespace string) (map[ imagePullSecret[0] = make(map[string]string) imagePullSecret[0]["name"] = ecrTokenName imagePullSecret[1] = make(map[string]string) - imagePullSecret[1]["name"] = mirrorCredName + imagePullSecret[1]["name"] = MirrorCredName values["imagePullSecrets"] = imagePullSecret return values, nil @@ -147,7 +147,6 @@ func (s *ecrSecret) cleanupPrevRuns(ctx context.Context) error { deletePropagation := metav1.DeletePropagationBackground jobs, err := s.clientset.BatchV1().Jobs(api.PackageNamespace). List(ctx, metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) - if err != nil { return err } diff --git a/pkg/registry/ecr_cred_injector.go b/pkg/registry/ecr_cred_injector.go new file mode 100644 index 00000000..53808ea6 --- /dev/null +++ b/pkg/registry/ecr_cred_injector.go @@ -0,0 +1,255 @@ +package registry + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + awsCredentials "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/docker/cli/cli/config" + dockerTypes "github.com/docker/cli/cli/config/types" + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "oras.land/oras-go/v2/registry/remote/auth" + "sigs.k8s.io/controller-runtime/pkg/client" + + api "github.com/aws/eks-anywhere-packages/api/v1alpha1" + "github.com/aws/eks-anywhere-packages/pkg/authenticator" +) + +const ( + awsSecret = "/tmp/config/aws-secret" +) + +// ECRCredInjector is an adapter to convert ECR credential to Docker credential. Since the converted docker credential is only valid for 12 hours, this adapter is constantly running. It's responsibility is to make sure docker config in the filesystem contains ECR credential to pull bundle yaml and charts. +type ECRCredInjector struct { + k8sClient client.Client + ecrClient *ecr.Client + log logr.Logger +} + +func NewECRCredInjector(ctx context.Context, k8sClient client.Client, log logr.Logger) (*ECRCredInjector, error) { + l := log.WithName("ECRCredInjector") + ecrClient, err := GetECRClient(ctx, l) + if err != nil { + return nil, err + } + + return &ECRCredInjector{ + k8sClient: k8sClient, + ecrClient: ecrClient, + log: l, + }, nil +} + +func (a *ECRCredInjector) Run(ctx context.Context) { + err := a.Refresh(ctx) + if err != nil { + a.log.Error(err, "Failed to inject ECR credential to docker config") + } else { + a.log.Info("ECR credential is injected to the docker config file") + } + + for range time.Tick(time.Hour) { + err := a.Refresh(ctx) + if err != nil { + a.log.Error(err, "Failed to refresh ECR credential in dockerconfig file") + } else { + a.log.Info("injected ECR credential has be refreshed") + } + } +} + +func (a *ECRCredInjector) Refresh(ctx context.Context) error { + a.log.Info("Refreshing ECR credential") + cred, err := GetCredential(a.ecrClient) + if err != nil { + return err + } + + dockerSecret, err := a.GetRegistryMirrorSecret(ctx) + if err != nil { + return err + } + + registry, err := a.GetECR(ctx) + if err != nil { + return err + } + + if !IsECRRegistry(registry) { + a.log.Info("defaultRegistry is not ECR registry, skip injecting credential to docker config") + } + // update "config.json" in dockerSecret + return a.InjectCredential(ctx, *dockerSecret, registry, cred) +} + +// GetECR get the defaultRegistry config from package bundle controller. +func (a *ECRCredInjector) GetECR(ctx context.Context) (string, error) { + pbc := &api.PackageBundleController{} + err := a.k8sClient.Get(ctx, types.NamespacedName{ + Namespace: api.PackageNamespace, + Name: os.Getenv("CLUSTER_NAME"), + }, pbc) + if err != nil { + return "", err + } + + // defaultRegistry could be followed by path + ss := strings.Split(pbc.Spec.DefaultRegistry, "/") + return ss[0], nil +} + +// GetRegistryMirrorSecret gets registry mirror secret from eksa-packages namespace +func (a *ECRCredInjector) GetRegistryMirrorSecret(ctx context.Context) (*v1.Secret, error) { + var secret v1.Secret + + err := a.k8sClient.Get(ctx, types.NamespacedName{ + Namespace: api.PackageNamespace, + // this secret is populated by token refresher + Name: authenticator.MirrorCredName, + }, &secret) + if err != nil { + return nil, err + } + + return &secret, nil +} + +// InjectCredentail update field "config.json" in the secret, which is used by packages controller's oras and helm +func (a *ECRCredInjector) InjectCredential(ctx context.Context, secret v1.Secret, registry string, cred auth.Credential) error { + d := secret.Data["config.json"] + var configJson []byte + if _, err := base64.StdEncoding.Decode(d, configJson); err != nil { + return err + } + + dockerConfig, err := config.LoadFromReader(strings.NewReader(string(configJson))) + if err != nil { + return err + } + dockerConfig.AuthConfigs[registry] = dockerTypes.AuthConfig{ + Username: cred.Username, + Password: cred.Password, + } + + buf := new(bytes.Buffer) + err = dockerConfig.SaveToWriter(buf) + if err != nil { + return err + } + + secret.Data["config.json"] = buf.Bytes() + return a.k8sClient.Update(ctx, &secret, &client.UpdateOptions{}) +} + +func GetECRClient(ctx context.Context, log logr.Logger) (*ecr.Client, error) { + var c *ecr.Client + var err error + c = getECRClientFromConfig(ctx, log) + if c == nil { + c, err = getECRClientFromVariables(ctx, log) + if err != nil { + return nil, fmt.Errorf("Unable to load AWS config, " + err.Error()) + } + } + return c, nil +} + +// getECRClientFromConfig tries to get ecrClient from aws config file, if failed, return nil +func getECRClientFromConfig(ctx context.Context, log logr.Logger) *ecr.Client { + configPath := awsSecret + "/config" + // check if configPath exist + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Info("aws config file does not exist, skip loading config") + return nil + } + + cfg, err := awsConfig.LoadDefaultConfig(ctx, + awsConfig.WithSharedConfigFiles([]string{configPath}), + ) + if err != nil { + fmt.Println("Unable to load AWS config from file, " + err.Error()) + return nil + } + + return ecr.NewFromConfig(cfg) +} + +// getECRClientFromVariables tries to get ecrClient from access_key and region +func getECRClientFromVariables(ctx context.Context, log logr.Logger) (*ecr.Client, error) { + // similar to https://github.com/aws/eks-anywhere-packages/blob/eca65837c277f7769f721f2251b3e92f0d8edb68/credentialproviderpackage/pkg/awscred/awscred.go#L11 + accessKeyPath := awsSecret + "/AWS_ACCESS_KEY_ID" + secretAccessKeyPath := awsSecret + "/AWS_SECRET_ACCESS_KEY" + regionPath := awsSecret + "/REGION" + + accessKeyByte, err := os.ReadFile(accessKeyPath) + if err != nil { + log.Error(err, "Cannot get access key from file") + } + accessKey := strings.Trim(string(accessKeyByte), "'") + secretAccessKeyByte, err := os.ReadFile(secretAccessKeyPath) + if err != nil { + log.Error(err, "Cannot get secret access key from file") + } + secretAccessKey := strings.Trim(string(secretAccessKeyByte), "'") + regionByte, err := os.ReadFile(regionPath) + if err != nil { + log.Error(err, "Cannot get region from file, %v") + } + region := strings.Trim(string(regionByte), "'") + + cfg, err := awsConfig.LoadDefaultConfig(ctx, + awsConfig.WithCredentialsProvider(awsCredentials.NewStaticCredentialsProvider(accessKey, secretAccessKey, "")), + awsConfig.WithRegion(region), + ) + if err != nil { + return nil, err + } + + return ecr.NewFromConfig(cfg), nil +} + +func GetCredential(ecrClient *ecr.Client) (auth.Credential, error) { + out, err := ecrClient.GetAuthorizationToken(context.Background(), &ecr.GetAuthorizationTokenInput{}) + if err != nil { + return auth.EmptyCredential, err + } + token := out.AuthorizationData[0].AuthorizationToken + + cred, err := ExtractECRToken(aws.ToString(token)) + if err != nil { + return auth.EmptyCredential, err + } + + return *cred, nil +} + +func ExtractECRToken(token string) (*auth.Credential, error) { + decodedToken, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + parts := strings.SplitN(string(decodedToken), ":", 2) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid token: expected two parts, got %d", len(parts)) + } + + return &auth.Credential{ + Username: parts[0], + Password: parts[1], + }, nil +} + +func IsECRRegistry(registry string) bool { + return strings.HasSuffix(registry, "amazonaws.com") +} diff --git a/pkg/registry/ecr_cred_injector_test.go b/pkg/registry/ecr_cred_injector_test.go new file mode 100644 index 00000000..d42897fd --- /dev/null +++ b/pkg/registry/ecr_cred_injector_test.go @@ -0,0 +1,50 @@ +package registry_test + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/go-logr/logr" + "github.com/golang/mock/gomock" + + ctrlmocks "github.com/aws/eks-anywhere-packages/controllers/mocks" + "github.com/aws/eks-anywhere-packages/pkg/registry" +) + +func TestECRCredInjector(t *testing.T) { + gomockController := gomock.NewController(t) + k8sClient := ctrlmocks.NewMockClient(gomockController) + injector, err := registry.NewECRCredInjector(context.Background(), k8sClient, logr.Discard()) + if err != nil { + t.Errorf("should not have failed to create injector: %v", err) + } + err = injector.Refresh(context.Background()) + if err == nil { + t.Error("refresh should have failed because no AWS credential has been set") + } +} + +func TestExtractECRToken(t *testing.T) { + auth, err := registry.ExtractECRToken(base64.StdEncoding.EncodeToString([]byte("username:password"))) + if err != nil { + t.Errorf("encode should not fail: %v", err) + } + if auth.Username != "username" { + t.Errorf("username is not expected") + } + if auth.Password != "password" { + t.Errorf("password is not expected") + } +} + +func TestIsECRRegistry(t *testing.T) { + res := registry.IsECRRegistry("5551212.dkr.ecr.us-west-2.amazonaws.com") + if res != true { + t.Errorf("registry is expected to be ECR") + } + res = registry.IsECRRegistry("localhost") + if res != false { + t.Errorf("registry is not ECR") + } +}