diff --git a/GNUmakefile b/GNUmakefile index d6062e1f..b1653876 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -2,11 +2,28 @@ TEST?=$$(go list ./... |grep -v 'vendor') GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) PKG_NAME=internal/provider +# Values to install the provider locally for testing purposes +HOSTNAME=registry.terraform.io +NAMESPACE=kreuzwerker +NAME=docker +BINARY=terraform-provider-${NAME} +VERSION=9.9.9 +OS_ARCH=darwin_arm64 + +.PHONY: build test testacc vet fmt fmtcheck errcheck test-compile website-link-check website-lint website-lint-fix + default: build build: fmtcheck go install +local-build: + go build -o ${BINARY} + +local-install: local-build + mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} + mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} + setup: go install github.com/katbyte/terrafmt go install github.com/client9/misspell/cmd/misspell @@ -90,8 +107,6 @@ website-lint-fix: @docker run --rm -v $(PWD):/markdown 06kellyjac/markdownlint-cli --fix docs/ @terrafmt fmt ./docs --pattern '*.md' -.PHONY: build test testacc vet fmt fmtcheck errcheck test-compile website-link-check website-lint website-lint-fix - chlog-%: @echo "Generating CHANGELOG.md" git-chglog --next-tag $* -o CHANGELOG.md diff --git a/internal/provider/authentication_helpers.go b/internal/provider/authentication_helpers.go new file mode 100644 index 00000000..66bb2a58 --- /dev/null +++ b/internal/provider/authentication_helpers.go @@ -0,0 +1,50 @@ +package provider + +import ( + b64 "encoding/base64" + "log" + "regexp" + "strings" +) + +// ECR HTTP authentication needs Bearer token in the Authorization header +// This token is an JWT which was b64 encoded again +// Depending on the aws cli command, the returned token is different +// "aws ecr get-login-password" is a simply JWT, which needs to be prefixed with "AWS:" and then b64 encoded +// "aws ecr get-authorization-token" is the best case, everything is encoded properly +// in case someone passes an base64 decoded token from "aws ecr get-authorization-token" we need to b64 encode it again +func normalizeECRPasswordForHTTPUsage(password string) string { + if strings.HasPrefix(password, "ey") { + return b64.StdEncoding.EncodeToString([]byte("AWS:" + password)) + } else if strings.HasPrefix(password, "AWS:") { + return b64.StdEncoding.EncodeToString([]byte(password)) + } + return password +} + +// Docker operations need a JWT, so this function basically does the opposite as `normalizeECRPasswordForHTTPUsage` +// aws ecr get-authorization-token does not return a JWT, but a base64 encoded string which we need to decode +func normalizeECRPasswordForDockerCLIUsage(password string) string { + if strings.HasPrefix(password, "ey") { + return password + } + + if !strings.HasPrefix(password, "AWS:") { + decodedPassword, err := b64.StdEncoding.DecodeString(password) + if err != nil { + log.Fatalf("Error creating registry request: %s", err) + } + password = string(decodedPassword) + } + + return password[4:] +} + +func isECRRepositoryURL(url string) bool { + if url == "public.ecr.aws" { + return true + } + // Regexp is based on the ecr urls shown in https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html + var ecrRexp = regexp.MustCompile(`^.*?dkr\.ecr\..*?\.amazonaws\.com$`) + return ecrRexp.MatchString(url) +} diff --git a/internal/provider/authentication_helpers_test.go b/internal/provider/authentication_helpers_test.go new file mode 100644 index 00000000..8f51d18f --- /dev/null +++ b/internal/provider/authentication_helpers_test.go @@ -0,0 +1,19 @@ +package provider + +import "testing" + +func TestIsECRRepositoryURL(t *testing.T) { + + if !isECRRepositoryURL("2385929435838.dkr.ecr.eu-central-1.amazonaws.com") { + t.Fatalf("Expected true") + } + if !isECRRepositoryURL("39e39fmgvkgd.dkr.ecr.us-east-1.amazonaws.com") { + t.Fatalf("Expected true") + } + if isECRRepositoryURL("39e39fmgvkgd.dkr.ecrus-east-1.amazonaws.com") { + t.Fatalf("Expected false") + } + if !isECRRepositoryURL("public.ecr.aws") { + t.Fatalf("Expected true") + } +} diff --git a/internal/provider/data_source_docker_registry_image.go b/internal/provider/data_source_docker_registry_image.go index 7cf27953..6d58f5f8 100644 --- a/internal/provider/data_source_docker_registry_image.go +++ b/internal/provider/data_source_docker_registry_image.go @@ -102,10 +102,15 @@ func getImageDigest(registry, image, tag, username, password string, insecureSki } if username != "" { - if registry != "ghcr.io" { + if registry != "ghcr.io" && !isECRRepositoryURL(registry) && registry != "gcr.io" { req.SetBasicAuth(username, password) } else { - req.Header.Add("Authorization", "Bearer "+b64.StdEncoding.EncodeToString([]byte(password))) + if isECRRepositoryURL(registry) { + password = normalizeECRPasswordForHTTPUsage(password) + req.Header.Add("Authorization", "Basic "+password) + } else { + req.Header.Add("Authorization", "Bearer "+b64.StdEncoding.EncodeToString([]byte(password))) + } } } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7e14d48d..8853ef4c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -223,8 +223,12 @@ func providerSetToRegistryAuth(authList []interface{}) (*AuthConfigs, error) { // username/password or the given config file if username, ok := auth["username"]; ok && username.(string) != "" { log.Println("[DEBUG] Using username for registry auths:", username) + password := auth["password"].(string) + if isECRRepositoryURL(registryHostname) { + password = normalizeECRPasswordForDockerCLIUsage(password) + } authConfig.Username = auth["username"].(string) - authConfig.Password = auth["password"].(string) + authConfig.Password = password // Note: check for config_file_content first because config_file has a default which would be used // nevertheless config_file_content is set or not. The default has to be kept to check for the