diff --git a/config.example.yaml b/config.example.yaml index 4e21775..1df95b3 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -120,7 +120,6 @@ upstream: # - bearer: Authorization header with Bearer token # - basic: Authorization header with Basic auth (username:password) # - header: Custom header name and value - # - ecr: AWS ECR auto-refreshing token via the AWS SDK credential chain auth: # Example: npm with bearer token # "https://registry.npmjs.org": @@ -144,13 +143,6 @@ upstream: # header_name: "X-Auth-Token" # header_value: "${MAVEN_TOKEN}" - # Example: private AWS ECR registry (12h tokens auto-refreshed via - # ecr:GetAuthorizationToken; credentials come from the AWS SDK default - # chain, so IRSA / instance profiles / AWS_* env vars all work) - # "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": - # type: ecr - # region: eu-west-1 - # Gradle HttpBuildCache configuration gradle: build_cache: diff --git a/docs/configuration.md b/docs/configuration.md index 30f156d..3b8b935 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -216,22 +216,6 @@ upstream: header_value: "${MAVEN_TOKEN}" ``` -### AWS ECR - -Private ECR registries issue authorization tokens that expire after 12 hours. The `ecr` auth type calls `ecr:GetAuthorizationToken` on demand, caches the result, and refreshes it shortly before expiry, so no static credential appears in the config file: - -```yaml -upstream: - oci: - ecr: "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com" - auth: - "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": - type: ecr - region: eu-west-1 -``` - -AWS credentials are resolved by the SDK's default chain, which covers EKS IAM Roles for Service Accounts (IRSA), EC2/ECS instance profiles, `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` environment variables, and `~/.aws/credentials`. The IAM identity needs the `ecr:GetAuthorizationToken` action plus the usual `ecr:BatchGetImage` / `ecr:GetDownloadUrlForLayer` permissions on the target repositories. If `region` is omitted the SDK's default region resolution applies. - ### URL Matching Auth keys must be absolute URLs. Matching compares the scheme, host, effective port, and path-segment prefix, preventing credentials for `registry.example.com` from being sent to a lookalike host such as `registry.example.com.evil.test`. The longest matching scope wins, so you can configure different credentials for different paths: diff --git a/go.mod b/go.mod index 921a0f9..d95ee13 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,6 @@ toolchain go1.26.6 require ( github.com/BurntSushi/toml v1.6.0 github.com/CycloneDX/cyclonedx-go v0.11.0 - github.com/aws/aws-sdk-go-v2/config v1.32.38 - github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7 github.com/git-pkgs/archives v0.5.1 github.com/git-pkgs/cooldown v0.1.1 github.com/git-pkgs/enrichment v0.6.5 @@ -76,24 +74,25 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect github.com/ashanbrown/makezero/v2 v2.2.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.43.7 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.37 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.19 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 // indirect - github.com/aws/smithy-go v1.27.8 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect + github.com/aws/smithy-go v1.26.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect diff --git a/go.sum b/go.sum index 79d84c4..e2fedc7 100644 --- a/go.sum +++ b/go.sum @@ -117,46 +117,44 @@ github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTi github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM= github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM= github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= -github.com/aws/aws-sdk-go-v2 v1.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ= -github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= +github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= +github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94= -github.com/aws/aws-sdk-go-v2/config v1.32.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps= -github.com/aws/aws-sdk-go-v2/config v1.32.38/go.mod h1:dencYsOS1R7rBy8zehCvwBYzdxxL4Q/nRK7In03wjN8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.37 h1:FJ8Iz4/xISMB/rwLlgfWujfGDFWr0oneQgtA6KPcYLY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.37/go.mod h1:Q6pWOgVUp49x4g5QVi29wHofUoICnZ+Zq4jHbRN/7ec= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 h1:Nqo2jU1wz5rnBM9XQyXfVD1RP8txkbP3EDx8hR/hbCE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38/go.mod h1:PzJFHhjR2vWFKHe8HmY5Lxhvwyxnr5MERtk0nDxWNbk= +github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc= +github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM= +github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 h1:w5OoDiMN6x53ROmiIImGzmVcxXv2q1GXY+aKV4WAJYM= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3/go.mod h1:dAhgYp776bX3LuWvnSCFwQEjNs6fuFg7YXIy5PXcP3Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 h1:MBMg0zJ6i4TkAJ0dVFLKKn2cOkY6FkicmUDM67BRr6g= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38/go.mod h1:9MWuJbyiUyj6eA7W1/zm1zuePDPSB3g+xcgRQeMWsXc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 h1:lHm4jPf3k1Lz5ZWc+Vcn3MKVwym+26kWCba9FkJ4f0Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38/go.mod h1:Rn+P2XR+FbyZzjmWKjg/KUZNxmGfr5oZwh5jQiE+CzI= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 h1:vo4xvMRs/F6h1E52qsgLqCQgWIQXgIJUauG6rlZEh4U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39/go.mod h1:jB03R1ij/A+OE2e1dz6vgj076gd7vlYcfstAzj3HcnU= -github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7 h1:HgGvHNphJn16nP+JI6zzhYk1Y6f3BeDqbc7EYgpYKxU= -github.com/aws/aws-sdk-go-v2/service/ecr v1.60.7/go.mod h1:dCl1nYmyWmxJO6mBprZdwoFbIBEieBSifLNAt1xnSe4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 h1:H/5TI1jqaHsNoDQ60UwvPvJBg4GURkinXI3Qga29t2w= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38/go.mod h1:PTVFf+XH++7NJOky+RLBYQx0QA5NcaeEYFQ2fsi0nwo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM= github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298= github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 h1:YcczQ6zNH/ojIzD/ikDrO+RfW06wmdMp18d4NH5hXY4= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.7/go.mod h1:nl9RVnb9ulgAYzOkjLq1NyFxmWcnH2maCUEuOdESy98= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 h1:P+bMNiA93gyuYT3Oh+4dWtvrnGcu2bd9Uy5hRJM8BNo= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.7/go.mod h1:zy+397isDFLvleg9H18Zq2MGzMso7uKyJyzR7DWSgFk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 h1:WWkehGZ4nWtOKLMy0yi8+RqzzVqAGe60hGaxwF06JAw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7/go.mod h1:T8AI4SbQYm9ybcVmki2T3n7Qg1g3kfWoeQlNwNYOyO8= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 h1:yU/9y2r7s9kSUPbHXbpQTa4LA8kt+CMgpu1OBrhx8p4= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.7/go.mod h1:0lQTDEBArMevQXpxu443LVGjKxxEeSsSnrw9n8YiTMg= -github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY= -github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= +github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= +github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= diff --git a/internal/config/config.go b/internal/config/config.go index c906682..a3dfbc6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -430,7 +430,7 @@ func authURLPort(value *url.URL) string { // AuthConfig configures authentication for an upstream registry. type AuthConfig struct { - // Type is the authentication type: "bearer", "basic", "header", or "ecr". + // Type is the authentication type: "bearer", "basic", or "header". Type string `json:"type" yaml:"type"` // Token is used for bearer authentication. @@ -450,11 +450,6 @@ type AuthConfig struct { // HeaderValue is the custom header value (for type "header"). // Can reference environment variables with ${VAR_NAME} syntax. HeaderValue string `json:"header_value" yaml:"header_value"` - - // Region is the AWS region for ECR authentication (for type "ecr"). - // If empty, the AWS SDK default region chain is used (AWS_REGION, - // shared config, IMDS). - Region string `json:"region" yaml:"region"` } // Default returns a Config with sensible defaults. diff --git a/internal/server/ecr_auth.go b/internal/server/ecr_auth.go deleted file mode 100644 index 207475b..0000000 --- a/internal/server/ecr_auth.go +++ /dev/null @@ -1,126 +0,0 @@ -package server - -import ( - "context" - "errors" - "log/slog" - "sync" - "time" - - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "golang.org/x/sync/singleflight" -) - -const ( - ecrTokenTimeout = 10 * time.Second - ecrTokenSkew = 5 * time.Minute - ecrDefaultTokenLifetime = 12 * time.Hour -) - -var errEmptyECRToken = errors.New("empty ECR authorization token") - -// ecrTokens caches AWS ECR authorization tokens per region and refreshes them -// on demand when they expire. Tokens are obtained via the AWS SDK default -// credential chain, so IAM roles for service accounts, instance profiles, and -// environment credentials all work without extra configuration. -type ecrTokens struct { - logger *slog.Logger - - mu sync.Mutex - cache map[string]ecrToken - sf singleflight.Group - - // getToken fetches a fresh authorization token for the given region and - // returns the raw base64 "AWS:password" value plus its expiry. Overridable - // in tests. - getToken func(ctx context.Context, region string) (string, time.Time, error) -} - -type ecrToken struct { - value string - expiresAt time.Time -} - -func newECRTokens(logger *slog.Logger) *ecrTokens { - return &ecrTokens{ - logger: logger, - cache: make(map[string]ecrToken), - getToken: fetchECRToken, - } -} - -// header returns an Authorization header for the given region, fetching and -// caching a token on first use and after expiry. Concurrent misses for the -// same region share a single GetAuthorizationToken call. On failure it logs -// and returns empty strings so the request proceeds unauthenticated; the OCI -// transport then follows the Bearer challenge and surfaces the token-endpoint -// error, matching the behaviour of any other misconfigured upstream credential. -func (e *ecrTokens) header(region string) (name, value string) { - if tok, ok := e.cached(region); ok { - return "Authorization", tok.value - } - - v, err, _ := e.sf.Do(region, func() (any, error) { - if tok, ok := e.cached(region); ok { - return tok, nil - } - - ctx, cancel := context.WithTimeout(context.Background(), ecrTokenTimeout) - defer cancel() - - raw, expiresAt, err := e.getToken(ctx, region) - if err != nil { - e.logger.Error("fetching ECR authorization token", "region", region, "error", err) - return ecrToken{}, err - } - if raw == "" { - e.logger.Error("ECR authorization token response was empty", "region", region) - return ecrToken{}, errEmptyECRToken - } - - tok := ecrToken{value: "Basic " + raw, expiresAt: expiresAt.Add(-ecrTokenSkew)} - e.mu.Lock() - e.cache[region] = tok - e.mu.Unlock() - return tok, nil - }) - if err != nil { - return "", "" - } - - return "Authorization", v.(ecrToken).value -} - -func (e *ecrTokens) cached(region string) (ecrToken, bool) { - e.mu.Lock() - tok, ok := e.cache[region] - e.mu.Unlock() - return tok, ok && time.Now().Before(tok.expiresAt) -} - -func fetchECRToken(ctx context.Context, region string) (string, time.Time, error) { - var opts []func(*awsconfig.LoadOptions) error - if region != "" { - opts = append(opts, awsconfig.WithRegion(region)) - } - cfg, err := awsconfig.LoadDefaultConfig(ctx, opts...) - if err != nil { - return "", time.Time{}, err - } - - out, err := ecr.NewFromConfig(cfg).GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) - if err != nil { - return "", time.Time{}, err - } - if len(out.AuthorizationData) == 0 || out.AuthorizationData[0].AuthorizationToken == nil { - return "", time.Time{}, nil - } - - data := out.AuthorizationData[0] - expiresAt := time.Now().Add(ecrDefaultTokenLifetime) - if data.ExpiresAt != nil { - expiresAt = *data.ExpiresAt - } - return *data.AuthorizationToken, expiresAt, nil -} diff --git a/internal/server/ecr_auth_test.go b/internal/server/ecr_auth_test.go deleted file mode 100644 index da96026..0000000 --- a/internal/server/ecr_auth_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package server - -import ( - "context" - "errors" - "io" - "log/slog" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/git-pkgs/proxy/internal/config" -) - -func testECRTokens() *ecrTokens { - return newECRTokens(slog.New(slog.NewTextHandler(io.Discard, nil))) -} - -func TestECRTokensCachesUntilExpiry(t *testing.T) { - e := testECRTokens() - calls := 0 - e.getToken = func(_ context.Context, region string) (string, time.Time, error) { - calls++ - if region != "eu-west-1" { - t.Errorf("region = %q, want eu-west-1", region) - } - return "QVdTOnNlY3JldA==", time.Now().Add(12 * time.Hour), nil - } - - name, value := e.header("eu-west-1") - if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" { - t.Fatalf("header() = %q, %q", name, value) - } - - e.header("eu-west-1") - e.header("eu-west-1") - if calls != 1 { - t.Fatalf("getToken called %d times, want 1", calls) - } -} - -func TestECRTokensRefreshesAfterExpiry(t *testing.T) { - e := testECRTokens() - calls := 0 - e.getToken = func(_ context.Context, _ string) (string, time.Time, error) { - calls++ - return "dG9rZW4=", time.Now().Add(time.Minute), nil - } - - e.header("us-east-1") - e.header("us-east-1") - if calls != 2 { - t.Fatalf("getToken called %d times, want 2 (token within skew window)", calls) - } -} - -func TestECRTokensPerRegion(t *testing.T) { - e := testECRTokens() - seen := map[string]int{} - e.getToken = func(_ context.Context, region string) (string, time.Time, error) { - seen[region]++ - return region + "-token", time.Now().Add(time.Hour), nil - } - - e.header("eu-west-1") - e.header("us-east-1") - e.header("eu-west-1") - - if seen["eu-west-1"] != 1 || seen["us-east-1"] != 1 { - t.Fatalf("per-region calls = %v, want one each", seen) - } -} - -func TestECRTokensConcurrentMissesShareOneFetch(t *testing.T) { - e := testECRTokens() - var calls atomic.Int32 - release := make(chan struct{}) - e.getToken = func(_ context.Context, _ string) (string, time.Time, error) { - calls.Add(1) - <-release - return "dG9rZW4=", time.Now().Add(time.Hour), nil - } - - const n = 10 - var wg sync.WaitGroup - wg.Add(n) - for range n { - go func() { - defer wg.Done() - name, value := e.header("eu-west-1") - if name != "Authorization" || value != "Basic dG9rZW4=" { - t.Errorf("header() = %q, %q", name, value) - } - }() - } - - close(release) - wg.Wait() - - if got := calls.Load(); got != 1 { - t.Fatalf("getToken called %d times, want 1", got) - } -} - -func TestECRTokensErrorReturnsNoAuth(t *testing.T) { - e := testECRTokens() - e.getToken = func(_ context.Context, _ string) (string, time.Time, error) { - return "", time.Time{}, errors.New("no credentials") - } - - name, value := e.header("eu-west-1") - if name != "" || value != "" { - t.Fatalf("header() = %q, %q; want empty on error", name, value) - } -} - -func TestAuthForURLRoutesECRType(t *testing.T) { - e := testECRTokens() - e.getToken = func(_ context.Context, region string) (string, time.Time, error) { - if region != "eu-west-1" { - t.Errorf("region = %q, want eu-west-1", region) - } - return "QVdTOnNlY3JldA==", time.Now().Add(time.Hour), nil - } - - s := &Server{ - ecr: e, - cfg: &config.Config{ - Upstream: config.UpstreamConfig{ - Auth: map[string]config.AuthConfig{ - "https://123456789012.dkr.ecr.eu-west-1.amazonaws.com": { - Type: "ecr", - Region: "eu-west-1", - }, - "https://ghcr.io": { - Type: "bearer", - Token: "ghcr-token", - }, - }, - }, - }, - } - - name, value := s.authForURL("https://123456789012.dkr.ecr.eu-west-1.amazonaws.com/v2/my/repo/manifests/latest") - if name != "Authorization" || value != "Basic QVdTOnNlY3JldA==" { - t.Fatalf("ecr authForURL() = %q, %q", name, value) - } - - name, value = s.authForURL("https://ghcr.io/v2/owner/repo/blobs/sha256:abc") - if name != "Authorization" || value != "Bearer ghcr-token" { - t.Fatalf("bearer authForURL() = %q, %q", name, value) - } - - name, value = s.authForURL("https://registry-1.docker.io/v2/") - if name != "" || value != "" { - t.Fatalf("unmatched authForURL() = %q, %q; want empty", name, value) - } -} diff --git a/internal/server/server.go b/internal/server/server.go index cd18be9..bb964e8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -98,7 +98,6 @@ type Server struct { cancel context.CancelFunc healthCache *healthCache accessLog *accesslog.Logger - ecr *ecrTokens } // New creates a new Server with the given configuration. @@ -175,7 +174,6 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, templates: &Templates{}, healthCache: hc, accessLog: activityLog, - ecr: newECRTokens(logger), } closeAccessLog = false return server, nil @@ -427,9 +425,6 @@ func (s *Server) authForURL(url string) (headerName, headerValue string) { if auth == nil { return "", "" } - if strings.EqualFold(auth.Type, "ecr") { - return s.ecr.header(auth.Region) - } return auth.Header() }