From a8633cced417975342c2a2edbe55130a4d80459e Mon Sep 17 00:00:00 2001 From: Tobias Gruetzmacher Date: Mon, 27 Jan 2025 14:35:09 +0100 Subject: [PATCH] Add "environment" credential helper --- deploy/Dockerfile | 4 + go.mod | 1 + go.sum | 2 + tools/tools.go | 1 + .../isometry/docker-credential-env/.gitignore | 2 + .../docker-credential-env/.goreleaser.yml | 50 +++++ .../isometry/docker-credential-env/LICENSE | 21 ++ .../isometry/docker-credential-env/README.md | 99 +++++++++ .../isometry/docker-credential-env/env.go | 190 ++++++++++++++++++ .../isometry/docker-credential-env/main.go | 12 ++ vendor/modules.txt | 3 + 11 files changed, 385 insertions(+) create mode 100644 vendor/github.com/isometry/docker-credential-env/.gitignore create mode 100644 vendor/github.com/isometry/docker-credential-env/.goreleaser.yml create mode 100644 vendor/github.com/isometry/docker-credential-env/LICENSE create mode 100644 vendor/github.com/isometry/docker-credential-env/README.md create mode 100644 vendor/github.com/isometry/docker-credential-env/env.go create mode 100644 vendor/github.com/isometry/docker-credential-env/main.go diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 6d0b58c36c..511fa44a01 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -49,6 +49,9 @@ RUN go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/doc # Get ACR docker env credential helper RUN go install github.com/chrismellard/docker-credential-acr-env +# Get docker generic environment credential helper +RUN go install github.com/isometry/docker-credential-env + RUN \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \ @@ -78,6 +81,7 @@ FROM kaniko-base-slim AS kaniko-base COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-gcr /kaniko/docker-credential-gcr COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-ecr-login /kaniko/docker-credential-ecr-login COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-acr-env /kaniko/docker-credential-acr-env +COPY --from=builder --chown=0:0 /usr/local/bin/docker-credential-env /kaniko/docker-credential-env COPY --from=builder /kaniko/.docker /kaniko/.docker diff --git a/go.mod b/go.mod index d38d66d997..48719a3e23 100644 --- a/go.mod +++ b/go.mod @@ -166,6 +166,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/google/subcommands v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/isometry/docker-credential-env v1.3.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect diff --git a/go.sum b/go.sum index 8755d68981..f47e6cd85d 100644 --- a/go.sum +++ b/go.sum @@ -312,6 +312,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/isometry/docker-credential-env v1.3.0 h1:0YCSPhbtJ096HwFKagF+sxveynKzgfnxui6UHhghtBE= +github.com/isometry/docker-credential-env v1.3.0/go.mod h1:k7IkbTjh/x63jnYMHY9IGZ1al/gPfWJGcr8GI4mHGXY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= diff --git a/tools/tools.go b/tools/tools.go index 700efce986..dda834421c 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -25,4 +25,5 @@ import ( _ "github.com/GoogleCloudPlatform/docker-credential-gcr/v2" _ "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login" _ "github.com/chrismellard/docker-credential-acr-env" + _ "github.com/isometry/docker-credential-env" ) diff --git a/vendor/github.com/isometry/docker-credential-env/.gitignore b/vendor/github.com/isometry/docker-credential-env/.gitignore new file mode 100644 index 0000000000..a34dbe9626 --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/.gitignore @@ -0,0 +1,2 @@ +dist +docker-credential-env diff --git a/vendor/github.com/isometry/docker-credential-env/.goreleaser.yml b/vendor/github.com/isometry/docker-credential-env/.goreleaser.yml new file mode 100644 index 0000000000..9c7267ab6a --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/.goreleaser.yml @@ -0,0 +1,50 @@ +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + mod_timestamp: "{{ .CommitTimestamp }}" + flags: + - -trimpath + ldflags: + - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - "386" + - arm + - arm64 + binary: docker-credential-env +archives: + - format: zip + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" +snapshot: + name_template: "{{ .Tag }}-next" +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" + algorithm: sha256 +release: + draft: false +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +brews: + - tap: + owner: isometry + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + folder: Formula + description: Environment-driven Docker credential helper + homepage: https://just.breathe.io/project/docker-credential-env/ + test: | + system "#{bin}/docker-credential-env --version" + install: | + bin.install "docker-credential-env" diff --git a/vendor/github.com/isometry/docker-credential-env/LICENSE b/vendor/github.com/isometry/docker-credential-env/LICENSE new file mode 100644 index 0000000000..805fb5a270 --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2021 Robin Breathe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/isometry/docker-credential-env/README.md b/vendor/github.com/isometry/docker-credential-env/README.md new file mode 100644 index 0000000000..9f488d4e3b --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/README.md @@ -0,0 +1,99 @@ +# Docker Credentials from the Environment + +A [Docker credential helper](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) to streamline repository interactions in scenarios where the cacheing of credentials to `~/.docker/config.json` is undesirable, including CI/CD pipelines, or anywhere ephemeral credentials are used. + +All OCI registry clients that support `~/.docker/config.json` are supported, including [`oras`](https://oras.land/), [`crane`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md), [`grype`](https://github.com/anchore/grype), etc. + +In addition to handling basic username:password credentials, the credential helper also includes special support for: + +* Amazon Elastic Container Registry (ECR) repositories using [standard AWS credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), including automatic cross-account role assumption. +* [GitHub Packages](https://ghcr.io/) via the common `GITHUB_TOKEN` environment variable. + +## Environment Variables + +For the docker repository `https://repo.example.com/v1`, the credential helper expects to retrieve credentials from the following environment variables: + +* `DOCKER_repo_example_com_USR` containing the repository username +* `DOCKER_repo_example_com_PSW` containing the repository password, token or secret. + +If no environment variables for the target repository's FQDN is found, then: + +1. The helper will remove DNS labels from the FQDN one-at-a-time from the right, and look again, for example: +`DOCKER_repo_example_com_USR` => `DOCKER_example_com_USR` => `DOCKER_com_USR` => `DOCKER__USR`. +2. If the target repository is a private AWS ECR repository (FQDN matches the regex `^[0-9]+\.dkr\.ecr\.[-a-z0-9]+\.amazonaws\.com$`), it will attempt to exchange local AWS credentials (most likely exposed through `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables) for short-lived ECR login credentials, including automatic sts:AssumeRole if `role_arn` is specified (e.g. via `AWS_ROLE_ARN`). + +Hyphens within DNS labels are transformed to underscores (`s/-/_/g`) for the purposes of credential lookup. + +## Configuration + +The `docker-credential-env` binary must be installed to `$PATH`, and is enabled via `~/.docker/config.json`: + +* Handle all docker authentication: + + ```json + { + "credsStore": "env" + } + ``` + +* Handle docker authentication for specific repositories: + + ```json + { + "credHelpers": { + "artifactory.example.com": "env" + } + } + ``` + +By default, attempts to explicitly `docker {login,logout}` will generate an error. To ignore these errors, set the environment variable `IGNORE_DOCKER_LOGIN=1`. + +## Example Usage + +### Jenkins + +```groovy +stages { + stage('Push Image to Artifactory') { + environment { + DOCKER_artifactory_example_com = credentials('jenkins.artifactory') // (Vault) Username-Password credential + } + steps { + sh 'docker push artifactory.example.com/example/example-image:1.0' + } + } + + stage('Push Image to Docker Hub') { + environment { + DOCKER_docker_com = credentials('hub.docker.com') // Username-Password credential, exploiting domain search + } + steps { + sh 'docker push hub.docker.com/example/example-image:1.0' + } + } + + stage('Push Image to AWS-ECR') { + environment { + // any standard AWS authentication mechanisms are supported + AWS_ROLE_ARN = 'arn:aws:iam::123456789:role/jenkins-user' // triggers automatic sts:AssumeRole + // AWS_CONFIG_FILE = file('AWS_CONFIG') + // AWS_PROFILE = 'jenkins' + AWS_ACCESS_KEY_ID = credentials('AWS_ACCESS_KEY_ID') // String credential + AWS_SECRET_ACCESS_KEY = credentials('AWS_SECRET_ACCESS_KEY') // String credential + } + steps { + sh 'docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/example/example-image:1.0' + } + } + + stage('Push Image to GHCR') { + environment { + GITHUB_TOKEN = credentials('github') // String credential + } + steps { + sh 'docker push ghcr.io/example/example-image:1.0' + } + } + +} +``` diff --git a/vendor/github.com/isometry/docker-credential-env/env.go b/vendor/github.com/isometry/docker-credential-env/env.go new file mode 100644 index 0000000000..60b8ab8889 --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/env.go @@ -0,0 +1,190 @@ +package main + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "net/url" + "os" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/aws/aws-sdk-go-v2/service/sts" + docker_credentials "github.com/docker/docker-credential-helpers/credentials" +) + +var ecrHostname = regexp.MustCompile(`^[0-9]+\.dkr\.ecr\.[-a-z0-9]+\.amazonaws\.com$`) +var ghcrHostname = regexp.MustCompile(`^ghcr\.io$`) + +const ( + defaultScheme = "https://" + envPrefix = "DOCKER" + envUsernameSuffix = "USR" + envPasswordSuffix = "PSW" + envSeparator = "_" + envIgnoreLogin = "IGNORE_DOCKER_LOGIN" +) + +type NotSupportedError struct{} + +func (m *NotSupportedError) Error() string { + return "not supported" +} + +// Env implements the Docker credentials Helper interface. +type Env struct{} + +// Add implements the set verb +func (*Env) Add(*docker_credentials.Credentials) error { + switch { + case os.Getenv(envIgnoreLogin) != "": + return nil + default: + return fmt.Errorf("add: %w", &NotSupportedError{}) + } +} + +// Delete implements the erase verb +func (*Env) Delete(string) error { + switch { + case os.Getenv(envIgnoreLogin) != "": + return nil + default: + return fmt.Errorf("delete: %w", &NotSupportedError{}) + } +} + +// List implements the list verb +func (*Env) List() (map[string]string, error) { + return nil, fmt.Errorf("list: %w", &NotSupportedError{}) +} + +// Get implements the get verb +func (e *Env) Get(serverURL string) (username string, password string, err error) { + var ( + hostname string + ok bool + ) + + hostname, err = getHostname(serverURL) + if err != nil { + return + } + + if username, password, ok = getEnvCredentials(hostname); ok { + return + } + + if ecrHostname.MatchString(hostname) { + // This is an AWS ECR Docker Registry: .dkr.ecr..amazonaws.com + username, password, err = getEcrToken() + return + } + + if ghcrHostname.MatchString(hostname) { + // This is a GitHub Container Registry: ghcr.io + if token, found := os.LookupEnv("GITHUB_TOKEN"); found { + username = "github" + password = token + } + return + } + + return +} + +func getHostname(serverURL string) (hostname string, err error) { + var server *url.URL + server, err = url.Parse(defaultScheme + strings.TrimPrefix(serverURL, defaultScheme)) + if err != nil { + return + } + + hostname = server.Hostname() + + return +} + +func getEnvVariables(labels []string, offset int) (envUsername, envPassword string) { + if offset < 0 { + offset = 0 + } else if offset > len(labels) { + offset = len(labels) + } + + envHostname := strings.Join(labels[offset:], envSeparator) + envUsername = strings.Join([]string{envPrefix, envHostname, envUsernameSuffix}, envSeparator) + envPassword = strings.Join([]string{envPrefix, envHostname, envPasswordSuffix}, envSeparator) + + return +} + +func getEnvCredentials(hostname string) (username, password string, found bool) { + hostname = strings.ReplaceAll(hostname, "-", "_") + labels := strings.Split(hostname, ".") + + for i := 0; i <= len(labels); i++ { + envUsername, envPassword := getEnvVariables(labels, i) + + if username, found = os.LookupEnv(envUsername); found { + if password, found = os.LookupEnv(envPassword); found { + break + } + } + } + return +} + +func getEcrToken() (username, password string, err error) { + ctx := context.TODO() + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return + } + + if roleArn := getRoleArn(cfg.ConfigSources...); roleArn != "" { + stsSvc := sts.NewFromConfig(cfg) + creds := stscreds.NewAssumeRoleProvider(stsSvc, roleArn) + cfg.Credentials = aws.NewCredentialsCache(creds) + } + + client := ecr.NewFromConfig(cfg) + + output, err := client.GetAuthorizationToken(ctx, nil) + if err != nil { + return + } + for _, authData := range output.AuthorizationData { + // authData.AuthorizationToken is a base64-encoded username:password string, + // where the username is always expected to be "AWS". + var tokenBytes []byte + tokenBytes, err = base64.StdEncoding.DecodeString(*authData.AuthorizationToken) + if err != nil { + return + } + token := bytes.SplitN(tokenBytes, []byte{':'}, 2) + username, password = string(token[0]), string(token[1]) + } + return +} + +func getRoleArn(configSources ...interface{}) (roleARN string) { + for _, x := range configSources { + switch impl := x.(type) { + case config.EnvConfig: + if impl.RoleARN != "" { + return strings.TrimSpace(impl.RoleARN) + } + case config.SharedConfig: + if impl.RoleARN != "" { + return strings.TrimSpace(impl.RoleARN) + } + } + } + return +} diff --git a/vendor/github.com/isometry/docker-credential-env/main.go b/vendor/github.com/isometry/docker-credential-env/main.go new file mode 100644 index 0000000000..2e9d87f3a1 --- /dev/null +++ b/vendor/github.com/isometry/docker-credential-env/main.go @@ -0,0 +1,12 @@ +// docker-credentials-env is a Docker credentials helper that reads +// credentials from the process environment. + +package main + +import ( + docker_credentials "github.com/docker/docker-credential-helpers/credentials" +) + +func main() { + docker_credentials.Serve(&Env{}) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1f3ab8ce7a..9082a30b92 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -818,6 +818,9 @@ github.com/hashicorp/hcl/json/token # github.com/inconshreveable/mousetrap v1.1.0 ## explicit; go 1.18 github.com/inconshreveable/mousetrap +# github.com/isometry/docker-credential-env v1.3.0 +## explicit; go 1.22 +github.com/isometry/docker-credential-env # github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 ## explicit github.com/jbenet/go-context/io