diff --git a/auth/aws/implementation_test.go b/auth/aws/implementation_test.go index e5e6c5741..1a2f94ea0 100644 --- a/auth/aws/implementation_test.go +++ b/auth/aws/implementation_test.go @@ -18,6 +18,7 @@ package aws_test import ( "context" + "encoding/base64" "net/http" "net/url" "testing" @@ -41,9 +42,13 @@ type mockImplementation struct { argSTSEndpoint string argProxyURL *url.URL argCredsProvider aws.CredentialsProvider + + returnCreds aws.Credentials + returnUsername string + returnPassword string } -type mockCredentialsProvider struct{} +type mockCredentialsProvider struct{ aws.Credentials } func (m *mockImplementation) LoadDefaultConfig(ctx context.Context, optFns ...func(*config.LoadOptions) error) (aws.Config, error) { m.t.Helper() @@ -62,7 +67,7 @@ func (m *mockImplementation) LoadDefaultConfig(ctx context.Context, optFns ...fu proxyURL, err := o.HTTPClient.(*http.Client).Transport.(*http.Transport).Proxy(nil) g.Expect(err).NotTo(HaveOccurred()) g.Expect(proxyURL).To(Equal(m.argProxyURL)) - return aws.Config{Credentials: mockCredentialsProvider{}}, nil + return aws.Config{Credentials: &mockCredentialsProvider{m.returnCreds}}, nil } func (m *mockImplementation) AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, options sts.Options) (*sts.AssumeRoleWithWebIdentityOutput, error) { @@ -87,7 +92,12 @@ func (m *mockImplementation) AssumeRoleWithWebIdentity(ctx context.Context, para g.Expect(err).NotTo(HaveOccurred()) g.Expect(proxyURL).To(Equal(m.argProxyURL)) return &sts.AssumeRoleWithWebIdentityOutput{ - Credentials: &ststypes.Credentials{}, + Credentials: &ststypes.Credentials{ + AccessKeyId: aws.String(m.returnCreds.AccessKeyID), + SecretAccessKey: aws.String(m.returnCreds.SecretAccessKey), + SessionToken: aws.String(m.returnCreds.SessionToken), + Expiration: aws.Time(m.returnCreds.Expires), + }, }, nil } @@ -106,11 +116,11 @@ func (m *mockImplementation) GetAuthorizationToken(ctx context.Context, cfg aws. g.Expect(proxyURL).To(Equal(m.argProxyURL)) return &ecr.GetAuthorizationTokenOutput{ AuthorizationData: []ecrtypes.AuthorizationData{{ - AuthorizationToken: aws.String("dXNlcm5hbWU6cGFzc3dvcmQ="), + AuthorizationToken: aws.String(base64.StdEncoding.EncodeToString([]byte(m.returnUsername + ":" + m.returnPassword))), }}, }, nil } -func (mockCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{}, nil +func (m *mockCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { + return m.Credentials, nil } diff --git a/auth/aws/options.go b/auth/aws/options.go index 09ff65f7e..a2dd7de2a 100644 --- a/auth/aws/options.go +++ b/auth/aws/options.go @@ -18,22 +18,11 @@ package aws import ( "fmt" - "os" "regexp" corev1 "k8s.io/api/core/v1" ) -func getSTSRegion() (string, error) { - // The AWS_REGION is usually automatically set in EKS clusters. - // If not, users can set it manually (e.g. Fargate). - region := os.Getenv("AWS_REGION") - if region == "" { - return "", fmt.Errorf("AWS_REGION environment variable is not set in the Flux controller") - } - return region, nil -} - const stsEndpointPattern = `^https://(.+\.)?sts(-fips)?(\.[^.]+)?(\.vpce)?\.amazonaws\.com$` var stsEndpointRegex = regexp.MustCompile(stsEndpointPattern) @@ -61,10 +50,11 @@ const roleARNPattern = `^arn:aws:iam::[0-9]{1,30}:role/.{1,200}$` var roleARNRegex = regexp.MustCompile(roleARNPattern) func getRoleARN(serviceAccount corev1.ServiceAccount) (string, error) { - arn := serviceAccount.Annotations["eks.amazonaws.com/role-arn"] + const key = "eks.amazonaws.com/role-arn" + arn := serviceAccount.Annotations[key] if !roleARNRegex.MatchString(arn) { - return "", fmt.Errorf("invalid AWS role ARN: '%s'. must match %s", - arn, roleARNPattern) + return "", fmt.Errorf("invalid %s annotation: '%s'. must match %s", + key, arn, roleARNPattern) } return arn, nil } @@ -74,18 +64,3 @@ func getRoleSessionName(serviceAccount corev1.ServiceAccount, region string) str namespace := serviceAccount.Namespace return fmt.Sprintf("%s.%s.%s.fluxcd.io", name, namespace, region) } - -// This regex is sourced from the AWS ECR Credential Helper (https://github.com/awslabs/amazon-ecr-credential-helper). -// It covers both public AWS partitions like amazonaws.com, China partitions like amazonaws.com.cn, and non-public partitions. -var registryPartRe = regexp.MustCompile(`([0-9+]*).dkr.ecr(?:-fips)?\.([^/.]*)\.(amazonaws\.com[.cn]*|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)`) - -// ParseRegistry returns the AWS account ID and region and `true` if -// the image registry/repository is hosted in AWS's Elastic Container Registry, -// otherwise empty strings and `false`. -func ParseRegistry(registry string) (accountId, awsEcrRegion string, ok bool) { - registryParts := registryPartRe.FindAllStringSubmatch(registry, -1) - if len(registryParts) < 1 || len(registryParts[0]) < 3 { - return "", "", false - } - return registryParts[0][1], registryParts[0][2], true -} diff --git a/auth/aws/options_test.go b/auth/aws/options_test.go index 67f8fa8b7..bc0bb6114 100644 --- a/auth/aws/options_test.go +++ b/auth/aws/options_test.go @@ -152,93 +152,3 @@ func TestValidateSTSEndpoint(t *testing.T) { }) } } - -func TestParseRegistry(t *testing.T) { - tests := []struct { - registry string - wantAccountID string - wantRegion string - wantOK bool - }{ - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "https://012345678901.dkr.ecr.us-east-1.amazonaws.com/v2/part/part", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.cn-north-1.amazonaws.com.cn/foo", - wantAccountID: "012345678901", - wantRegion: "cn-north-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr-fips.us-gov-west-1.amazonaws.com", - wantAccountID: "012345678901", - wantRegion: "us-gov-west-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-secret-region.sc2s.sgov.gov", - wantAccountID: "012345678901", - wantRegion: "us-secret-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr-fips.us-ts-region.c2s.ic.gov", - wantAccountID: "012345678901", - wantRegion: "us-ts-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.uk-region.cloud.adc-e.uk", - wantAccountID: "012345678901", - wantRegion: "uk-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-ts-region.csp.hci.ic.gov", - wantAccountID: "012345678901", - wantRegion: "us-ts-region", - wantOK: true, - }, - // TODO: Fix: this invalid registry is allowed by the regex. - // { - // registry: ".dkr.ecr.error.amazonaws.com", - // wantOK: false, - // }, - { - registry: "gcr.io/foo/bar:baz", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.registry, func(t *testing.T) { - g := NewWithT(t) - - accId, region, ok := aws.ParseRegistry(tt.registry) - g.Expect(ok).To(Equal(tt.wantOK), "unexpected OK") - g.Expect(accId).To(Equal(tt.wantAccountID), "unexpected account IDs") - g.Expect(region).To(Equal(tt.wantRegion), "unexpected regions") - }) - } -} diff --git a/auth/aws/provider.go b/auth/aws/provider.go index c3a154b72..a712d68bf 100644 --- a/auth/aws/provider.go +++ b/auth/aws/provider.go @@ -19,14 +19,18 @@ package aws import ( "context" "encoding/base64" + "errors" "fmt" "net/http" + "os" + "regexp" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/google/go-containerregistry/pkg/authn" corev1 "k8s.io/api/core/v1" "github.com/fluxcd/pkg/auth" @@ -43,16 +47,43 @@ func (Provider) GetName() string { return ProviderName } -// NewDefaultToken implements auth.Provider. -func (p Provider) NewDefaultToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { +// NewControllerToken implements auth.Provider. +func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { var o auth.Options o.Apply(opts...) var awsOpts []func(*config.LoadOptions) error - stsRegion, err := getSTSRegion() - if err != nil { - return nil, err + stsRegion := o.STSRegion + if stsRegion == "" { + // A region is required. Try to get it somewhere else. + switch { + // For artifact repositories we can take advantage of the fact that ECR + // repositories have a region we can use. + // **Important**: This code path is required for supporting EKS Node Identity + // for artifact repositories! This is because the environment variable + // AWS_REGION is set automatically for IRSA or EKS Pod Identity, but + // not for Node Identity. + // We strive to support Node Identity for container registry-based APIs because + // EKS users also use Node Identity for container images, so this allows a + // simpler/consistent user experience. + case o.ArtifactRepository != "": + // We can safely ignore the error here, auth.GetToken() has already called + // ParseArtifactRepository() and validated the repository at this point. + ecrRegion, _ := p.ParseArtifactRepository(o.ArtifactRepository) + stsRegion = ecrRegion + // EKS sets this environment variable automatically if the controller pod is + // properly configured with IRSA or EKS Pod Identity, so we can rely on this + // and communicate this to users since this is controller-level configuration. + default: + stsRegion = os.Getenv("AWS_REGION") + if stsRegion == "" { + return nil, errors.New("AWS_REGION environment variable is not set in the Flux controller. " + + "if you have properly configured IAM Roles for Service Accounts (IRSA) or EKS Pod Identity, " + + "please delete/replace the controller pod so the EKS admission controllers can inject this " + + "environment variable, or set it manually if the cluster is not EKS") + } + } } awsOpts = append(awsOpts, config.WithRegion(stsRegion)) @@ -100,9 +131,27 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin var o auth.Options o.Apply(opts...) - stsRegion, err := getSTSRegion() - if err != nil { - return nil, err + stsRegion := o.STSRegion + if stsRegion == "" { + // A region is required. Try to get it somewhere else. + switch { + // For artifact repositories we can take advantage of the fact that ECR + // repositories have a region we can use. + case o.ArtifactRepository != "": + // We can safely ignore the error here, auth.GetToken() has already called + // ParseArtifactRepository() and validated the repository at this point. + ecrRegion, _ := p.ParseArtifactRepository(o.ArtifactRepository) + stsRegion = ecrRegion + // In this case we can't rely on IRSA or EKS Pod Identity for the controller + // pod because this is object-level configuration, so we show a different + // error message. + // In this error message we assume an API that has a region field, e.g. the + // Bucket API. APIs that can extract the region from the ARN (e.g. KMS) will + // never reach this code path. + default: + return nil, errors.New("an AWS region is required for authenticating with a service account. " + + "please configure one in the object spec") + } } roleARN, err := getRoleARN(serviceAccount) @@ -151,31 +200,41 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin return token, nil } -// GetArtifactCacheKey implements auth.Provider. -func (Provider) GetArtifactCacheKey(artifactRepository string) string { - if _, ecrRegion, ok := ParseRegistry(artifactRepository); ok { - return ecrRegion +// This regex is sourced from the AWS ECR Credential Helper (https://github.com/awslabs/amazon-ecr-credential-helper). +// It covers both public AWS partitions like amazonaws.com, China partitions like amazonaws.com.cn, and non-public partitions. +const registryPattern = `([0-9+]*).dkr.ecr(?:-fips)?\.([^/.]*)\.(amazonaws\.com[.cn]*|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)` + +var registryRegex = regexp.MustCompile(registryPattern) + +// ParseArtifactRepository implements auth.Provider. +// ParseArtifactRepository returns the ECR region. +func (Provider) ParseArtifactRepository(artifactRepository string) (string, error) { + registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository) + if err != nil { + return "", err + } + + parts := registryRegex.FindAllStringSubmatch(registry, -1) + if len(parts) < 1 || len(parts[0]) < 3 { + return "", fmt.Errorf("invalid AWS registry: '%s'. must match %s", + registry, registryPattern) } - return "" + + // For issuing AWS registry credentials the ECR region is required. + ecrRegion := parts[0][2] + return ecrRegion, nil } -// NewArtifactRegistryToken implements auth.Provider. -func (p Provider) NewArtifactRegistryToken(ctx context.Context, artifactRepository string, - accessToken auth.Token, opts ...auth.Option) (auth.Token, error) { +// NewArtifactRegistryCredentials implements auth.Provider. +func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, ecrRegion string, + accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) { var o auth.Options o.Apply(opts...) - _, ecrRegion, ok := ParseRegistry(artifactRepository) - if !ok { - return nil, fmt.Errorf("invalid ecr repository: '%s'", artifactRepository) - } - - credsProvider := accessToken.(*Token).CredentialsProvider() - conf := aws.Config{ Region: ecrRegion, - Credentials: credsProvider, + Credentials: accessToken.(*Token).CredentialsProvider(), } if hc := o.GetHTTPClient(); hc != nil { @@ -209,8 +268,10 @@ func (p Provider) NewArtifactRegistryToken(ctx context.Context, artifactReposito expiresAt = *exp } return &auth.ArtifactRegistryCredentials{ - Username: s[0], - Password: s[1], + Authenticator: authn.FromConfig(authn.AuthConfig{ + Username: s[0], + Password: s[1], + }), ExpiresAt: expiresAt, }, nil } diff --git a/auth/aws/provider_test.go b/auth/aws/provider_test.go index 735cff970..7ef2c7056 100644 --- a/auth/aws/provider_test.go +++ b/auth/aws/provider_test.go @@ -20,10 +20,12 @@ import ( "context" "net/url" "testing" + "time" awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/sts/types" + "github.com/google/go-containerregistry/pkg/authn" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,20 +34,21 @@ import ( "github.com/fluxcd/pkg/auth/aws" ) -func TestProvider_NewDefaultToken_Options(t *testing.T) { - t.Setenv("AWS_REGION", "us-east-1") - +func TestProvider_NewControllerToken(t *testing.T) { impl := &mockImplementation{ t: t, argRegion: "us-east-1", argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, argSTSEndpoint: "https://sts.amazonaws.com", + returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id"}, } for _, tt := range []struct { - name string - stsEndpoint string - err string + name string + stsEndpoint string + artifactRepository string + skipSTSRegion bool + err string }{ { name: "valid", @@ -56,21 +59,46 @@ func TestProvider_NewDefaultToken_Options(t *testing.T) { stsEndpoint: "https://something.amazonaws.com", err: `invalid STS endpoint: 'https://something.amazonaws.com'. must match ^https://(.+\.)?sts(-fips)?(\.[^.]+)?(\.vpce)?\.amazonaws\.com$`, }, + { + name: "missing region", + stsEndpoint: "https://sts.amazonaws.com", + skipSTSRegion: true, + err: "AWS_REGION environment variable is not set in the Flux controller. " + + "if you have properly configured IAM Roles for Service Accounts (IRSA) or EKS Pod Identity, " + + "please delete/replace the controller pod so the EKS admission controllers can inject this " + + "environment variable, or set it manually if the cluster is not EKS", + }, + { + name: "missing region but can extract from artifact repository", + stsEndpoint: "https://sts.amazonaws.com", + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", + skipSTSRegion: true, + }, } { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + if !tt.skipSTSRegion { + t.Setenv("AWS_REGION", "us-east-1") + } + opts := []auth.Option{ auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), auth.WithSTSEndpoint(tt.stsEndpoint), + auth.WithArtifactRepository(tt.artifactRepository), } provider := aws.Provider{Implementation: impl} - token, err := provider.NewDefaultToken(context.Background(), opts...) + token, err := provider.NewControllerToken(context.Background(), opts...) if tt.err == "" { g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + g.Expect(token).To(Equal(&aws.Token{Credentials: types.Credentials{ + AccessKeyId: awssdk.String("access-key-id"), + SecretAccessKey: awssdk.String(""), + SessionToken: awssdk.String(""), + Expiration: awssdk.Time(time.Time{}), + }})) } else { g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(Equal(tt.err)) @@ -80,9 +108,7 @@ func TestProvider_NewDefaultToken_Options(t *testing.T) { } } -func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { - t.Setenv("AWS_REGION", "us-east-1") - +func TestProvider_NewTokenForServiceAccount(t *testing.T) { impl := &mockImplementation{ t: t, argRegion: "us-east-1", @@ -91,40 +117,70 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { argOIDCToken: "oidc-token", argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, argSTSEndpoint: "https://sts.amazonaws.com", - } - - oidcToken := "oidc-token" - serviceAccount := corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-sa", - Namespace: "test-ns", - Annotations: map[string]string{ - "eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role", - }, - }, + returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id"}, } for _, tt := range []struct { - name string - stsEndpoint string - err string + name string + annotations map[string]string + stsEndpoint string + artifactRepository string + skipSTSRegion bool + err string }{ { name: "valid", + annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"}, stsEndpoint: "https://sts.amazonaws.com", }, { name: "invalid sts endpoint", + annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"}, stsEndpoint: "https://something.amazonaws.com", err: `invalid STS endpoint: 'https://something.amazonaws.com'. must match ^https://(.+\.)?sts(-fips)?(\.[^.]+)?(\.vpce)?\.amazonaws\.com$`, }, + { + name: "missing region", + annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"}, + stsEndpoint: "https://sts.amazonaws.com", + skipSTSRegion: true, + err: "an AWS region is required for authenticating with a service account. " + + "please configure one in the object spec", + }, + { + name: "missing region but can extract from artifact repository", + annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"}, + stsEndpoint: "https://sts.amazonaws.com", + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", + skipSTSRegion: true, + }, + { + name: "invalid role ARN", + annotations: map[string]string{"eks.amazonaws.com/role-arn": "foobar"}, + stsEndpoint: "https://sts.amazonaws.com", + err: "invalid eks.amazonaws.com/role-arn annotation: 'foobar'. must match ^arn:aws:iam::[0-9]{1,30}:role/.{1,200}$", + }, } { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + oidcToken := "oidc-token" + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + Namespace: "test-ns", + Annotations: tt.annotations, + }, + } + opts := []auth.Option{ auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), auth.WithSTSEndpoint(tt.stsEndpoint), + auth.WithArtifactRepository(tt.artifactRepository), + } + + if !tt.skipSTSRegion { + opts = append(opts, auth.WithSTSRegion("us-east-1")) } provider := aws.Provider{Implementation: impl} @@ -132,7 +188,12 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { if tt.err == "" { g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + g.Expect(token).To(Equal(&aws.Token{Credentials: types.Credentials{ + AccessKeyId: awssdk.String("access-key-id"), + SecretAccessKey: awssdk.String(""), + SessionToken: awssdk.String(""), + Expiration: awssdk.Time(time.Time{}), + }})) } else { g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(Equal(tt.err)) @@ -142,7 +203,7 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { } } -func TestProvider_NewArtifactRegistryToken_Options(t *testing.T) { +func TestProvider_NewArtifactRegistryCredentials(t *testing.T) { g := NewWithT(t) impl := &mockImplementation{ @@ -150,9 +211,11 @@ func TestProvider_NewArtifactRegistryToken_Options(t *testing.T) { argRegion: "us-east-1", argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, argCredsProvider: credentials.NewStaticCredentialsProvider("access-key-id", "secret-access-key", "session-token"), + returnUsername: "username", + returnPassword: "password", } - artifactRepository := "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo" + ecrRegion := "us-east-1" accessToken := &aws.Token{ Credentials: types.Credentials{ AccessKeyId: awssdk.String("access-key-id"), @@ -165,7 +228,86 @@ func TestProvider_NewArtifactRegistryToken_Options(t *testing.T) { } provider := aws.Provider{Implementation: impl} - token, err := provider.NewArtifactRegistryToken(context.Background(), artifactRepository, accessToken, opts...) + creds, err := provider.NewArtifactRegistryCredentials( + context.Background(), ecrRegion, accessToken, opts...) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + g.Expect(creds).To(Equal(&auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{ + Username: "username", + Password: "password", + }), + })) +} + +func TestProvider_ParseArtifactRepository(t *testing.T) { + tests := []struct { + artifactRepository string + expectedRegion string + expectValid bool + }{ + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", + expectedRegion: "us-east-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo", + expectedRegion: "us-east-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com", + expectedRegion: "us-east-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/v2/part/part", + expectedRegion: "us-east-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.cn-north-1.amazonaws.com.cn/foo", + expectedRegion: "cn-north-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr-fips.us-gov-west-1.amazonaws.com", + expectedRegion: "us-gov-west-1", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-secret-region.sc2s.sgov.gov", + expectedRegion: "us-secret-region", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr-fips.us-ts-region.c2s.ic.gov", + expectedRegion: "us-ts-region", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.uk-region.cloud.adc-e.uk", + expectedRegion: "uk-region", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-ts-region.csp.hci.ic.gov", + expectedRegion: "us-ts-region", + expectValid: true, + }, + { + artifactRepository: "gcr.io/foo/bar:baz", + expectValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.artifactRepository, func(t *testing.T) { + g := NewWithT(t) + + region, err := aws.Provider{}.ParseArtifactRepository(tt.artifactRepository) + g.Expect(err == nil).To(Equal(tt.expectValid)) + g.Expect(region).To(Equal(tt.expectedRegion)) + }) + } } diff --git a/auth/azure/implementation_test.go b/auth/azure/implementation_test.go index 5258741b6..c2a65377c 100644 --- a/auth/azure/implementation_test.go +++ b/auth/azure/implementation_test.go @@ -18,6 +18,7 @@ package azure_test import ( "context" + "io" "net/http" "net/url" "testing" @@ -34,16 +35,21 @@ type mockImplementation struct { argTenantID string argClientID string argOIDCToken string + argURL string + argBody string argProxyURL *url.URL argScopes []string - returnResp *http.Response + returnResp *http.Response + returnToken string } type mockTokenCredential struct { t *testing.T argScopes []string + + returnToken string } func (m *mockImplementation) NewDefaultAzureCredential(options azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) { @@ -57,7 +63,7 @@ func (m *mockImplementation) NewDefaultAzureCredential(options azidentity.Defaul proxyURL, err := options.Transport.(*http.Client).Transport.(*http.Transport).Proxy(nil) g.Expect(err).NotTo(HaveOccurred()) g.Expect(proxyURL).To(Equal(m.argProxyURL)) - return &mockTokenCredential{t: m.t, argScopes: m.argScopes}, nil + return &mockTokenCredential{t: m.t, argScopes: m.argScopes, returnToken: m.returnToken}, nil } func (m *mockImplementation) NewClientAssertionCredential(tenantID string, clientID string, getAssertion func(context.Context) (string, error), options *azidentity.ClientAssertionCredentialOptions) (azcore.TokenCredential, error) { @@ -78,12 +84,20 @@ func (m *mockImplementation) NewClientAssertionCredential(tenantID string, clien proxyURL, err := options.Transport.(*http.Client).Transport.(*http.Transport).Proxy(nil) g.Expect(err).NotTo(HaveOccurred()) g.Expect(proxyURL).To(Equal(m.argProxyURL)) - return &mockTokenCredential{t: m.t, argScopes: m.argScopes}, nil + return &mockTokenCredential{t: m.t, argScopes: m.argScopes, returnToken: m.returnToken}, nil } func (m *mockImplementation) SendRequest(req *http.Request, client *http.Client) (*http.Response, error) { m.t.Helper() g := NewWithT(m.t) + g.Expect(req).NotTo(BeNil()) + g.Expect(req.Method).To(Equal(http.MethodPost)) + g.Expect(req.URL).NotTo(BeNil()) + g.Expect(req.URL.String()).To(Equal(m.argURL)) + g.Expect(req.Body).NotTo(BeNil()) + b, err := io.ReadAll(req.Body) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(string(b)).To(Equal(m.argBody)) g.Expect(client).NotTo(BeNil()) g.Expect(client.Transport).NotTo(BeNil()) g.Expect(client.Transport.(*http.Transport)).NotTo(BeNil()) @@ -98,5 +112,5 @@ func (m *mockTokenCredential) GetToken(ctx context.Context, options policy.Token m.t.Helper() g := NewWithT(m.t) g.Expect(options.Scopes).To(Equal(m.argScopes)) - return azcore.AccessToken{}, nil + return azcore.AccessToken{Token: m.returnToken}, nil } diff --git a/auth/azure/options.go b/auth/azure/options.go index f5b29f504..246fb7eff 100644 --- a/auth/azure/options.go +++ b/auth/azure/options.go @@ -18,7 +18,6 @@ package azure import ( "fmt" - "os" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" @@ -40,42 +39,49 @@ func getIdentity(serviceAccount corev1.ServiceAccount) (string, error) { } func getTenantID(serviceAccount corev1.ServiceAccount) (string, error) { - if tenantID, ok := serviceAccount.Annotations["azure.workload.identity/tenant-id"]; ok { + const key = "azure.workload.identity/tenant-id" + if tenantID, ok := serviceAccount.Annotations[key]; ok { return tenantID, nil } - if tenantID := os.Getenv("AZURE_TENANT_ID"); tenantID != "" { - return tenantID, nil - } - return "", fmt.Errorf("azure tenant ID not found in the service account annotations nor in the environment variable AZURE_TENANT_ID") + return "", fmt.Errorf("azure tenant ID is not set in the service account annotation %s", key) } func getClientID(serviceAccount corev1.ServiceAccount) (string, error) { - if clientID, ok := serviceAccount.Annotations["azure.workload.identity/client-id"]; ok { + const key = "azure.workload.identity/client-id" + if clientID, ok := serviceAccount.Annotations[key]; ok { return clientID, nil } - return "", fmt.Errorf("azure client ID not found in the service account annotations") + return "", fmt.Errorf("azure client ID is not set in the service account annotation %s", key) } func getScopes(o *auth.Options) []string { - if ar := o.ArtifactRepository; ar != "" { - return []string{getACRScope(ar)} + if acrScope := getACRScope(o.ArtifactRepository); acrScope != "" { + return []string{acrScope} } return o.Scopes } func getACRScope(artifactRepository string) string { + if artifactRepository == "" { + return "" + } + + registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository) + if err != nil { + // it's ok to swallow the error here, it should never happen + // because GetRegistryFromArtifactRepository() is already called + // earlier by auth.GetToken() and the error is handled there. + return "" + } + var conf *cloud.Configuration switch { - case strings.HasSuffix(artifactRepository, ".azurecr.cn"): + case strings.HasSuffix(registry, ".azurecr.cn"): conf = &cloud.AzureChina - case strings.HasSuffix(artifactRepository, ".azurecr.us"): + case strings.HasSuffix(registry, ".azurecr.us"): conf = &cloud.AzureGovernment default: conf = &cloud.AzurePublic } return conf.Services[cloud.ResourceManager].Endpoint + "/" + ".default" } - -func getACRHost(artifactRepository string) string { - return strings.SplitN(artifactRepository, "/", 2)[0] -} diff --git a/auth/azure/provider.go b/auth/azure/provider.go index fbe116581..4869ddc9b 100644 --- a/auth/azure/provider.go +++ b/auth/azure/provider.go @@ -23,11 +23,13 @@ import ( "net/http" "net/url" "path" + "regexp" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/golang-jwt/jwt/v5" + "github.com/google/go-containerregistry/pkg/authn" corev1 "k8s.io/api/core/v1" "github.com/fluxcd/pkg/auth" @@ -44,8 +46,8 @@ func (Provider) GetName() string { return ProviderName } -// NewDefaultToken implements auth.Provider. -func (p Provider) NewDefaultToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { +// NewControllerToken implements auth.Provider. +func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { var o auth.Options o.Apply(opts...) @@ -116,14 +118,32 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin return &Token{token}, nil } -// GetArtifactCacheKey implements auth.Provider. -func (Provider) GetArtifactCacheKey(artifactRepository string) string { - return getACRHost(artifactRepository) +// https://github.com/kubernetes/kubernetes/blob/v1.23.1/pkg/credentialprovider/azure/azure_credentials.go#L55 +const registryPattern = `^.+\.(azurecr\.io|azurecr\.cn|azurecr\.de|azurecr\.us)$` + +var registryRegex = regexp.MustCompile(registryPattern) + +// ParseArtifactRepository implements auth.Provider. +// ParseArtifactRepository returns the ACR registry URL. +func (Provider) ParseArtifactRepository(artifactRepository string) (string, error) { + registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository) + if err != nil { + return "", err + } + + if !registryRegex.MatchString(registry) { + return "", fmt.Errorf("invalid Azure registry: '%s'. must match %s", + registry, registryPattern) + } + + // For issuing Azure registry credentials the registry URL is required. + registryURL := fmt.Sprintf("https://%s", registry) + return registryURL, nil } -// NewArtifactRegistryToken implements auth.Provider. -func (p Provider) NewArtifactRegistryToken(ctx context.Context, artifactRepository string, - accessToken auth.Token, opts ...auth.Option) (auth.Token, error) { +// NewArtifactRegistryCredentials implements auth.Provider. +func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryURL string, + accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) { t := accessToken.(*Token) @@ -131,10 +151,6 @@ func (p Provider) NewArtifactRegistryToken(ctx context.Context, artifactReposito o.Apply(opts...) // Build request. - registryURL := artifactRepository - if !strings.HasPrefix(artifactRepository, "http") { - registryURL = fmt.Sprintf("https://%s", getACRHost(artifactRepository)) - } exchangeURL, err := url.Parse(registryURL) if err != nil { return nil, err @@ -182,9 +198,11 @@ func (p Provider) NewArtifactRegistryToken(ctx context.Context, artifactReposito } return &auth.ArtifactRegistryCredentials{ - // https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#az-acr-login-with---expose-token - Username: "00000000-0000-0000-0000-000000000000", - Password: tokenResp.RefreshToken, + Authenticator: authn.FromConfig(authn.AuthConfig{ + // https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#az-acr-login-with---expose-token + Username: "00000000-0000-0000-0000-000000000000", + Password: tokenResp.RefreshToken, + }), ExpiresAt: expiry.Time, }, nil } diff --git a/auth/azure/provider_test.go b/auth/azure/provider_test.go index 9b036eaae..50c4b7887 100644 --- a/auth/azure/provider_test.go +++ b/auth/azure/provider_test.go @@ -28,7 +28,9 @@ import ( "testing" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/golang-jwt/jwt/v5" + "github.com/google/go-containerregistry/pkg/authn" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,13 +39,14 @@ import ( "github.com/fluxcd/pkg/auth/azure" ) -func TestProvider_NewDefaultToken_Options(t *testing.T) { +func TestProvider_NewControllerToken(t *testing.T) { g := NewWithT(t) impl := &mockImplementation{ t: t, argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, argScopes: []string{"scope1", "scope2"}, + returnToken: "access-token", } opts := []auth.Option{ @@ -52,14 +55,12 @@ func TestProvider_NewDefaultToken_Options(t *testing.T) { } provider := azure.Provider{Implementation: impl} - token, err := provider.NewDefaultToken(context.Background(), opts...) + token, err := provider.NewControllerToken(context.Background(), opts...) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}})) } -func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { - g := NewWithT(t) - +func TestProvider_NewTokenForServiceAccount(t *testing.T) { impl := &mockImplementation{ t: t, argTenantID: "tenant-id", @@ -67,55 +68,188 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { argOIDCToken: "oidc-token", argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, argScopes: []string{"scope1", "scope2"}, + returnToken: "access-token", } - oidcToken := "oidc-token" - serviceAccount := corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ + for _, tt := range []struct { + name string + annotations map[string]string + err string + }{ + { + name: "valid", + annotations: map[string]string{ "azure.workload.identity/tenant-id": "tenant-id", "azure.workload.identity/client-id": "client-id", }, }, + { + name: "tenant id missing", + annotations: map[string]string{ + "azure.workload.identity/client-id": "client-id", + }, + err: "azure tenant ID is not set in the service account annotation azure.workload.identity/tenant-id", + }, + { + name: "client id missing", + annotations: map[string]string{ + "azure.workload.identity/tenant-id": "tenant-id", + }, + err: "azure client ID is not set in the service account annotation azure.workload.identity/client-id", + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + oidcToken := "oidc-token" + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: tt.annotations, + }, + } + opts := []auth.Option{ + auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), + auth.WithScopes("scope1", "scope2"), + } + + provider := azure.Provider{Implementation: impl} + token, err := provider.NewTokenForServiceAccount(context.Background(), oidcToken, serviceAccount, opts...) + + if tt.err == "" { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}})) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(Equal(tt.err)) + g.Expect(token).To(BeNil()) + } + }) } - opts := []auth.Option{ - auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), - auth.WithScopes("scope1", "scope2"), - } - - provider := azure.Provider{Implementation: impl} - token, err := provider.NewTokenForServiceAccount(context.Background(), oidcToken, serviceAccount, opts...) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) } -func TestProvider_NewArtifactRegistryToken_Options(t *testing.T) { +func TestProvider_NewArtifactRegistryCredentials(t *testing.T) { g := NewWithT(t) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) g.Expect(err).NotTo(HaveOccurred()) + exp := time.Now().Add(time.Hour).Unix() refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "exp": time.Now().Add(time.Hour).Unix(), + "exp": exp, }).SignedString(privateKey) g.Expect(err).NotTo(HaveOccurred()) - impl := &mockImplementation{ - t: t, - argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, - returnResp: &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"refresh_token":"%s"}`, refreshToken))), + for _, tt := range []struct { + registry string + expectedScope string + }{ + { + registry: "foo.azurecr.io", + expectedScope: "https://management.azure.com/.default", + }, + { + registry: "foo.azurecr.cn", + expectedScope: "https://management.chinacloudapi.cn/.default", }, + { + registry: "foo.azurecr.us", + expectedScope: "https://management.usgovcloudapi.net/.default", + }, + } { + t.Run(tt.registry, func(t *testing.T) { + g := NewWithT(t) + + impl := &mockImplementation{ + t: t, + argURL: fmt.Sprintf("https://%s/oauth2/exchange", tt.registry), + argBody: fmt.Sprintf("access_token=access-token&grant_type=access_token&service=%s", tt.registry), + argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, + argScopes: []string{tt.expectedScope}, + returnToken: "access-token", + returnResp: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"refresh_token":"%s"}`, refreshToken))), + }, + } + provider := azure.Provider{Implementation: impl} + + artifactRepository := fmt.Sprintf("%s/repo", tt.registry) + opts := []auth.Option{ + auth.WithArtifactRepository(artifactRepository), + auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), + } + + registryURL, err := provider.ParseArtifactRepository(artifactRepository) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(registryURL).To(Equal(fmt.Sprintf("https://%s", tt.registry))) + + accessToken, err := provider.NewControllerToken(context.Background(), opts...) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(accessToken).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}})) + + token, err := provider.NewArtifactRegistryCredentials(context.Background(), registryURL, accessToken, opts...) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(token).To(Equal(&auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{ + Username: "00000000-0000-0000-0000-000000000000", + Password: refreshToken, + }), + ExpiresAt: time.Unix(exp, 0), + })) + }) } +} - artifactRepository := "acr-repo" - accessToken := &azure.Token{} - opts := []auth.Option{ - auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}), +func TestProvider_ParseArtifactRegistry(t *testing.T) { + for _, tt := range []struct { + artifactRepository string + expectedRegistryURL string + expectValid bool + }{ + { + artifactRepository: "foo.azurecr.io", + expectedRegistryURL: "https://foo.azurecr.io", + expectValid: true, + }, + { + artifactRepository: "foo.azurecr.cn", + expectedRegistryURL: "https://foo.azurecr.cn", + expectValid: true, + }, + { + artifactRepository: "foo.azurecr.de", + expectedRegistryURL: "https://foo.azurecr.de", + expectValid: true, + }, + { + artifactRepository: "foo.azurecr.us", + expectedRegistryURL: "https://foo.azurecr.us", + expectValid: true, + }, + { + artifactRepository: "foo.azurecr.com", + expectValid: false, + }, + { + artifactRepository: ".azurecr.io", + expectValid: false, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com", + expectValid: false, + }, + } { + t.Run(tt.artifactRepository, func(t *testing.T) { + g := NewWithT(t) + + registryURL, err := azure.Provider{}.ParseArtifactRepository(tt.artifactRepository) + + if tt.expectValid { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(registryURL).To(Equal(tt.expectedRegistryURL)) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(registryURL).To(BeEmpty()) + } + }) } - - provider := azure.Provider{Implementation: impl} - token, err := provider.NewArtifactRegistryToken(context.Background(), artifactRepository, accessToken, opts...) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) } diff --git a/auth/gcp/implementation_test.go b/auth/gcp/implementation_test.go index 98019a689..d914b66da 100644 --- a/auth/gcp/implementation_test.go +++ b/auth/gcp/implementation_test.go @@ -32,6 +32,8 @@ type mockImplementation struct { argConfig externalaccount.Config argProxyURL *url.URL + + returnToken *oauth2.Token } func (m *mockImplementation) DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) { @@ -50,7 +52,7 @@ func (m *mockImplementation) DefaultTokenSource(ctx context.Context, scope ...st "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", })) - return oauth2.StaticTokenSource(&oauth2.Token{}), nil + return oauth2.StaticTokenSource(m.returnToken), nil } func (m *mockImplementation) NewTokenSource(ctx context.Context, conf externalaccount.Config) (oauth2.TokenSource, error) { @@ -66,5 +68,5 @@ func (m *mockImplementation) NewTokenSource(ctx context.Context, conf externalac g.Expect(err).NotTo(HaveOccurred()) g.Expect(proxyURL).To(Equal(m.argProxyURL)) g.Expect(conf).To(Equal(m.argConfig)) - return oauth2.StaticTokenSource(&oauth2.Token{}), nil + return oauth2.StaticTokenSource(m.returnToken), nil } diff --git a/auth/gcp/options.go b/auth/gcp/options.go index 2ac09c1fb..62927016e 100644 --- a/auth/gcp/options.go +++ b/auth/gcp/options.go @@ -28,13 +28,14 @@ const serviceAccountEmailPattern = `^[a-zA-Z0-9-]{1,100}@[a-zA-Z0-9-]{1,100}\.ia var serviceAccountEmailRegex = regexp.MustCompile(serviceAccountEmailPattern) func getServiceAccountEmail(serviceAccount corev1.ServiceAccount) (string, error) { - email := serviceAccount.Annotations["iam.gke.io/gcp-service-account"] + const key = "iam.gke.io/gcp-service-account" + email := serviceAccount.Annotations[key] if email == "" { return "", nil } if !serviceAccountEmailRegex.MatchString(email) { - return "", fmt.Errorf("invalid GCP service account email: '%s'. must match %s", - email, serviceAccountEmailPattern) + return "", fmt.Errorf("invalid %s annotation: '%s'. must match %s", + key, email, serviceAccountEmailPattern) } return email, nil } diff --git a/auth/gcp/provider.go b/auth/gcp/provider.go index f51b88923..d15cf1775 100644 --- a/auth/gcp/provider.go +++ b/auth/gcp/provider.go @@ -19,7 +19,9 @@ package gcp import ( "context" "fmt" + "regexp" + "github.com/google/go-containerregistry/pkg/authn" "golang.org/x/oauth2" "golang.org/x/oauth2/google/externalaccount" corev1 "k8s.io/api/core/v1" @@ -43,8 +45,8 @@ func (Provider) GetName() string { return ProviderName } -// NewDefaultToken implements auth.Provider. -func (p Provider) NewDefaultToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { +// NewControllerToken implements auth.Provider. +func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { var o auth.Options o.Apply(opts...) @@ -128,22 +130,38 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin return &Token{*token}, nil } -// GetArtifactCacheKey implements auth.Provider. -func (Provider) GetArtifactCacheKey(artifactRepository string) string { - // The artifact repository is irrelevant for GCP registry credentials. - return ProviderName +const registryPattern = `^(((.+\.)?gcr\.io)|(.+-docker\.pkg\.dev))$` + +var registryRegex = regexp.MustCompile(registryPattern) + +// ParseArtifactRepository implements auth.Provider. +func (Provider) ParseArtifactRepository(artifactRepository string) (string, error) { + registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository) + if err != nil { + return "", err + } + + if !registryRegex.MatchString(registry) { + return "", fmt.Errorf("invalid GCP registry: '%s'. must match %s", + registry, registryPattern) + } + + // The artifact repository is irrelevant for issuing GCP registry credentials, + // just return the provider name for inclusion in the cache key. + return ProviderName, nil } -// NewArtifactRegistryToken implements auth.Provider. -func (Provider) NewArtifactRegistryToken(ctx context.Context, artifactRepository string, - accessToken auth.Token, opts ...auth.Option) (auth.Token, error) { +// NewArtifactRegistryCredentials implements auth.Provider. +func (Provider) NewArtifactRegistryCredentials(_ context.Context, _ string, + accessToken auth.Token, _ ...auth.Option) (*auth.ArtifactRegistryCredentials, error) { t := accessToken.(*Token) - // The artifact repository is irrelevant for GCP registry credentials. return &auth.ArtifactRegistryCredentials{ - Username: "oauth2accesstoken", - Password: t.AccessToken, + Authenticator: authn.FromConfig(authn.AuthConfig{ + Username: "oauth2accesstoken", + Password: t.AccessToken, + }), ExpiresAt: t.Expiry, }, nil } diff --git a/auth/gcp/provider_test.go b/auth/gcp/provider_test.go index 3f9082539..2614a4f41 100644 --- a/auth/gcp/provider_test.go +++ b/auth/gcp/provider_test.go @@ -27,7 +27,9 @@ import ( "testing" "time" + "github.com/google/go-containerregistry/pkg/authn" . "github.com/onsi/gomega" + "golang.org/x/oauth2" "golang.org/x/oauth2/google/externalaccount" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -36,12 +38,13 @@ import ( "github.com/fluxcd/pkg/auth/gcp" ) -func TestProvider_NewDefaultToken_Options(t *testing.T) { +func TestNewControllerToken(t *testing.T) { g := NewWithT(t) impl := &mockImplementation{ t: t, argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, + returnToken: &oauth2.Token{AccessToken: "access-token"}, } opts := []auth.Option{ @@ -49,12 +52,12 @@ func TestProvider_NewDefaultToken_Options(t *testing.T) { } provider := gcp.Provider{Implementation: impl} - token, err := provider.NewDefaultToken(context.Background(), opts...) + token, err := provider.NewControllerToken(context.Background(), opts...) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + g.Expect(token).To(Equal(&gcp.Token{oauth2.Token{AccessToken: "access-token"}})) } -func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { +func TestProvider_NewTokenForServiceAccount(t *testing.T) { g := NewWithT(t) // Start GKE metadata server. @@ -89,9 +92,10 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { t.Setenv("GCE_METADATA_HOST", gceMetadataHost) for _, tt := range []struct { - name string - conf externalaccount.Config - saAnnotations map[string]string + name string + conf externalaccount.Config + annotations map[string]string + err string }{ { name: "direct access", @@ -122,10 +126,17 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { SubjectTokenSupplier: gcp.TokenSupplier("oidc-token"), UniverseDomain: "googleapis.com", }, - saAnnotations: map[string]string{ + annotations: map[string]string{ "iam.gke.io/gcp-service-account": "test-sa@project-id.iam.gserviceaccount.com", }, }, + { + name: "invalid sa email", + annotations: map[string]string{ + "iam.gke.io/gcp-service-account": "foobar", + }, + err: `invalid iam.gke.io/gcp-service-account annotation: 'foobar'. must match ^[a-zA-Z0-9-]{1,100}@[a-zA-Z0-9-]{1,100}\.iam\.gserviceaccount\.com$`, + }, } { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) @@ -134,6 +145,7 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { t: t, argConfig: tt.conf, argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"}, + returnToken: &oauth2.Token{AccessToken: "access-token"}, } oidcToken := "oidc-token" @@ -141,7 +153,7 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "test-sa", Namespace: "test-ns", - Annotations: tt.saAnnotations, + Annotations: tt.annotations, }, } opts := []auth.Option{ @@ -151,8 +163,84 @@ func TestProvider_NewTokenForServiceAccount_Options(t *testing.T) { provider := gcp.Provider{Implementation: impl} token, err := provider.NewTokenForServiceAccount(context.Background(), oidcToken, serviceAccount, opts...) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(token).NotTo(BeNil()) + + if tt.err == "" { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(token).To(Equal(&gcp.Token{oauth2.Token{AccessToken: "access-token"}})) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(Equal(tt.err)) + g.Expect(token).To(BeNil()) + } + }) + } +} + +func TestProvider_NewArtifactRegistryCredentials(t *testing.T) { + g := NewWithT(t) + + exp := time.Now() + + accessToken := &gcp.Token{oauth2.Token{ + AccessToken: "access-token", + Expiry: exp, + }} + + creds, err := gcp.Provider{}.NewArtifactRegistryCredentials(context.Background(), "", accessToken) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(creds).NotTo(BeNil()) + g.Expect(creds.ExpiresAt).To(Equal(exp)) + g.Expect(creds.Authenticator).NotTo(BeNil()) + authConf, err := creds.Authenticator.Authorization() + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(authConf).To(Equal(&authn.AuthConfig{ + Username: "oauth2accesstoken", + Password: "access-token", + })) +} + +func TestProvider_ParseArtifactRegistry(t *testing.T) { + for _, tt := range []struct { + artifactRepository string + expectValid bool + }{ + { + artifactRepository: "gcr.io", + expectValid: true, + }, + { + artifactRepository: ".gcr.io", + expectValid: false, + }, + { + artifactRepository: "a.gcr.io", + expectValid: true, + }, + { + artifactRepository: "-docker.pkg.dev", + expectValid: false, + }, + { + artifactRepository: "a-docker.pkg.dev", + expectValid: true, + }, + { + artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com", + expectValid: false, + }, + } { + t.Run(tt.artifactRepository, func(t *testing.T) { + g := NewWithT(t) + + cacheKey, err := gcp.Provider{}.ParseArtifactRepository(tt.artifactRepository) + + if tt.expectValid { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(cacheKey).To(Equal("gcp")) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(cacheKey).To(BeEmpty()) + } }) } } diff --git a/auth/get_token.go b/auth/get_token.go index a2e4c3ed2..48980ec02 100644 --- a/auth/get_token.go +++ b/auth/get_token.go @@ -35,23 +35,24 @@ func GetToken(ctx context.Context, provider Provider, opts ...Option) (Token, er var o Options o.Apply(opts...) - // Initialize default token fetcher. + // Initialize access token fetcher for controller. newAccessToken := func() (Token, error) { - token, err := provider.NewDefaultToken(ctx, opts...) + token, err := provider.NewControllerToken(ctx, opts...) if err != nil { - return nil, fmt.Errorf("failed to create default access token: %w", err) + return nil, fmt.Errorf("failed to create provider access token for the controlller: %w", err) } return token, nil } - // Initialize service account token fetcher if service account is specified. + // Update access token fetcher for a service account if specified. var providerIdentity string var serviceAccountP *corev1.ServiceAccount if o.ServiceAccount != nil { // Get service account and prepare a function to create a token for it. var serviceAccount corev1.ServiceAccount if err := o.Client.Get(ctx, *o.ServiceAccount, &serviceAccount); err != nil { - return nil, fmt.Errorf("failed to get service account: %w", err) + return nil, fmt.Errorf("failed to get service account '%s/%s': %w", + o.ServiceAccount.Namespace, o.ServiceAccount.Name, err) } serviceAccountP = &serviceAccount @@ -69,34 +70,50 @@ func GetToken(ctx context.Context, provider Provider, opts ...Option) (Token, er serviceAccount.Namespace, serviceAccount.Name, err) } - // Initialize access token fetcher that will use the identity token. + // Update access token fetcher. newAccessToken = func() (Token, error) { identityToken, err := newServiceAccountToken(ctx, o.Client, serviceAccount, providerAudience) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create kubernetes token for service account '%s/%s': %w", + serviceAccount.Namespace, serviceAccount.Name, err) } token, err := provider.NewTokenForServiceAccount(ctx, identityToken, serviceAccount, opts...) if err != nil { - return nil, fmt.Errorf("failed to create access token: %w", err) + return nil, fmt.Errorf("failed to create provider access token for service account '%s/%s': %w", + serviceAccount.Namespace, serviceAccount.Name, err) } return token, nil } } - // Initialize registry token fetcher if artifact repository is specified. + // Initialize token fetcher with access token fetcher. newToken := newAccessToken + + // Update token fetcher to registry token fetcher if artifact repository is specified. + var artifactRepositoryCacheKey string if o.ArtifactRepository != "" { + // Parse artifact repository. + registryInput, err := provider.ParseArtifactRepository(o.ArtifactRepository) + if err != nil { + return nil, fmt.Errorf("failed to parse artifact repository '%s': %w", + o.ArtifactRepository, err) + } + + // Set artifact repository cache key. + artifactRepositoryCacheKey = registryInput + + // Update token fetcher. newToken = func() (Token, error) { accessToken, err := newAccessToken() if err != nil { return nil, err } - token, err := provider.NewArtifactRegistryToken(ctx, o.ArtifactRepository, accessToken, opts...) + token, err := provider.NewArtifactRegistryCredentials(ctx, registryInput, accessToken, opts...) if err != nil { - return nil, fmt.Errorf("failed to create artifact registry login: %w", err) + return nil, fmt.Errorf("failed to create artifact registry credentials: %w", err) } return token, nil @@ -109,7 +126,7 @@ func GetToken(ctx context.Context, provider Provider, opts ...Option) (Token, er } // Build cache key. - cacheKey := buildCacheKey(provider, providerIdentity, serviceAccountP, opts...) + cacheKey := buildCacheKey(provider, providerIdentity, artifactRepositoryCacheKey, serviceAccountP, opts...) // Get involved object details. kind := o.InvolvedObject.Kind @@ -136,12 +153,12 @@ func newServiceAccountToken(ctx context.Context, client client.Client, }, } if err := client.SubResource("token").Create(ctx, &serviceAccount, tokenReq); err != nil { - return "", fmt.Errorf("failed to create kubernetes service account token: %w", err) + return "", err } return tokenReq.Status.Token, nil } -func buildCacheKey(provider Provider, providerIdentity string, +func buildCacheKey(provider Provider, providerIdentity, artifactRepositoryKey string, serviceAccount *corev1.ServiceAccount, opts ...Option) string { var o Options @@ -162,7 +179,11 @@ func buildCacheKey(provider Provider, providerIdentity string, } if o.ArtifactRepository != "" { - keyParts = append(keyParts, fmt.Sprintf("artifactRepositoryKey=%s", provider.GetArtifactCacheKey(o.ArtifactRepository))) + keyParts = append(keyParts, fmt.Sprintf("artifactRepositoryKey=%s", artifactRepositoryKey)) + } + + if o.STSRegion != "" { + keyParts = append(keyParts, fmt.Sprintf("stsRegion=%s", o.STSRegion)) } if o.STSEndpoint != "" { diff --git a/auth/get_token_test.go b/auth/get_token_test.go index 9a8008639..70f5d990a 100644 --- a/auth/get_token_test.go +++ b/auth/get_token_test.go @@ -28,6 +28,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt/v5" + "github.com/google/go-containerregistry/pkg/authn" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -54,10 +55,11 @@ type mockProvider struct { returnAudience string returnIdentity string returnIdentityErr string - returnArtifactCacheKey string - returnDefaultToken auth.Token + returnRegistryErr string + returnRegistryInput string + returnControllerToken auth.Token returnAccessToken auth.Token - returnRegistryToken auth.Token + returnRegistryToken *auth.ArtifactRegistryCredentials paramServiceAccount corev1.ServiceAccount paramOIDCTokenClient *http.Client paramArtifactRepository string @@ -68,9 +70,9 @@ func (m *mockProvider) GetName() string { return m.returnName } -func (m *mockProvider) NewDefaultToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { +func (m *mockProvider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) { checkOptions(m.t, opts...) - return m.returnDefaultToken, nil + return m.returnControllerToken, nil } func (m *mockProvider) GetAudience(ctx context.Context) (string, error) { @@ -114,18 +116,21 @@ func (m *mockProvider) NewTokenForServiceAccount(ctx context.Context, oidcToken return m.returnAccessToken, nil } -func (m *mockProvider) GetArtifactCacheKey(artifactRepository string) string { +func (m *mockProvider) ParseArtifactRepository(artifactRepository string) (string, error) { m.t.Helper() g := NewWithT(m.t) g.Expect(artifactRepository).To(Equal(m.paramArtifactRepository)) - return m.returnArtifactCacheKey + if m.returnRegistryErr != "" { + return "", errors.New(m.returnRegistryErr) + } + return m.returnRegistryInput, nil } -func (m *mockProvider) NewArtifactRegistryToken(ctx context.Context, artifactRepository string, - accessToken auth.Token, opts ...auth.Option) (auth.Token, error) { +func (m *mockProvider) NewArtifactRegistryCredentials(ctx context.Context, registryInput string, + accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) { m.t.Helper() g := NewWithT(m.t) - g.Expect(artifactRepository).To(Equal(m.paramArtifactRepository)) + g.Expect(registryInput).To(Equal(m.paramArtifactRepository)) g.Expect(accessToken).To(Equal(m.paramAccessToken)) checkOptions(m.t, opts...) return m.returnRegistryToken, nil @@ -139,6 +144,7 @@ func checkOptions(t *testing.T, opts ...auth.Option) { o.Apply(opts...) g.Expect(o.Scopes).To(Equal([]string{"scope1", "scope2"})) + g.Expect(o.STSRegion).To(Equal("us-east-1")) g.Expect(o.STSEndpoint).To(Equal("https://sts.some-cloud.io")) g.Expect(o.ProxyURL).To(Equal(&url.URL{Scheme: "http", Host: "proxy.io:8080"})) } @@ -209,32 +215,39 @@ func TestGetToken(t *testing.T) { expectedErr string }{ { - name: "default access token (from controller)", + name: "controller access token", provider: &mockProvider{ - returnDefaultToken: &mockToken{token: "mock-default-token"}, + returnControllerToken: &mockToken{token: "mock-default-token"}, }, opts: []auth.Option{ auth.WithScopes("scope1", "scope2"), + auth.WithSTSRegion("us-east-1"), auth.WithSTSEndpoint("https://sts.some-cloud.io"), auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}), }, expectedToken: &mockToken{token: "mock-default-token"}, }, { - name: "registry token from default access token (from controller)", + name: "registry token from controller access token", provider: &mockProvider{ - returnDefaultToken: &mockToken{token: "mock-default-token"}, - returnRegistryToken: &mockToken{token: "mock-registry-token"}, + returnRegistryInput: "some-registry.io/some/artifact", + returnControllerToken: &mockToken{token: "mock-default-token"}, + returnRegistryToken: &auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}), + }, paramAccessToken: &mockToken{token: "mock-default-token"}, paramArtifactRepository: "some-registry.io/some/artifact", }, opts: []auth.Option{ auth.WithArtifactRepository("some-registry.io/some/artifact"), auth.WithScopes("scope1", "scope2"), + auth.WithSTSRegion("us-east-1"), auth.WithSTSEndpoint("https://sts.some-cloud.io"), auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}), }, - expectedToken: &mockToken{token: "mock-registry-token"}, + expectedToken: &auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}), + }, }, { name: "access token from service account", @@ -248,6 +261,7 @@ func TestGetToken(t *testing.T) { opts: []auth.Option{ auth.WithServiceAccount(saRef, kubeClient), auth.WithScopes("scope1", "scope2"), + auth.WithSTSRegion("us-east-1"), auth.WithSTSEndpoint("https://sts.some-cloud.io"), auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}), // Exercise the code path where a cache is set but no token is @@ -263,10 +277,13 @@ func TestGetToken(t *testing.T) { { name: "registry token from access token from service account", provider: &mockProvider{ - returnName: "mock-provider", - returnAudience: "mock-audience", - returnAccessToken: &mockToken{token: "mock-access-token"}, - returnRegistryToken: &mockToken{token: "mock-registry-token"}, + returnName: "mock-provider", + returnAudience: "mock-audience", + returnRegistryInput: "some-registry.io/some/artifact", + returnAccessToken: &mockToken{token: "mock-access-token"}, + returnRegistryToken: &auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}), + }, paramServiceAccount: *defaultServiceAccount, paramOIDCTokenClient: oidcClient, paramArtifactRepository: "some-registry.io/some/artifact", @@ -276,17 +293,20 @@ func TestGetToken(t *testing.T) { auth.WithServiceAccount(saRef, kubeClient), auth.WithArtifactRepository("some-registry.io/some/artifact"), auth.WithScopes("scope1", "scope2"), + auth.WithSTSRegion("us-east-1"), auth.WithSTSEndpoint("https://sts.some-cloud.io"), auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}), }, - expectedToken: &mockToken{token: "mock-registry-token"}, + expectedToken: &auth.ArtifactRegistryCredentials{ + Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}), + }, }, { name: "all the options are taken into account in the cache key", provider: &mockProvider{ returnName: "mock-provider", returnIdentity: "mock-identity", - returnArtifactCacheKey: "artifact-cache-key", + returnRegistryInput: "artifact-cache-key", paramServiceAccount: *defaultServiceAccount, paramArtifactRepository: "some-registry.io/some/artifact", }, @@ -294,13 +314,14 @@ func TestGetToken(t *testing.T) { auth.WithServiceAccount(saRef, kubeClient), auth.WithScopes("scope1", "scope2"), auth.WithArtifactRepository("some-registry.io/some/artifact"), + auth.WithSTSRegion("us-east-1"), auth.WithSTSEndpoint("https://sts.some-cloud.io"), auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}), func(o *auth.Options) { tokenCache, err := cache.NewTokenCache(1) g.Expect(err).NotTo(HaveOccurred()) - const key = "2fc658e8e2711dbfd5301a99dcf981c2d94d53bd9f4b616e33c7d087d6f1110f" + const key = "da48da328aa46181e677d76c835b7ca32b5fbf64da01577463d42a2720708ecb" token := &mockToken{token: "cached-token"} cachedToken, ok, err := tokenCache.GetOrSet(ctx, key, func(ctx context.Context) (cache.Token, error) { return token, nil @@ -341,6 +362,17 @@ func TestGetToken(t *testing.T) { }, expectedErr: "failed to get provider identity from service account 'default/default' annotations: mock error", }, + { + name: "error parsing artifact repository", + provider: &mockProvider{ + paramArtifactRepository: "some-registry.io/some/artifact", + returnRegistryErr: "mock error", + }, + opts: []auth.Option{ + auth.WithArtifactRepository("some-registry.io/some/artifact"), + }, + expectedErr: "failed to parse artifact repository 'some-registry.io/some/artifact': mock error", + }, } { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) diff --git a/auth/go.mod b/auth/go.mod index 73c59a84d..63022434c 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -8,7 +8,7 @@ replace ( ) require ( - cloud.google.com/go/compute/metadata v0.3.0 + cloud.google.com/go/compute/metadata v0.6.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 github.com/aws/aws-sdk-go-v2 v1.36.3 @@ -19,6 +19,7 @@ require ( github.com/coreos/go-oidc/v3 v3.14.1 github.com/fluxcd/pkg/cache v0.9.0 github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/go-containerregistry v0.20.3 github.com/onsi/gomega v1.37.0 golang.org/x/oauth2 v0.28.0 k8s.io/api v0.33.0 @@ -41,6 +42,8 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -58,15 +61,18 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.37.0 // indirect diff --git a/auth/go.sum b/auth/go.sum index 8a02bf4c1..fae556606 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -1,5 +1,5 @@ -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= @@ -53,6 +53,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -88,6 +92,8 @@ github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcb github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -114,6 +120,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -125,6 +133,8 @@ github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0 github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -144,12 +154,15 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -184,6 +197,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -219,6 +233,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= diff --git a/auth/options.go b/auth/options.go index ac3089d60..96c41b141 100644 --- a/auth/options.go +++ b/auth/options.go @@ -37,6 +37,7 @@ type Options struct { InvolvedObject cache.InvolvedObject Scopes []string ArtifactRepository string + STSRegion string STSEndpoint string ProxyURL *url.URL } @@ -76,6 +77,14 @@ func WithArtifactRepository(artifactRepository string) Option { } } +// WithSTSRegion sets the region for the STS service (some cloud providers +// require a region, e.g. AWS). +func WithSTSRegion(stsRegion string) Option { + return func(o *Options) { + o.STSRegion = stsRegion + } +} + // WithSTSEndpoint sets the endpoint for the STS service. func WithSTSEndpoint(stsEndpoint string) Option { return func(o *Options) { diff --git a/auth/provider.go b/auth/provider.go index f09186bf0..911926334 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -28,13 +28,11 @@ type Provider interface { // GetName returns the name of the provider. GetName() string - // NewDefaultToken returns a token that can be used to authenticate with the - // cloud provider retrieved from the default source, i.e. from the pod's - // environment, e.g. files mounted in the pod, environment variables, - // local metadata services, etc. In this case the method would implicitly - // use the ServiceAccount associated with the controller pod, and not one - // specified in the options. - NewDefaultToken(ctx context.Context, opts ...Option) (Token, error) + // NewControllerToken returns a token that can be used to authenticate + // with the cloud provider retrieved from the default source, i.e. from + // the environment of the controller pod, e.g. files mounted in the pod, + // environment variables, local metadata services, etc. + NewControllerToken(ctx context.Context, opts ...Option) (Token, error) // GetAudience returns the audience the OIDC tokens issued representing // ServiceAccounts should have. This is usually a string that represents @@ -54,12 +52,15 @@ type Provider interface { NewTokenForServiceAccount(ctx context.Context, oidcToken string, serviceAccount corev1.ServiceAccount, opts ...Option) (Token, error) - // GetArtifactCacheKey extracts the part of the artifact repository that must be - // included in cache keys when caching registry credentials for the provider. - GetArtifactCacheKey(artifactRepository string) string + // ParseArtifactRepository parses the artifact repository to verify if it + // is a valid repository for the provider. As a result, it returns the + // input required for the provider to issue the registry credentials. This + // input is also included as part of the cache key for the issued credentials. + ParseArtifactRepository(artifactRepository string) (string, error) - // NewArtifactRegistryToken takes an artifact repository and an access token and returns a token - // that can be used to authenticate with the artifact registry of the artifact. - NewArtifactRegistryToken(ctx context.Context, artifactRepository string, - accessToken Token, opts ...Option) (Token, error) + // NewArtifactRegistryCredentials takes the registry input extracted by + // ParseArtifactRepository() and an access token and returns credentials + // that can be used to authenticate with the registry. + NewArtifactRegistryCredentials(ctx context.Context, registryInput string, + accessToken Token, opts ...Option) (*ArtifactRegistryCredentials, error) } diff --git a/auth/registry.go b/auth/registry.go new file mode 100644 index 000000000..b6994b22e --- /dev/null +++ b/auth/registry.go @@ -0,0 +1,51 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "strings" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" +) + +// ArtifactRegistryCredentials is a particular type implementing the Token interface +// for credentials that can be used to authenticate against an artifact registry +// from a cloud provider. This type is compatible with all the cloud providers +// and should be returned when the artifact repository is configured in the options. +type ArtifactRegistryCredentials struct { + authn.Authenticator + ExpiresAt time.Time +} + +func (a *ArtifactRegistryCredentials) GetDuration() time.Duration { + return time.Until(a.ExpiresAt) +} + +// GetRegistryFromArtifactRepository returns the registry from the artifact repository. +func GetRegistryFromArtifactRepository(artifactRepository string) (string, error) { + registry := strings.TrimSuffix(artifactRepository, "/") + if strings.ContainsRune(registry, '/') { + ref, err := name.ParseReference(registry) + if err != nil { + return "", err + } + return ref.Context().RegistryStr(), nil + } + return registry, nil +} diff --git a/auth/registry_test.go b/auth/registry_test.go new file mode 100644 index 000000000..5b2a4e610 --- /dev/null +++ b/auth/registry_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/fluxcd/pkg/auth" +) + +func TestGetRegistryFromArtifactRepository(t *testing.T) { + for _, tt := range []struct { + name string + artifactRepository string + expectedRegistry string + }{ + { + name: "dot-less host with port", + artifactRepository: "localhost:5000", + expectedRegistry: "localhost:5000", + }, + { + name: "dot-less host without port", + artifactRepository: "localhost", + expectedRegistry: "localhost", + }, + { + name: "host with port", + artifactRepository: "registry.io:5000", + expectedRegistry: "registry.io:5000", + }, + { + name: "host without port", + artifactRepository: "registry.io", + expectedRegistry: "registry.io", + }, + { + name: "dot-less repo with port", + artifactRepository: "localhost:5000/repo", + expectedRegistry: "localhost:5000", + }, + { + name: "dot-less repo without port", + artifactRepository: "localhost/repo", + expectedRegistry: "index.docker.io", + }, + { + name: "repo with port", + artifactRepository: "registry.io:5000/repo", + expectedRegistry: "registry.io:5000", + }, + { + name: "repo without port", + artifactRepository: "registry.io/repo", + expectedRegistry: "registry.io", + }, + { + name: "tag", + artifactRepository: "registry.io/repo:tag", + expectedRegistry: "registry.io", + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + reg, err := auth.GetRegistryFromArtifactRepository(tt.artifactRepository) + + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(reg).To(Equal(tt.expectedRegistry)) + }) + } +} diff --git a/auth/token.go b/auth/token.go index bf3e3ffa7..9daeaa5ea 100644 --- a/auth/token.go +++ b/auth/token.go @@ -16,7 +16,9 @@ limitations under the License. package auth -import "time" +import ( + "time" +) // Token is an interface that represents an access token that can be used to // authenticate requests for a cloud provider. The only common method is for @@ -27,21 +29,7 @@ import "time" // cast it to. type Token interface { // GetDuration returns the duration for which the token will still be valid - // relative to approximately time.Now(). This is used to determine when the token should - // be refreshed. + // relative to approximately time.Now(). This is used to determine when the + // token should be renewed. GetDuration() time.Duration } - -// ArtifactRegistryCredentials is a particular type implementing the Token interface -// for credentials that can be used to authenticate with an artifact registry -// from a cloud provider. This type is compatible with all the cloud providers -// and should be returned when the artifact repository is configured in the options. -type ArtifactRegistryCredentials struct { - Username string - Password string - ExpiresAt time.Time -} - -func (a *ArtifactRegistryCredentials) GetDuration() time.Duration { - return time.Until(a.ExpiresAt) -} diff --git a/oci/errors.go b/auth/utils/doc.go similarity index 62% rename from oci/errors.go rename to auth/utils/doc.go index b9b63e0e7..f050cab54 100644 --- a/oci/errors.go +++ b/auth/utils/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Flux authors +Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package oci - -import "errors" - -var ( - // ErrUnconfiguredProvider is returned when the OCI registry provider is - // not configured. - ErrUnconfiguredProvider = errors.New("registry provider not configured") -) +// authutils contains utility functions that import both the core +// auth package and the provider packages i.e. functions that +// cannot be placed in the core package because they would cause +// a cyclic dependency (the provider packages also import the +// core package). +package authutils diff --git a/oci/auth/flag_test.go b/auth/utils/provider.go similarity index 53% rename from oci/auth/flag_test.go rename to auth/utils/provider.go index 229ba8f71..52134dc92 100644 --- a/oci/auth/flag_test.go +++ b/auth/utils/provider.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Flux authors +Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Make sure we never inject non-test flags when the auth packages are imported. -// Refer https://github.com/fluxcd/pkg/issues/645. -package auth_test +package authutils import ( - "flag" - "strings" - "testing" - - _ "github.com/fluxcd/pkg/oci/auth/login" + "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/auth/aws" + "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/gcp" ) -func TestNonTestFlagCheck(t *testing.T) { - flagCheck := func(f *flag.Flag) { - if !strings.HasPrefix(f.Name, "test.") { - t.Errorf("found non-test command line flag: %q", f.Name) - } +// ProviderByName looks up the implemented providers by name. +func ProviderByName(name string) auth.Provider { + switch name { + case aws.ProviderName: + return aws.Provider{} + case azure.ProviderName: + return azure.Provider{} + case gcp.ProviderName: + return gcp.Provider{} + default: + return nil } - flag.VisitAll(flagCheck) } diff --git a/auth/utils/registry.go b/auth/utils/registry.go new file mode 100644 index 000000000..c3a7d45d6 --- /dev/null +++ b/auth/utils/registry.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authutils + +import ( + "context" + "errors" + + "github.com/google/go-containerregistry/pkg/authn" + + "github.com/fluxcd/pkg/auth" +) + +// ErrProviderDoesNotSupportRegistry is returned when the provider does not +// support registry authentication. +var ErrProviderDoesNotSupportRegistry = errors.New("provider does not support registry authentication") + +// GetArtifactRegistryCredentials retrieves the credentials for the specified +// artifact repository using the specified provider. It returns an +// authn.Authenticator that can be used to authenticate with the registry. +func GetArtifactRegistryCredentials(ctx context.Context, + providerName, artifactRepository string, + opts ...auth.Option) (authn.Authenticator, error) { + + provider := ProviderByName(providerName) + if provider == nil { + return nil, nil + } + + opts = append(opts, auth.WithArtifactRepository(artifactRepository)) + + token, err := auth.GetToken(ctx, provider, opts...) + if err != nil { + return nil, err + } + + authenticator, ok := token.(authn.Authenticator) + if !ok { + return nil, ErrProviderDoesNotSupportRegistry + } + + return authenticator, nil +} diff --git a/git/go.mod b/git/go.mod index efbe4c344..611dbf9ed 100644 --- a/git/go.mod +++ b/git/go.mod @@ -13,7 +13,7 @@ require ( github.com/ProtonMail/go-crypto v1.2.0 github.com/bradleyfalzon/ghinstallation/v2 v2.15.0 github.com/cyphar/filepath-securejoin v0.4.1 - github.com/fluxcd/pkg/auth v0.11.0 + github.com/fluxcd/pkg/auth v0.12.0 github.com/fluxcd/pkg/cache v0.9.0 github.com/fluxcd/pkg/ssh v0.18.0 github.com/onsi/gomega v1.37.0 @@ -28,6 +28,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -41,6 +43,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -48,14 +51,18 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.37.0 // indirect diff --git a/git/go.sum b/git/go.sum index 452e7e502..ef0065d30 100644 --- a/git/go.sum +++ b/git/go.sum @@ -31,6 +31,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -67,6 +71,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -97,6 +103,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -108,6 +116,8 @@ github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0 github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -127,12 +137,15 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -167,6 +180,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -200,6 +214,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= diff --git a/git/gogit/go.mod b/git/gogit/go.mod index d3aa66836..f5680a96d 100644 --- a/git/gogit/go.mod +++ b/git/gogit/go.mod @@ -17,9 +17,9 @@ require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/elazarl/goproxy v1.7.2 github.com/fluxcd/gitkit v0.6.0 - github.com/fluxcd/pkg/auth v0.11.0 + github.com/fluxcd/pkg/auth v0.12.0 github.com/fluxcd/pkg/cache v0.9.0 - github.com/fluxcd/pkg/git v0.28.0 + github.com/fluxcd/pkg/git v0.29.0 github.com/fluxcd/pkg/gittestserver v0.17.0 github.com/fluxcd/pkg/ssh v0.18.0 github.com/fluxcd/pkg/version v0.7.0 @@ -42,6 +42,8 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -59,6 +61,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -68,16 +71,20 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/git/gogit/go.sum b/git/gogit/go.sum index 111aab4ab..548f3ca92 100644 --- a/git/gogit/go.sum +++ b/git/gogit/go.sum @@ -42,6 +42,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -98,6 +102,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -133,6 +139,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -144,6 +152,8 @@ github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0 github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -168,6 +178,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -178,6 +190,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -262,6 +275,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= diff --git a/git/internal/e2e/go.mod b/git/internal/e2e/go.mod index 8645c5f70..3fa8b01ab 100644 --- a/git/internal/e2e/go.mod +++ b/git/internal/e2e/go.mod @@ -14,7 +14,7 @@ replace ( require ( github.com/fluxcd/go-git-providers v0.22.0 - github.com/fluxcd/pkg/git v0.28.0 + github.com/fluxcd/pkg/git v0.29.0 github.com/fluxcd/pkg/git/gogit v0.23.0 github.com/fluxcd/pkg/gittestserver v0.17.0 github.com/fluxcd/pkg/ssh v0.18.0 @@ -40,11 +40,13 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fluxcd/gitkit v0.6.0 // indirect - github.com/fluxcd/pkg/auth v0.11.0 // indirect + github.com/fluxcd/pkg/auth v0.12.0 // indirect github.com/fluxcd/pkg/cache v0.9.0 // indirect github.com/fluxcd/pkg/version v0.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -61,6 +63,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect github.com/google/go-github/v71 v71.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect @@ -74,16 +77,20 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/git/internal/e2e/go.sum b/git/internal/e2e/go.sum index 608103ba4..fd6febb72 100644 --- a/git/internal/e2e/go.sum +++ b/git/internal/e2e/go.sum @@ -42,6 +42,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -104,6 +108,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= @@ -158,6 +164,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -173,6 +181,8 @@ github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0 github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -197,6 +207,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -207,6 +219,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -295,6 +308,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= diff --git a/oci/auth/aws/auth.go b/oci/auth/aws/auth.go deleted file mode 100644 index 5e0b1952e..000000000 --- a/oci/auth/aws/auth.go +++ /dev/null @@ -1,206 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "net/http" - "net/url" - "regexp" - "strings" - "sync" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/authn" - - "github.com/fluxcd/pkg/oci" -) - -// This regex is sourced from the AWS ECR Credential Helper (https://github.com/awslabs/amazon-ecr-credential-helper). -// It covers both public AWS partitions like amazonaws.com, China partitions like amazonaws.com.cn, and non-public partitions. -var registryPartRe = regexp.MustCompile(`([0-9+]*).dkr.ecr(?:-fips)?\.([^/.]*)\.(amazonaws\.com[.cn]*|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)`) - -// ParseRegistry returns the AWS account ID and region and `true` if -// the image registry/repository is hosted in AWS's Elastic Container Registry, -// otherwise empty strings and `false`. -func ParseRegistry(registry string) (accountId, awsEcrRegion string, ok bool) { - registryParts := registryPartRe.FindAllStringSubmatch(registry, -1) - if len(registryParts) < 1 || len(registryParts[0]) < 3 { - return "", "", false - } - return registryParts[0][1], registryParts[0][2], true -} - -// Client is a AWS ECR client which can log into the registry and return -// authorization information. -type Client struct { - config *aws.Config - mu sync.Mutex - proxyURL *url.URL -} - -// Option is a functional option for configuring the client. -type Option func(*Client) - -// WithProxyURL sets the proxy URL for the client. -func WithProxyURL(proxyURL *url.URL) Option { - return func(c *Client) { - c.proxyURL = proxyURL - } -} - -// NewClient creates a new empty ECR client. -// NOTE: In order to avoid breaking the auth API with aws-sdk-go-v2's default -// config, return an empty Client. Client.getLoginAuth() loads the default -// config if Client.config is nil. This also enables tests to configure the -// Client with stub before calling the login method using Client.WithConfig(). -func NewClient(opts ...Option) *Client { - client := &Client{} - for _, opt := range opts { - opt(client) - } - return client -} - -// WithConfig allows setting the client config if it's uninitialized. -func (c *Client) WithConfig(cfg *aws.Config) { - c.mu.Lock() - defer c.mu.Unlock() - if c.config == nil { - c.config = cfg - } -} - -// getLoginAuth obtains authentication for ECR given the -// region (taken from the image). This assumes that the pod has -// IAM permissions to get an authentication token, which will usually -// be the case if it's running in EKS, and may need additional setup -// otherwise (visit https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/ -// as a starting point). -func (c *Client) getLoginAuth(ctx context.Context, awsEcrRegion string) (authn.AuthConfig, time.Time, error) { - var authConfig authn.AuthConfig - var cfg aws.Config - - c.mu.Lock() - if c.config != nil { - cfg = c.config.Copy() - } else { - var confOpts []func(*config.LoadOptions) error - confOpts = append(confOpts, config.WithRegion(awsEcrRegion)) - if c.proxyURL != nil { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.Proxy = http.ProxyURL(c.proxyURL) - confOpts = append(confOpts, config.WithHTTPClient(&http.Client{Transport: transport})) - } - - var err error - cfg, err = config.LoadDefaultConfig(ctx, confOpts...) - if err != nil { - c.mu.Unlock() - return authConfig, time.Time{}, fmt.Errorf("failed to load default configuration: %w", err) - } - c.config = &cfg - } - c.mu.Unlock() - - ecrService := ecr.NewFromConfig(cfg) - // NOTE: ecr.GetAuthorizationTokenInput has deprecated RegistryIds. Hence, - // pass nil input. - ecrToken, err := ecrService.GetAuthorizationToken(ctx, nil) - if err != nil { - return authConfig, time.Time{}, err - } - - // Validate the authorization data. - if len(ecrToken.AuthorizationData) == 0 { - return authConfig, time.Time{}, errors.New("no authorization data") - } - if ecrToken.AuthorizationData[0].AuthorizationToken == nil { - return authConfig, time.Time{}, fmt.Errorf("no authorization token") - } - token, err := base64.StdEncoding.DecodeString(*ecrToken.AuthorizationData[0].AuthorizationToken) - if err != nil { - return authConfig, time.Time{}, err - } - - tokenSplit := strings.Split(string(token), ":") - // Validate the tokens. - if len(tokenSplit) != 2 { - return authConfig, time.Time{}, fmt.Errorf("invalid authorization token, expected the token to have two parts separated by ':', got %d parts", len(tokenSplit)) - } - authConfig = authn.AuthConfig{ - Username: tokenSplit[0], - Password: tokenSplit[1], - } - expiresAt := ecrToken.AuthorizationData[0].ExpiresAt - if expiresAt == nil { - expiresAt = &time.Time{} - } - return authConfig, *expiresAt, nil -} - -// LoginWithExpiry attempts to get the authentication material for ECR. -// It returns the authentication material and the expiry time of the token. -func (c *Client) LoginWithExpiry(ctx context.Context, autoLogin bool, image string) (authn.Authenticator, time.Time, error) { - if autoLogin { - logr.FromContextOrDiscard(ctx).Info("logging in to AWS ECR for " + image) - _, awsEcrRegion, ok := ParseRegistry(image) - if !ok { - return nil, time.Time{}, errors.New("failed to parse AWS ECR image, invalid ECR image") - } - - authConfig, expiresAt, err := c.getLoginAuth(ctx, awsEcrRegion) - if err != nil { - return nil, time.Time{}, err - } - - auth := authn.FromConfig(authConfig) - return auth, expiresAt, nil - } - return nil, time.Time{}, fmt.Errorf("ECR authentication failed: %w", oci.ErrUnconfiguredProvider) -} - -// Login attempts to get the authentication material for ECR. -func (c *Client) Login(ctx context.Context, autoLogin bool, image string) (authn.Authenticator, error) { - auth, _, err := c.LoginWithExpiry(ctx, autoLogin, image) - return auth, err -} - -// OIDCLogin attempts to get the authentication material for ECR. -// -// Deprecated: Use LoginWithExpiry instead. -func (c *Client) OIDCLogin(ctx context.Context, registryURL string) (authn.Authenticator, error) { - _, awsEcrRegion, ok := ParseRegistry(registryURL) - if !ok { - return nil, errors.New("failed to parse AWS ECR image, invalid ECR image") - } - - authConfig, _, err := c.getLoginAuth(ctx, awsEcrRegion) - if err != nil { - return nil, err - } - - auth := authn.FromConfig(authConfig) - return auth, nil -} diff --git a/oci/auth/aws/auth_test.go b/oci/auth/aws/auth_test.go deleted file mode 100644 index 396a5324f..000000000 --- a/oci/auth/aws/auth_test.go +++ /dev/null @@ -1,283 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package aws - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/google/go-containerregistry/pkg/authn" - . "github.com/onsi/gomega" -) - -const ( - testValidECRImage = "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1" -) - -func TestParseRegistry(t *testing.T) { - tests := []struct { - registry string - wantAccountID string - wantRegion string - wantOK bool - }{ - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "https://012345678901.dkr.ecr.us-east-1.amazonaws.com/v2/part/part", - wantAccountID: "012345678901", - wantRegion: "us-east-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.cn-north-1.amazonaws.com.cn/foo", - wantAccountID: "012345678901", - wantRegion: "cn-north-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr-fips.us-gov-west-1.amazonaws.com", - wantAccountID: "012345678901", - wantRegion: "us-gov-west-1", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-secret-region.sc2s.sgov.gov", - wantAccountID: "012345678901", - wantRegion: "us-secret-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr-fips.us-ts-region.c2s.ic.gov", - wantAccountID: "012345678901", - wantRegion: "us-ts-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.uk-region.cloud.adc-e.uk", - wantAccountID: "012345678901", - wantRegion: "uk-region", - wantOK: true, - }, - { - registry: "012345678901.dkr.ecr.us-ts-region.csp.hci.ic.gov", - wantAccountID: "012345678901", - wantRegion: "us-ts-region", - wantOK: true, - }, - // TODO: Fix: this invalid registry is allowed by the regex. - // { - // registry: ".dkr.ecr.error.amazonaws.com", - // wantOK: false, - // }, - { - registry: "gcr.io/foo/bar:baz", - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.registry, func(t *testing.T) { - g := NewWithT(t) - - accId, region, ok := ParseRegistry(tt.registry) - g.Expect(ok).To(Equal(tt.wantOK), "unexpected OK") - g.Expect(accId).To(Equal(tt.wantAccountID), "unexpected account IDs") - g.Expect(region).To(Equal(tt.wantRegion), "unexpected regions") - }) - } -} - -func TestGetLoginAuth(t *testing.T) { - authorizationData := fmt.Sprintf(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ=","expiresAt": %d}]}`, time.Now().Add(1*time.Hour).Unix()) - tests := []struct { - name string - responseBody []byte - statusCode int - wantErr bool - wantAuthConfig authn.AuthConfig - }{ - { - // NOTE: The authorizationToken is base64 encoded. - name: "success", - responseBody: []byte(authorizationData), - statusCode: http.StatusOK, - wantAuthConfig: authn.AuthConfig{ - Username: "some-key", - Password: "some-secret", - }, - }, - { - name: "fail", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid token", - responseBody: []byte(`{ - "authorizationData": [ - { - "authorizationToken": "c29tZS10b2tlbg==" - } - ] -}`), - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "invalid data", - responseBody: []byte(`{ - "authorizationData": [ - { - "foo": "bar" - } - ] -}`), - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "invalid response", - responseBody: []byte(`{}`), - statusCode: http.StatusOK, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - // Configure test client. - ec := NewClient() - cfg := aws.NewConfig() - cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { - return aws.Endpoint{URL: srv.URL}, nil - }) - // set the region in the config since we are not using the `LoadDefaultConfig` function that sets the region - // by querying the instance metadata service(IMDS) - cfg.Credentials = credentials.NewStaticCredentialsProvider("x", "y", "z") - ec.WithConfig(cfg) - - a, expiresAt, err := ec.getLoginAuth(context.TODO(), "us-east-1") - g.Expect(err != nil).To(Equal(tt.wantErr)) - if !tt.wantErr { - g.Expect(expiresAt).To(BeTemporally("~", time.Now().Add(1*time.Hour), time.Second)) - } - if tt.statusCode == http.StatusOK { - g.Expect(a).To(Equal(tt.wantAuthConfig)) - } - }) - } -} - -func TestLogin(t *testing.T) { - tests := []struct { - name string - autoLogin bool - image string - statusCode int - testOIDC bool - wantErr bool - }{ - { - name: "no auto login", - autoLogin: false, - image: testValidECRImage, - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "with auto login", - autoLogin: true, - image: testValidECRImage, - statusCode: http.StatusOK, - testOIDC: true, - }, - { - name: "login failure", - autoLogin: true, - image: testValidECRImage, - statusCode: http.StatusInternalServerError, - testOIDC: true, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ="}]}`)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - // Configure test client. - ecrClient := NewClient() - cfg := aws.NewConfig() - cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { - return aws.Endpoint{URL: srv.URL}, nil - }) - cfg.Credentials = credentials.NewStaticCredentialsProvider("x", "y", "z") - ecrClient.WithConfig(cfg) - - _, err := ecrClient.Login(context.TODO(), tt.autoLogin, tt.image) - g.Expect(err != nil).To(Equal(tt.wantErr)) - - if tt.testOIDC { - _, err = ecrClient.OIDCLogin(context.TODO(), tt.image) - g.Expect(err != nil).To(Equal(tt.wantErr)) - } - }) - } -} diff --git a/oci/auth/azure/auth.go b/oci/auth/azure/auth.go deleted file mode 100644 index f085ba027..000000000 --- a/oci/auth/azure/auth.go +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package azure - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - _ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - - "github.com/fluxcd/pkg/oci" -) - -// Default cache expiration time in seconds for ACR refresh token. -// TODO @souleb: This is copied from https://github.com/Azure/msi-acrpull/blob/0ca921a7740e561c7204d9c3b3b55c4e0b9bd7b9/pkg/authorizer/token_retriever.go#L21C2-L21C39 -// as it is not provided by the Azure SDK. Check with the Azure SDK team to see if there is a better way to get this value. -const defaultCacheExpirationInSeconds = 600 - -// Client is an Azure ACR client which can log into the registry and return -// authorization information. -type Client struct { - credential azcore.TokenCredential - scheme string - proxyURL *url.URL -} - -// Option is a functional option for configuring the client. -type Option func(*Client) - -// WithProxyURL sets the proxy URL for the client. -func WithProxyURL(proxyURL *url.URL) Option { - return func(c *Client) { - c.proxyURL = proxyURL - } -} - -// NewClient creates a new ACR client with default configurations. -func NewClient(opts ...Option) *Client { - client := &Client{scheme: "https"} - for _, opt := range opts { - opt(client) - } - return client -} - -// WithTokenCredential sets the token credential used by the ACR client. -func (c *Client) WithTokenCredential(tc azcore.TokenCredential) *Client { - c.credential = tc - return c -} - -// WithScheme sets the scheme of the http request that the client makes. -func (c *Client) WithScheme(scheme string) *Client { - c.scheme = scheme - return c -} - -// getLoginAuth returns authentication for ACR. The details needed for authentication -// are gotten from environment variable so there is no need to mount a host path. -// The endpoint is the registry server and will be queried for OAuth authorization token. -func (c *Client) getLoginAuth(ctx context.Context, registryURL string) (authn.AuthConfig, time.Time, error) { - var authConfig authn.AuthConfig - - // Use default credentials if no token credential is provided. - // NOTE: NewDefaultAzureCredential() performs a lot of environment lookup - // for creating default token credential. Load it only when it's needed. - if c.credential == nil { - opts := &azidentity.DefaultAzureCredentialOptions{} - if c.proxyURL != nil { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.Proxy = http.ProxyURL(c.proxyURL) - opts.Transport = &http.Client{Transport: transport} - } - - cred, err := azidentity.NewDefaultAzureCredential(opts) - if err != nil { - return authConfig, time.Time{}, err - } - c.credential = cred - } - - configurationEnvironment := getCloudConfiguration(registryURL) - // Obtain access token using the token credential. - armToken, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{ - Scopes: []string{configurationEnvironment.Services[cloud.ResourceManager].Endpoint + "/" + ".default"}, - }) - if err != nil { - return authConfig, time.Time{}, err - } - - // Obtain ACR access token using exchanger. - ex := newExchanger(registryURL, c.proxyURL) - accessToken, err := ex.ExchangeACRAccessToken(string(armToken.Token)) - if err != nil { - return authConfig, time.Time{}, fmt.Errorf("error exchanging token: %w", err) - } - - expiresAt := time.Now().Add(defaultCacheExpirationInSeconds * time.Second) - - return authn.AuthConfig{ - // This is the acr username used by Azure - // See documentation: https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#az-acr-login-with---expose-token - Username: "00000000-0000-0000-0000-000000000000", - Password: accessToken, - }, expiresAt, nil -} - -// getCloudConfiguration returns the cloud configuration based on the registry URL. -// List from https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/containers/azcontainerregistry/cloud_config.go#L16 -func getCloudConfiguration(url string) cloud.Configuration { - switch { - case strings.HasSuffix(url, ".azurecr.cn"): - return cloud.AzureChina - case strings.HasSuffix(url, ".azurecr.us"): - return cloud.AzureGovernment - default: - return cloud.AzurePublic - } -} - -// ValidHost returns if a given host is a Azure container registry. -// List from https://github.com/kubernetes/kubernetes/blob/v1.23.1/pkg/credentialprovider/azure/azure_credentials.go#L55 -func ValidHost(host string) bool { - for _, v := range []string{".azurecr.io", ".azurecr.cn", ".azurecr.de", ".azurecr.us"} { - if strings.HasSuffix(host, v) { - return true - } - } - return false -} - -// LoginWithExpiry attempts to get the authentication material for ACR. -// It returns the authentication material and the expiry time of the token. -// The caller can ensure that the passed image is a valid ACR image using ValidHost(). -func (c *Client) LoginWithExpiry(ctx context.Context, autoLogin bool, image string, ref name.Reference) (authn.Authenticator, time.Time, error) { - if autoLogin { - logr.FromContextOrDiscard(ctx).Info("logging in to Azure ACR for " + image) - // get registry host from image - strArr := strings.SplitN(image, "/", 2) - endpoint := fmt.Sprintf("%s://%s", c.scheme, strArr[0]) - authConfig, expiresAt, err := c.getLoginAuth(ctx, endpoint) - if err != nil { - logr.FromContextOrDiscard(ctx).Info("error logging into ACR " + err.Error()) - return nil, time.Time{}, err - } - - auth := authn.FromConfig(authConfig) - return auth, expiresAt, nil - } - return nil, time.Time{}, fmt.Errorf("ACR authentication failed: %w", oci.ErrUnconfiguredProvider) -} - -// Login attempts to get the authentication material for ACR. The caller can -// ensure that the passed image is a valid ACR image using ValidHost(). -func (c *Client) Login(ctx context.Context, autoLogin bool, image string, ref name.Reference) (authn.Authenticator, error) { - auth, _, err := c.LoginWithExpiry(ctx, autoLogin, image, ref) - return auth, err -} - -// OIDCLogin attempts to get an Authenticator for the provided ACR registry URL endpoint. -// -// If you want to construct an Authenticator based on an image reference, -// you may want to use Login instead. -// -// Deprecated: Use LoginWithExpiry instead. -func (c *Client) OIDCLogin(ctx context.Context, registryUrl string) (authn.Authenticator, error) { - authConfig, _, err := c.getLoginAuth(ctx, registryUrl) - if err != nil { - logr.FromContextOrDiscard(ctx).Info("error logging into ACR " + err.Error()) - return nil, err - } - - auth := authn.FromConfig(authConfig) - return auth, nil -} diff --git a/oci/auth/azure/auth_test.go b/oci/auth/azure/auth_test.go deleted file mode 100644 index f17071336..000000000 --- a/oci/auth/azure/auth_test.go +++ /dev/null @@ -1,203 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package azure - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "net/url" - "path" - "testing" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - . "github.com/onsi/gomega" -) - -func TestGetAzureLoginAuth(t *testing.T) { - tests := []struct { - name string - tokenCredential azcore.TokenCredential - responseBody string - statusCode int - wantErr bool - wantAuthConfig authn.AuthConfig - }{ - { - name: "success", - tokenCredential: &FakeTokenCredential{Token: "foo"}, - responseBody: `{"refresh_token": "bbbbb"}`, - statusCode: http.StatusOK, - wantAuthConfig: authn.AuthConfig{ - Username: "00000000-0000-0000-0000-000000000000", - Password: "bbbbb", - }, - }, - { - name: "fail to get access token", - tokenCredential: &FakeTokenCredential{Err: errors.New("no access token")}, - wantErr: true, - }, - { - name: "error from exchange service", - tokenCredential: &FakeTokenCredential{Token: "foo"}, - responseBody: `[{"code": "111","message": "error message 1"}]`, - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - // Run a test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - // Configure new client with test token credential. - c := NewClient(). - WithTokenCredential(tt.tokenCredential). - WithScheme("http") - - auth, expiresAt, err := c.getLoginAuth(context.TODO(), srv.URL) - g.Expect(err != nil).To(Equal(tt.wantErr)) - if !tt.wantErr { - g.Expect(expiresAt).To(BeTemporally("~", time.Now().Add(defaultCacheExpirationInSeconds*time.Second), time.Second)) - } - if tt.statusCode == http.StatusOK { - g.Expect(auth).To(Equal(tt.wantAuthConfig)) - } - }) - } -} - -func TestValidHost(t *testing.T) { - tests := []struct { - host string - result bool - }{ - {"foo.azurecr.io", true}, - {"foo.azurecr.cn", true}, - {"foo.azurecr.de", true}, - {"foo.azurecr.us", true}, - {"gcr.io", false}, - {"docker.io", false}, - } - - for _, tt := range tests { - t.Run(tt.host, func(t *testing.T) { - g := NewWithT(t) - g.Expect(ValidHost(tt.host)).To(Equal(tt.result)) - }) - } -} - -func TestGetCloudConfiguration(t *testing.T) { - tests := []struct { - host string - result cloud.Configuration - }{ - {"foo.azurecr.io", cloud.AzurePublic}, - {"foo.azurecr.cn", cloud.AzureChina}, - {"foo.azurecr.de", cloud.AzurePublic}, - {"foo.azurecr.us", cloud.AzureGovernment}, - } - - for _, tt := range tests { - t.Run(tt.host, func(t *testing.T) { - g := NewWithT(t) - g.Expect(getCloudConfiguration(tt.host)).To(Equal(tt.result)) - }) - } -} - -func TestLogin(t *testing.T) { - tests := []struct { - name string - autoLogin bool - statusCode int - testOIDC bool - wantErr bool - }{ - { - name: "no auto login", - autoLogin: false, - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "with auto login", - autoLogin: true, - testOIDC: true, - statusCode: http.StatusOK, - }, - { - name: "login failure", - autoLogin: true, - statusCode: http.StatusInternalServerError, - testOIDC: true, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - // Run a test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(`{"refresh_token": "bbbbb"}`)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - // Construct an image repo name against the test server. - u, err := url.Parse(srv.URL) - g.Expect(err).ToNot(HaveOccurred()) - image := path.Join(u.Host, "foo/bar:v1") - ref, err := name.ParseReference(image) - g.Expect(err).ToNot(HaveOccurred()) - - ac := NewClient(). - WithTokenCredential(&FakeTokenCredential{Token: "foo"}). - WithScheme("http") - - _, err = ac.Login(context.TODO(), tt.autoLogin, u.Host, ref) - g.Expect(err != nil).To(Equal(tt.wantErr)) - - if tt.testOIDC { - _, err = ac.OIDCLogin(context.TODO(), srv.URL) - g.Expect(err != nil).To(Equal(tt.wantErr)) - } - }) - } -} diff --git a/oci/auth/azure/exchanger.go b/oci/auth/azure/exchanger.go deleted file mode 100644 index 14c475050..000000000 --- a/oci/auth/azure/exchanger.go +++ /dev/null @@ -1,133 +0,0 @@ -/* -MIT License - -Copyright (c) Microsoft Corporation. - -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 -*/ - -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -This package exchanges an ARM access token for an ACR access token on Azure -It has been derived from -https://github.com/Azure/msi-acrpull/blob/main/pkg/authorizer/token_exchanger.go -since the project isn't actively maintained. -*/ - -package azure - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "path" -) - -type tokenResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - Resource string `json:"resource"` - TokenType string `json:"token_type"` -} - -type acrError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -type exchanger struct { - endpoint string - proxyURL *url.URL -} - -// newExchanger returns an Azure Exchanger for Azure Container Registry with -// a given endpoint, for example https://azurecr.io. -func newExchanger(endpoint string, proxyURL *url.URL) *exchanger { - return &exchanger{ - endpoint: endpoint, - proxyURL: proxyURL, - } -} - -// ExchangeACRAccessToken exchanges an access token for a refresh token with the -// exchange service. -func (e *exchanger) ExchangeACRAccessToken(armToken string) (string, error) { - // Construct the exchange URL. - exchangeURL, err := url.Parse(e.endpoint) - if err != nil { - return "", err - } - exchangeURL.Path = path.Join(exchangeURL.Path, "oauth2/exchange") - - httpClient := &http.Client{} - - if e.proxyURL != nil { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.Proxy = http.ProxyURL(e.proxyURL) - httpClient.Transport = transport - } - - parameters := url.Values{} - parameters.Add("grant_type", "access_token") - parameters.Add("service", exchangeURL.Hostname()) - parameters.Add("access_token", armToken) - - resp, err := httpClient.PostForm(exchangeURL.String(), parameters) - if err != nil { - return "", fmt.Errorf("failed to send token exchange request: %w", err) - } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read the body of the response: %w", err) - } - if resp.StatusCode != http.StatusOK { - // Parse the error response. - var errors []acrError - if err = json.Unmarshal(b, &errors); err == nil { - return "", fmt.Errorf("unexpected status code %d from exchange request: %s", - resp.StatusCode, errors) - } - - // Error response could not be parsed, return a generic error. - return "", fmt.Errorf("unexpected status code %d from exchange request, response body: %s", - resp.StatusCode, string(b)) - } - - var tokenResp tokenResponse - if err = json.Unmarshal(b, &tokenResp); err != nil { - return "", fmt.Errorf("failed to decode the response: %w, response body: %s", err, string(b)) - } - return tokenResp.RefreshToken, nil -} diff --git a/oci/auth/azure/exchanger_test.go b/oci/auth/azure/exchanger_test.go deleted file mode 100644 index 89209bb3f..000000000 --- a/oci/auth/azure/exchanger_test.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package azure - -import ( - "net/http" - "net/http/httptest" - "testing" - - . "github.com/onsi/gomega" -) - -func TestExchanger_ExchangeACRAccessToken(t *testing.T) { - tests := []struct { - name string - responseBody string - statusCode int - wantErr bool - wantToken string - }{ - { - name: "successful", - responseBody: `{ - "access_token": "aaaaa", - "refresh_token": "bbbbb", - "resource": "ccccc", - "token_type": "ddddd" -}`, - statusCode: http.StatusOK, - wantToken: "bbbbb", - }, - { - name: "fail", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid response", - responseBody: "foo", - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "error response", - responseBody: `[ - { - "code": "111", - "message": "error message 1" - }, - { - "code": "112", - "message": "error message 2" - } -]`, - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - ex := newExchanger(srv.URL, nil /*proxyURL*/) - token, err := ex.ExchangeACRAccessToken("some-access-token") - g.Expect(err != nil).To(Equal(tt.wantErr)) - if tt.statusCode == http.StatusOK { - g.Expect(token).To(Equal(tt.wantToken)) - } - }) - } -} diff --git a/oci/auth/azure/fake.go b/oci/auth/azure/fake.go deleted file mode 100644 index 1719abeba..000000000 --- a/oci/auth/azure/fake.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package azure - -import ( - "context" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" -) - -type FakeTokenCredential struct { - Token string - ExpiresOn time.Time - Err error -} - -func (tc *FakeTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { - if tc.Err != nil { - return azcore.AccessToken{}, tc.Err - } - return azcore.AccessToken{Token: tc.Token, ExpiresOn: tc.ExpiresOn}, nil -} diff --git a/oci/auth/gcp/auth.go b/oci/auth/gcp/auth.go deleted file mode 100644 index 584f0b256..000000000 --- a/oci/auth/gcp/auth.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gcp - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - - "github.com/fluxcd/pkg/oci" -) - -type gceToken struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` -} - -// GCP_TOKEN_URL is the default GCP metadata endpoint used for authentication. -const GCP_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" - -// ValidHost returns if a given host is a valid GCR host. -func ValidHost(host string) bool { - return host == "gcr.io" || strings.HasSuffix(host, ".gcr.io") || strings.HasSuffix(host, "-docker.pkg.dev") -} - -// Client is a GCP GCR client which can log into the registry and return -// authorization information. -type Client struct { - tokenURL string - proxyURL *url.URL -} - -// Option is a functional option for configuring the client. -type Option func(*Client) - -// WithProxyURL sets the proxy URL for the client. -func WithProxyURL(proxyURL *url.URL) Option { - return func(c *Client) { - c.proxyURL = proxyURL - } -} - -// NewClient creates a new GCR client with default configurations. -func NewClient(opts ...Option) *Client { - client := &Client{tokenURL: GCP_TOKEN_URL} - for _, opt := range opts { - opt(client) - } - return client -} - -// WithTokenURL sets the token URL used by the GCR client. -func (c *Client) WithTokenURL(url string) *Client { - c.tokenURL = url - return c -} - -// getLoginAuth obtains authentication by getting a token from the metadata API -// on GCP. This assumes that the pod has right to pull the image which would be -// the case if it is hosted on GCP. It works with both service account and -// workload identity enabled clusters. -func (c *Client) getLoginAuth(ctx context.Context) (authn.AuthConfig, time.Time, error) { - var authConfig authn.AuthConfig - - request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.tokenURL, nil) - if err != nil { - return authConfig, time.Time{}, err - } - - request.Header.Add("Metadata-Flavor", "Google") - - var transport http.RoundTripper - if c.proxyURL != nil { - t := http.DefaultTransport.(*http.Transport).Clone() - t.Proxy = http.ProxyURL(c.proxyURL) - transport = t - } - - client := &http.Client{Transport: transport} - response, err := client.Do(request) - if err != nil { - return authConfig, time.Time{}, err - } - defer response.Body.Close() - defer io.Copy(io.Discard, response.Body) - - if response.StatusCode != http.StatusOK { - return authConfig, time.Time{}, fmt.Errorf("unexpected status from metadata service: %s", response.Status) - } - - var accessToken gceToken - decoder := json.NewDecoder(response.Body) - if err := decoder.Decode(&accessToken); err != nil { - return authConfig, time.Time{}, err - } - - authConfig = authn.AuthConfig{ - Username: "oauth2accesstoken", - Password: accessToken.AccessToken, - } - - // add expiresIn seconds to the current time to get the expiry time - expiresAt := time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second) - - return authConfig, expiresAt, nil -} - -// Login attempts to get the authentication material for GCR. -// It returns the authentication material and the expiry time of the token. -// The caller can ensure that the passed image is a valid GCR image using ValidHost(). -func (c *Client) LoginWithExpiry(ctx context.Context, autoLogin bool, image string, ref name.Reference) (authn.Authenticator, time.Time, error) { - if autoLogin { - logr.FromContextOrDiscard(ctx).Info("logging in to GCP GCR for " + image) - authConfig, expiresAt, err := c.getLoginAuth(ctx) - if err != nil { - logr.FromContextOrDiscard(ctx).Info("error logging into GCP " + err.Error()) - return nil, time.Time{}, err - } - - auth := authn.FromConfig(authConfig) - return auth, expiresAt, nil - } - return nil, time.Time{}, fmt.Errorf("GCR authentication failed: %w", oci.ErrUnconfiguredProvider) -} - -// Login attempts to get the authentication material for GCR. The caller can -// ensure that the passed image is a valid GCR image using ValidHost(). -func (c *Client) Login(ctx context.Context, autoLogin bool, image string, ref name.Reference) (authn.Authenticator, error) { - auth, _, err := c.LoginWithExpiry(ctx, autoLogin, image, ref) - return auth, err -} - -// OIDCLogin attempts to get the authentication material for GCR from the token url set in the client. -// -// Deprecated: Use LoginWithExpiry instead. -func (c *Client) OIDCLogin(ctx context.Context) (authn.Authenticator, error) { - authConfig, _, err := c.getLoginAuth(ctx) - if err != nil { - logr.FromContextOrDiscard(ctx).Info("error logging into GCP " + err.Error()) - return nil, err - } - - auth := authn.FromConfig(authConfig) - return auth, nil -} diff --git a/oci/auth/gcp/auth_test.go b/oci/auth/gcp/auth_test.go deleted file mode 100644 index 185c34f69..000000000 --- a/oci/auth/gcp/auth_test.go +++ /dev/null @@ -1,178 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gcp - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - . "github.com/onsi/gomega" -) - -const testValidGCRImage = "gcr.io/foo/bar:v1" - -func TestGetLoginAuth(t *testing.T) { - tests := []struct { - name string - responseBody string - statusCode int - wantErr bool - wantAuthConfig authn.AuthConfig - }{ - { - name: "success", - responseBody: `{ - "access_token": "some-token", - "expires_in": 10, - "token_type": "foo" -}`, - statusCode: http.StatusOK, - wantAuthConfig: authn.AuthConfig{ - Username: "oauth2accesstoken", - Password: "some-token", - }, - }, - { - name: "fail", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid response", - responseBody: "foo", - statusCode: http.StatusOK, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - gc := NewClient().WithTokenURL(srv.URL) - a, expiresAt, err := gc.getLoginAuth(context.TODO()) - g.Expect(err != nil).To(Equal(tt.wantErr)) - if !tt.wantErr { - g.Expect(expiresAt).To(BeTemporally("~", time.Now().Add(10*time.Second), time.Second)) - } - if tt.statusCode == http.StatusOK { - g.Expect(a).To(Equal(tt.wantAuthConfig)) - } - }) - } -} - -func TestValidHost(t *testing.T) { - tests := []struct { - host string - result bool - }{ - {"gcr.io", true}, - {"foo.gcr.io", true}, - {"foo-docker.pkg.dev", true}, - {"docker.io", false}, - } - - for _, tt := range tests { - t.Run(tt.host, func(t *testing.T) { - g := NewWithT(t) - g.Expect(ValidHost(tt.host)).To(Equal(tt.result)) - }) - } -} - -func TestLogin(t *testing.T) { - tests := []struct { - name string - autoLogin bool - image string - statusCode int - testOIDC bool - wantErr bool - }{ - { - name: "no auto login", - autoLogin: false, - image: testValidGCRImage, - statusCode: http.StatusOK, - wantErr: true, - }, - { - name: "with auto login", - autoLogin: true, - image: testValidGCRImage, - testOIDC: true, - statusCode: http.StatusOK, - }, - { - name: "login failure", - autoLogin: true, - image: testValidGCRImage, - statusCode: http.StatusInternalServerError, - testOIDC: true, - wantErr: true, - }, - { - name: "non GCR image", - autoLogin: true, - image: "foo/bar:v1", - statusCode: http.StatusOK, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(`{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - ref, err := name.ParseReference(tt.image) - g.Expect(err).ToNot(HaveOccurred()) - - gc := NewClient().WithTokenURL(srv.URL) - - _, err = gc.Login(context.TODO(), tt.autoLogin, tt.image, ref) - g.Expect(err != nil).To(Equal(tt.wantErr)) - - if tt.testOIDC { - _, err = gc.OIDCLogin(context.TODO()) - g.Expect(err != nil).To(Equal(tt.wantErr)) - } - }) - } -} diff --git a/oci/auth/login/login.go b/oci/auth/login/login.go deleted file mode 100644 index 60f319687..000000000 --- a/oci/auth/login/login.go +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package login - -import ( - "context" - "fmt" - "net/url" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - - "github.com/fluxcd/pkg/oci" - "github.com/fluxcd/pkg/oci/auth/aws" - "github.com/fluxcd/pkg/oci/auth/azure" - "github.com/fluxcd/pkg/oci/auth/gcp" -) - -// ImageRegistryProvider analyzes the provided registry and returns the identified -// container image registry provider. -func ImageRegistryProvider(url string, ref name.Reference) oci.Provider { - // If the url is a repository root address, use it to analyze. Else, derive - // the registry from the name reference. - // NOTE: This is because name.Reference of a repository root assumes that - // the reference is an image name and defaults to using index.docker.io as - // the registry host. - addr := strings.TrimSuffix(url, "/") - if strings.ContainsRune(addr, '/') { - addr = ref.Context().RegistryStr() - } - - _, _, ok := aws.ParseRegistry(addr) - if ok { - return oci.ProviderAWS - } - if gcp.ValidHost(addr) { - return oci.ProviderGCP - } - if azure.ValidHost(addr) { - return oci.ProviderAzure - } - return oci.ProviderGeneric -} - -// ProviderOptions contains options for registry provider login. -type ProviderOptions struct { - // AwsAutoLogin enables automatic attempt to get credentials for images in - // ECR. - AwsAutoLogin bool - // GcpAutoLogin enables automatic attempt to get credentials for images in - // GCP. - GcpAutoLogin bool - // AzureAutoLogin enables automatic attempt to get credentials for images in - // ACR. - AzureAutoLogin bool -} - -// Manager is a login manager for various registry providers. -type Manager struct { - ecr *aws.Client - gcr *gcp.Client - acr *azure.Client -} - -// Option is a functional option for configuring the manager. -type Option func(*options) - -type options struct { - proxyURL *url.URL -} - -// WithProxyURL sets the proxy URL for the manager. -func WithProxyURL(proxyURL *url.URL) Option { - return func(o *options) { - o.proxyURL = proxyURL - } -} - -// NewManager initializes a Manager with default registry clients -// configurations. -func NewManager(opts ...Option) *Manager { - var o options - for _, opt := range opts { - opt(&o) - } - - var awsOpts []aws.Option - var gcpOpts []gcp.Option - var azureOpts []azure.Option - - if o.proxyURL != nil { - awsOpts = append(awsOpts, aws.WithProxyURL(o.proxyURL)) - gcpOpts = append(gcpOpts, gcp.WithProxyURL(o.proxyURL)) - azureOpts = append(azureOpts, azure.WithProxyURL(o.proxyURL)) - } - - return &Manager{ - ecr: aws.NewClient(awsOpts...), - gcr: gcp.NewClient(gcpOpts...), - acr: azure.NewClient(azureOpts...), - } -} - -// WithECRClient allows overriding the default ECR client. -func (m *Manager) WithECRClient(c *aws.Client) *Manager { - m.ecr = c - return m -} - -// WithGCRClient allows overriding the default GCR client. -func (m *Manager) WithGCRClient(c *gcp.Client) *Manager { - m.gcr = c - return m -} - -// WithACRClient allows overriding the default ACR client. -func (m *Manager) WithACRClient(c *azure.Client) *Manager { - m.acr = c - return m -} - -// Login performs authentication against a registry and returns the Authenticator. -// For generic registry provider, it is no-op. -func (m *Manager) Login(ctx context.Context, url string, ref name.Reference, opts ProviderOptions) (authn.Authenticator, error) { - auth, _, err := m.LoginWithExpiry(ctx, url, ref, opts) - return auth, err -} - -// LoginWithExpiry performs authentication against a registry and returns the -// Authenticator along with the auth expiry time. -// For generic registry provider, it is no-op. -func (m *Manager) LoginWithExpiry(ctx context.Context, url string, ref name.Reference, opts ProviderOptions) (authn.Authenticator, time.Time, error) { - provider := ImageRegistryProvider(url, ref) - switch provider { - case oci.ProviderAWS: - return m.ecr.LoginWithExpiry(ctx, opts.AwsAutoLogin, url) - case oci.ProviderGCP: - return m.gcr.LoginWithExpiry(ctx, opts.GcpAutoLogin, url, ref) - case oci.ProviderAzure: - return m.acr.LoginWithExpiry(ctx, opts.AzureAutoLogin, url, ref) - } - return nil, time.Time{}, nil -} - -// OIDCLogin attempts to get an Authenticator for the provided URL endpoint. -// -// If you want to construct an Authenticator based on an image reference, -// you may want to use Login instead. -// -// Deprecated: Use Login instead. -func (m *Manager) OIDCLogin(ctx context.Context, registryURL string, opts ProviderOptions) (authn.Authenticator, error) { - u, err := url.Parse(registryURL) - if err != nil { - return nil, fmt.Errorf("unable to parse registry url: %w", err) - } - provider := ImageRegistryProvider(u.Host, nil) - switch provider { - case oci.ProviderAWS: - if !opts.AwsAutoLogin { - return nil, fmt.Errorf("ECR authentication failed: %w", oci.ErrUnconfiguredProvider) - } - logr.FromContextOrDiscard(ctx).Info("logging in to AWS ECR for " + u.Host) - return m.ecr.OIDCLogin(ctx, u.Host) - case oci.ProviderGCP: - if !opts.GcpAutoLogin { - return nil, fmt.Errorf("GCR authentication failed: %w", oci.ErrUnconfiguredProvider) - } - logr.FromContextOrDiscard(ctx).Info("logging in to GCP GCR for " + u.Host) - return m.gcr.OIDCLogin(ctx) - case oci.ProviderAzure: - if !opts.AzureAutoLogin { - return nil, fmt.Errorf("ACR authentication failed: %w", oci.ErrUnconfiguredProvider) - } - logr.FromContextOrDiscard(ctx).Info("logging in to Azure ACR for " + u.Host) - return m.acr.OIDCLogin(ctx, fmt.Sprintf("%s://%s", u.Scheme, u.Host)) - } - return nil, nil -} diff --git a/oci/auth/login/login_test.go b/oci/auth/login/login_test.go deleted file mode 100644 index b23be1dc9..000000000 --- a/oci/auth/login/login_test.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package login - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "testing" - - awssdk "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/google/go-containerregistry/pkg/name" - . "github.com/onsi/gomega" - - "github.com/fluxcd/pkg/oci" - "github.com/fluxcd/pkg/oci/auth/aws" - "github.com/fluxcd/pkg/oci/auth/azure" - "github.com/fluxcd/pkg/oci/auth/gcp" -) - -func TestImageRegistryProvider(t *testing.T) { - tests := []struct { - name string - image string - want oci.Provider - }{ - {"ecr", "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1", oci.ProviderAWS}, - {"ecr-root", "012345678901.dkr.ecr.us-east-1.amazonaws.com", oci.ProviderAWS}, - {"ecr-root with slash", "012345678901.dkr.ecr.us-east-1.amazonaws.com/", oci.ProviderAWS}, - {"gcr", "gcr.io/foo/bar:v1", oci.ProviderGCP}, - {"gcr-root", "gcr.io", oci.ProviderGCP}, - {"acr", "foo.azurecr.io/bar:v1", oci.ProviderAzure}, - {"acr-root", "foo.azurecr.io", oci.ProviderAzure}, - {"docker.io", "foo/bar:v1", oci.ProviderGeneric}, - {"docker.io-root", "docker.io", oci.ProviderGeneric}, - {"library", "alpine", oci.ProviderGeneric}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - // Trim suffix to allow parsing it as reference without modifying - // the given image address. - ref, err := name.ParseReference(strings.TrimSuffix(tt.image, "/")) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(ImageRegistryProvider(tt.image, ref)).To(Equal(tt.want)) - }) - } -} - -func TestLogin(t *testing.T) { - tests := []struct { - name string - responseBody string - statusCode int - providerOpts ProviderOptions - beforeFunc func(serverURL string, mgr *Manager, image *string) - wantErr bool - }{ - { - name: "ecr", - responseBody: `{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ="}]}`, - providerOpts: ProviderOptions{AwsAutoLogin: true}, - beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create ECR client and configure the manager. - ecrClient := aws.NewClient() - cfg := awssdk.NewConfig() - cfg.EndpointResolverWithOptions = awssdk.EndpointResolverWithOptionsFunc( - func(service, region string, options ...interface{}) (awssdk.Endpoint, error) { - return awssdk.Endpoint{URL: serverURL}, nil - }) - cfg.Credentials = credentials.NewStaticCredentialsProvider("x", "y", "z") - ecrClient.WithConfig(cfg) - - mgr.WithECRClient(ecrClient) - - *image = "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1" - }, - }, - { - name: "gcr", - responseBody: `{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`, - providerOpts: ProviderOptions{GcpAutoLogin: true}, - beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create GCR client and configure the manager. - gcrClient := gcp.NewClient().WithTokenURL(serverURL) - mgr.WithGCRClient(gcrClient) - - *image = "gcr.io/foo/bar:v1" - }, - }, - { - name: "acr", - responseBody: `{"refresh_token": "bbbbb"}`, - providerOpts: ProviderOptions{AzureAutoLogin: true}, - beforeFunc: func(serverURL string, mgr *Manager, image *string) { - acrClient := azure.NewClient().WithTokenCredential(&azure.FakeTokenCredential{Token: "foo"}).WithScheme("http") - mgr.WithACRClient(acrClient) - - *image = "foo.azurecr.io/bar:v1" - }, - // NOTE: This fails because the azure exchanger uses the image host - // to exchange token which can't be modified here without - // interfering image name based categorization of the login - // provider, that's actually being tested here. This is tested in - // detail in the azure package. - wantErr: true, - }, - { - name: "generic", - providerOpts: ProviderOptions{}, - beforeFunc: func(serverURL string, mgr *Manager, image *string) { - *image = "foo/bar:v1" - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - // Create test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.responseBody)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - - mgr := NewManager() - var image string - - if tt.beforeFunc != nil { - tt.beforeFunc(srv.URL, mgr, &image) - } - - ref, err := name.ParseReference(image) - g.Expect(err).ToNot(HaveOccurred()) - - _, err = mgr.Login(context.TODO(), image, ref, tt.providerOpts) - g.Expect(err != nil).To(Equal(tt.wantErr)) - }) - } -} diff --git a/oci/client/build.go b/oci/build.go similarity index 98% rename from oci/client/build.go rename to oci/build.go index e0a9163bc..b6a3ddb22 100644 --- a/oci/client/build.go +++ b/oci/build.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "archive/tar" @@ -26,7 +26,7 @@ import ( "strings" "time" - "github.com/fluxcd/pkg/oci/client/internal/fs" + "github.com/fluxcd/pkg/oci/internal/fs" "github.com/fluxcd/pkg/sourceignore" ) diff --git a/oci/client/build_test.go b/oci/build_test.go similarity index 99% rename from oci/client/build_test.go rename to oci/build_test.go index 9b17f5926..cddf188f6 100644 --- a/oci/client/build_test.go +++ b/oci/build_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "bytes" diff --git a/oci/client/client.go b/oci/client.go similarity index 95% rename from oci/client/client.go rename to oci/client.go index b3cd257a0..0a9625132 100644 --- a/oci/client/client.go +++ b/oci/client.go @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/v1/remote" - - "github.com/fluxcd/pkg/oci" ) // Client holds the options for accessing remote OCI registries. @@ -33,7 +31,7 @@ type Client struct { // NewClient returns an OCI client configured with the given crane options. func NewClient(opts []crane.Option) *Client { options := []crane.Option{ - crane.WithUserAgent(oci.UserAgent), + crane.WithUserAgent(UserAgent), } options = append(options, opts...) diff --git a/oci/constants.go b/oci/constants.go index c11f469d0..e27f9e28e 100644 --- a/oci/constants.go +++ b/oci/constants.go @@ -16,26 +16,6 @@ limitations under the License. package oci -// Provider is used to categorize the OCI registry providers. -type Provider int - -// Registry providers. -const ( - // ProviderGeneric is used to categorize registry provider that we don't - // support. - ProviderGeneric Provider = iota - ProviderAWS - ProviderGCP - ProviderAzure -) - -// Registry TLS transport config. -const ( - ClientCert = "certFile" - ClientKey = "keyFile" - CACert = "caFile" -) - const ( // SourceAnnotation is the OpenContainers annotation for specifying // the upstream source of an OCI artifact. diff --git a/oci/client/delete.go b/oci/delete.go similarity index 98% rename from oci/client/delete.go rename to oci/delete.go index a6b771abb..60370b660 100644 --- a/oci/client/delete.go +++ b/oci/delete.go @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" "fmt" + "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" ) diff --git a/oci/client/delete_test.go b/oci/delete_test.go similarity index 99% rename from oci/client/delete_test.go rename to oci/delete_test.go index aed4c839b..e1c140517 100644 --- a/oci/client/delete_test.go +++ b/oci/delete_test.go @@ -14,18 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" "fmt" + "testing" + "time" + "github.com/google/go-containerregistry/pkg/crane" gcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" . "github.com/onsi/gomega" - "testing" - "time" ) func TestDelete(t *testing.T) { diff --git a/oci/client/diff.go b/oci/diff.go similarity index 99% rename from oci/client/diff.go rename to oci/diff.go index 9d9229add..1bc5d9ac2 100644 --- a/oci/client/diff.go +++ b/oci/diff.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/diff_test.go b/oci/diff_test.go similarity index 99% rename from oci/client/diff_test.go rename to oci/diff_test.go index f39a26774..b7689f47d 100644 --- a/oci/client/diff_test.go +++ b/oci/diff_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/doc.go b/oci/doc.go index 3af362c15..4fe2d73d0 100644 --- a/oci/doc.go +++ b/oci/doc.go @@ -16,7 +16,6 @@ limitations under the License. // Package oci contains OCI registry related helpers for the registries offered // by the various cloud providers. It can be used to perform various operations -// like pushing, pulling and tagging artifacts, auto-login using the native -// authentication mechanism of the platform, etc. +// like pushing, pulling and tagging artifacts, etc. // +kubebuilder:object:generate=false package oci diff --git a/oci/go.mod b/oci/go.mod index c735f3846..1734b7856 100644 --- a/oci/go.mod +++ b/oci/go.mod @@ -3,42 +3,44 @@ module github.com/fluxcd/pkg/oci go 1.24.0 replace ( + github.com/fluxcd/pkg/auth => ../auth github.com/fluxcd/pkg/sourceignore => ../sourceignore github.com/fluxcd/pkg/tar => ../tar github.com/fluxcd/pkg/version => ../version ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 github.com/Masterminds/semver/v3 v3.3.0 - github.com/aws/aws-sdk-go-v2 v1.36.3 - github.com/aws/aws-sdk-go-v2/config v1.29.14 - github.com/aws/aws-sdk-go-v2/credentials v1.17.67 - github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 github.com/distribution/distribution/v3 v3.0.0 + github.com/fluxcd/pkg/auth v0.12.0 github.com/fluxcd/pkg/sourceignore v0.12.0 github.com/fluxcd/pkg/tar v0.12.0 github.com/fluxcd/pkg/version v0.7.0 - github.com/go-logr/logr v1.4.2 github.com/google/go-containerregistry v0.20.3 github.com/onsi/gomega v1.37.0 github.com/sirupsen/logrus v1.9.3 ) require ( + cloud.google.com/go/compute/metadata v0.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect - github.com/aws/smithy-go v1.22.3 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -54,12 +56,23 @@ require ( github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/pkg/cache v0.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.16.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect @@ -69,25 +82,30 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.1 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/vbatts/tar-split v0.11.6 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 // indirect @@ -113,14 +131,29 @@ require ( go.opentelemetry.io/proto/otlp v1.4.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.0 // indirect + k8s.io/apimachinery v0.33.0 // indirect + k8s.io/client-go v0.32.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/controller-runtime v0.20.4 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/oci/go.sum b/oci/go.sum index 1b8c2fe3f..5f4545ceb 100644 --- a/oci/go.sum +++ b/oci/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= @@ -40,8 +42,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0c github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= -github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -61,9 +63,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -86,14 +91,24 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk= +github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -102,11 +117,23 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -114,12 +141,17 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -138,28 +170,42 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -182,16 +228,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= -github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= @@ -212,17 +258,27 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= @@ -271,31 +327,63 @@ go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1 go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= @@ -308,6 +396,10 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -318,3 +410,28 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/oci/client/internal/fs/LICENSE b/oci/internal/fs/LICENSE similarity index 100% rename from oci/client/internal/fs/LICENSE rename to oci/internal/fs/LICENSE diff --git a/oci/client/internal/fs/fs.go b/oci/internal/fs/fs.go similarity index 100% rename from oci/client/internal/fs/fs.go rename to oci/internal/fs/fs.go diff --git a/oci/client/internal/fs/fs_test.go b/oci/internal/fs/fs_test.go similarity index 100% rename from oci/client/internal/fs/fs_test.go rename to oci/internal/fs/fs_test.go diff --git a/oci/client/internal/fs/rename.go b/oci/internal/fs/rename.go similarity index 100% rename from oci/client/internal/fs/rename.go rename to oci/internal/fs/rename.go diff --git a/oci/client/internal/fs/rename_windows.go b/oci/internal/fs/rename_windows.go similarity index 100% rename from oci/client/internal/fs/rename_windows.go rename to oci/internal/fs/rename_windows.go diff --git a/oci/client/internal/fs/testdata/test.file b/oci/internal/fs/testdata/test.file similarity index 100% rename from oci/client/internal/fs/testdata/test.file rename to oci/internal/fs/testdata/test.file diff --git a/oci/client/list.go b/oci/list.go similarity index 99% rename from oci/client/list.go rename to oci/list.go index 32fb82f5f..75c91817e 100644 --- a/oci/client/list.go +++ b/oci/list.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "bytes" diff --git a/oci/client/list_test.go b/oci/list_test.go similarity index 99% rename from oci/client/list_test.go rename to oci/list_test.go index fb14659e7..42e6b29da 100644 --- a/oci/client/list_test.go +++ b/oci/list_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/login.go b/oci/login.go similarity index 71% rename from oci/client/login.go rename to oci/login.go index fc8d674f6..cefd96b4c 100644 --- a/oci/client/login.go +++ b/oci/login.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" @@ -22,13 +22,10 @@ import ( "fmt" "strings" - "github.com/fluxcd/pkg/oci" - "github.com/fluxcd/pkg/oci/auth/aws" - "github.com/fluxcd/pkg/oci/auth/azure" - "github.com/fluxcd/pkg/oci/auth/gcp" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/name" + + authutils "github.com/fluxcd/pkg/auth/utils" ) // LoginWithCredentials configures the client with static credentials, accepts a single token @@ -64,30 +61,14 @@ func GetAuthFromCredentials(credentials string) (authn.Authenticator, error) { } // LoginWithProvider configures the client to log in to the specified provider -func (c *Client) LoginWithProvider(ctx context.Context, url string, provider oci.Provider) error { - var authenticator authn.Authenticator - var err error - - ref, err := name.ParseReference(url) +func (c *Client) LoginWithProvider(ctx context.Context, url string, provider string) error { + authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url) if err != nil { - return fmt.Errorf("could not create reference from url '%s': %w", url, err) + return fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err) } - - switch provider { - case oci.ProviderAWS: - authenticator, err = aws.NewClient().Login(ctx, true, url) - case oci.ProviderGCP: - authenticator, err = gcp.NewClient().Login(ctx, true, url, ref) - case oci.ProviderAzure: - authenticator, err = azure.NewClient().Login(ctx, true, url, ref) - default: + if authenticator == nil { return errors.New("unsupported provider") } - - if err != nil { - return fmt.Errorf("could not login to provider %v with url %s: %w", provider, url, err) - } - c.options = append(c.options, crane.WithAuth(authenticator)) return nil } diff --git a/oci/client/login_test.go b/oci/login_test.go similarity index 99% rename from oci/client/login_test.go rename to oci/login_test.go index 73219209c..6bb7acedc 100644 --- a/oci/client/login_test.go +++ b/oci/login_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/meta.go b/oci/meta.go similarity index 82% rename from oci/client/meta.go rename to oci/meta.go index 592f8234a..e706b9745 100644 --- a/oci/client/meta.go +++ b/oci/meta.go @@ -14,11 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client - -import ( - "github.com/fluxcd/pkg/oci" -) +package oci // Metadata holds the upstream information about on artifact's source. // https://github.com/opencontainers/image-spec/blob/main/annotations.md @@ -34,9 +30,9 @@ type Metadata struct { // ToAnnotations returns the OpenContainers annotations map. func (m *Metadata) ToAnnotations() map[string]string { annotations := map[string]string{ - oci.CreatedAnnotation: m.Created, - oci.SourceAnnotation: m.Source, - oci.RevisionAnnotation: m.Revision, + CreatedAnnotation: m.Created, + SourceAnnotation: m.Source, + RevisionAnnotation: m.Revision, } for k, v := range m.Annotations { @@ -49,9 +45,9 @@ func (m *Metadata) ToAnnotations() map[string]string { // MetadataFromAnnotations parses the OpenContainers annotations and returns a Metadata object. func MetadataFromAnnotations(annotations map[string]string) *Metadata { return &Metadata{ - Created: annotations[oci.CreatedAnnotation], - Source: annotations[oci.SourceAnnotation], - Revision: annotations[oci.RevisionAnnotation], + Created: annotations[CreatedAnnotation], + Source: annotations[SourceAnnotation], + Revision: annotations[RevisionAnnotation], Annotations: annotations, } } diff --git a/oci/client/pull.go b/oci/pull.go similarity index 99% rename from oci/client/pull.go rename to oci/pull.go index 633bf4b30..1674b74fd 100644 --- a/oci/client/pull.go +++ b/oci/pull.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "bufio" diff --git a/oci/client/pull_test.go b/oci/pull_test.go similarity index 95% rename from oci/client/pull_test.go rename to oci/pull_test.go index 86795284d..ee76a6f86 100644 --- a/oci/client/pull_test.go +++ b/oci/pull_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" @@ -22,7 +22,6 @@ import ( "path/filepath" "testing" - "github.com/fluxcd/pkg/oci" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -46,7 +45,7 @@ func Test_PullAnyTarball(t *testing.T) { g.Expect(build(artifact, testDir, nil)).To(Succeed()) img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) - img = mutate.ConfigMediaType(img, oci.CanonicalConfigMediaType) + img = mutate.ConfigMediaType(img, CanonicalConfigMediaType) layer, err := tarball.LayerFromFile(artifact, tarball.WithMediaType("application/vnd.acme.some.content.layer.v1.tar+gzip")) g.Expect(err).ToNot(HaveOccurred()) diff --git a/oci/client/push.go b/oci/push.go similarity index 94% rename from oci/client/push.go rename to oci/push.go index 8ed2cf6c7..43d7f432a 100644 --- a/oci/client/push.go +++ b/oci/push.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" @@ -31,8 +31,6 @@ import ( "github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" - - "github.com/fluxcd/pkg/oci" ) // LayerType is an enumeration of the supported layer types @@ -120,7 +118,7 @@ func (c *Client) Push(ctx context.Context, url, sourcePath string, opts ...PushO } img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) - img = mutate.ConfigMediaType(img, oci.CanonicalConfigMediaType) + img = mutate.ConfigMediaType(img, CanonicalConfigMediaType) img = mutate.Annotations(img, o.meta.ToAnnotations()).(gcrv1.Image) img, err = mutate.Append(img, mutate.Addendum{Layer: layer}) @@ -144,7 +142,7 @@ func (c *Client) Push(ctx context.Context, url, sourcePath string, opts ...PushO func createLayer(path string, layerType LayerType, opts layerOptions) (gcrv1.Layer, error) { switch layerType { case LayerTypeTarball: - var ociMediaType = oci.CanonicalContentMediaType + var ociMediaType = CanonicalContentMediaType var tmpDir string tmpDir, err := os.MkdirTemp("", "oci") if err != nil { @@ -170,7 +168,7 @@ func createLayer(path string, layerType LayerType, opts layerOptions) (gcrv1.Lay func getLayerMediaType(extension string) types.MediaType { if extension == "" { - return oci.CanonicalMediaTypePrefix + return CanonicalMediaTypePrefix } - return types.MediaType(fmt.Sprintf("%s.%s", oci.CanonicalMediaTypePrefix, extension)) + return types.MediaType(fmt.Sprintf("%s.%s", CanonicalMediaTypePrefix, extension)) } diff --git a/oci/client/push_pull_test.go b/oci/push_pull_test.go similarity index 88% rename from oci/client/push_pull_test.go rename to oci/push_pull_test.go index 3c68b2537..95cbea421 100644 --- a/oci/client/push_pull_test.go +++ b/oci/push_pull_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "bufio" @@ -35,8 +35,6 @@ import ( "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" . "github.com/onsi/gomega" - - "github.com/fluxcd/pkg/oci" ) func Test_Push_Pull(t *testing.T) { @@ -68,7 +66,7 @@ func Test_Push_Pull(t *testing.T) { tag: "v0.0.1", sourcePath: "testdata/artifact", testLayerType: LayerTypeTarball, - expectedMediaType: oci.CanonicalContentMediaType, + expectedMediaType: CanonicalContentMediaType, }, { name: "push directory (specify layer type)", @@ -81,7 +79,7 @@ func Test_Push_Pull(t *testing.T) { WithPullLayerType(LayerTypeTarball), }, testLayerType: LayerTypeTarball, - expectedMediaType: oci.CanonicalContentMediaType, + expectedMediaType: CanonicalContentMediaType, }, { name: "push static file", @@ -113,7 +111,7 @@ func Test_Push_Pull(t *testing.T) { WithPushLayerType(LayerTypeStatic), }, testLayerType: LayerTypeStatic, - expectedMediaType: oci.CanonicalMediaTypePrefix, + expectedMediaType: CanonicalMediaTypePrefix, }, { name: "push directory and pull archive (push with LayerTypeTarball and pull with LayerTypeStatic)", @@ -123,7 +121,7 @@ func Test_Push_Pull(t *testing.T) { WithPullLayerType(LayerTypeStatic), }, testLayerType: LayerTypeStatic, - expectedMediaType: oci.CanonicalContentMediaType, + expectedMediaType: CanonicalContentMediaType, }, { name: "push static artifact (and with LayerTypeTarball PullOption - should return error)", @@ -132,7 +130,7 @@ func Test_Push_Pull(t *testing.T) { pullOpts: []PullOption{WithPullLayerType(LayerTypeTarball)}, pushOpts: []PushOption{WithPushLayerType(LayerTypeStatic)}, expectedPullErr: true, - expectedMediaType: oci.CanonicalMediaTypePrefix, + expectedMediaType: CanonicalMediaTypePrefix, }, { name: "two layers in image (specify index)", @@ -147,11 +145,11 @@ func Test_Push_Pull(t *testing.T) { } img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) - img = mutate.ConfigMediaType(img, oci.CanonicalConfigMediaType) + img = mutate.ConfigMediaType(img, CanonicalConfigMediaType) - layer1 := static.NewLayer([]byte("test-byte"), oci.CanonicalMediaTypePrefix) + layer1 := static.NewLayer([]byte("test-byte"), CanonicalMediaTypePrefix) - layer2, err := tarball.LayerFromFile(artifact, tarball.WithMediaType(oci.CanonicalContentMediaType)) + layer2, err := tarball.LayerFromFile(artifact, tarball.WithMediaType(CanonicalContentMediaType)) if err != nil { return err } @@ -170,7 +168,7 @@ func Test_Push_Pull(t *testing.T) { testLayerIndex: 1, testLayerType: LayerTypeTarball, expectedNumLayers: 2, - expectedMediaType: oci.CanonicalContentMediaType, + expectedMediaType: CanonicalContentMediaType, }, { name: "specify wrong layer (should return error)", @@ -185,9 +183,9 @@ func Test_Push_Pull(t *testing.T) { } img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) - img = mutate.ConfigMediaType(img, oci.CanonicalConfigMediaType) + img = mutate.ConfigMediaType(img, CanonicalConfigMediaType) - layer1, err := tarball.LayerFromFile(artifact, tarball.WithMediaType(oci.CanonicalContentMediaType)) + layer1, err := tarball.LayerFromFile(artifact, tarball.WithMediaType(CanonicalContentMediaType)) if err != nil { return err } @@ -204,7 +202,7 @@ func Test_Push_Pull(t *testing.T) { } return err }, - expectedMediaType: oci.CanonicalContentMediaType, + expectedMediaType: CanonicalContentMediaType, expectedPullErr: true, }, } @@ -252,14 +250,14 @@ func Test_Push_Pull(t *testing.T) { // Verify that annotations exist in manifest if len(manifest.Annotations) != 0 { - g.Expect(manifest.Annotations[oci.CreatedAnnotation]).To(BeEquivalentTo(created)) - g.Expect(manifest.Annotations[oci.SourceAnnotation]).To(BeEquivalentTo(source)) - g.Expect(manifest.Annotations[oci.RevisionAnnotation]).To(BeEquivalentTo(revision)) + g.Expect(manifest.Annotations[CreatedAnnotation]).To(BeEquivalentTo(created)) + g.Expect(manifest.Annotations[SourceAnnotation]).To(BeEquivalentTo(source)) + g.Expect(manifest.Annotations[RevisionAnnotation]).To(BeEquivalentTo(revision)) } // Verify media types g.Expect(manifest.MediaType).To(Equal(types.OCIManifestSchema1)) - g.Expect(manifest.Config.MediaType).To(BeEquivalentTo(oci.CanonicalConfigMediaType)) + g.Expect(manifest.Config.MediaType).To(BeEquivalentTo(CanonicalConfigMediaType)) numLayers := 1 layerIdx := 0 @@ -305,6 +303,7 @@ func Test_Push_Pull(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) fileInfo, err := os.Stat(tt.sourcePath) + g.Expect(err).ToNot(HaveOccurred()) // if a directory was pushed, then the created file should be a gzipped archive if fileInfo.IsDir() { bufReader := bufio.NewReader(bytes.NewReader(got)) @@ -331,12 +330,12 @@ func Test_getLayerMediaType(t *testing.T) { }{ { name: "default oci media type", - expectedMediaType: oci.CanonicalMediaTypePrefix, + expectedMediaType: CanonicalMediaTypePrefix, }, { name: "oci media type with extension", ext: "test", - expectedMediaType: types.MediaType(fmt.Sprintf("%s.test", oci.CanonicalMediaTypePrefix)), + expectedMediaType: types.MediaType(fmt.Sprintf("%s.test", CanonicalMediaTypePrefix)), }, } diff --git a/oci/client/retry_transport.go b/oci/retry_transport.go similarity index 96% rename from oci/client/retry_transport.go rename to oci/retry_transport.go index a55452765..1a0cdd5fb 100644 --- a/oci/client/retry_transport.go +++ b/oci/retry_transport.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" @@ -30,8 +30,6 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" - - "github.com/fluxcd/pkg/oci" ) // WithRetryTransport returns a crane.Option for setting transport that uses the backoff for retries @@ -48,7 +46,7 @@ func WithRetryTransport(ctx context.Context, ref name.Reference, auth authn.Auth transport.WithRetryPredicate(defaultRetryPredicate), transport.WithRetryStatusCodes(retryableStatusCodes...), transport.WithRetryBackoff(backoff)) - retryTransport = transport.NewUserAgent(retryTransport, oci.UserAgent) + retryTransport = transport.NewUserAgent(retryTransport, UserAgent) t, err := transport.NewWithContext(ctx, ref.Context().Registry, auth, retryTransport, scopes) if err != nil { diff --git a/oci/client/suite_test.go b/oci/suite_test.go similarity index 99% rename from oci/client/suite_test.go rename to oci/suite_test.go index 9acdf2e52..f05197f82 100644 --- a/oci/client/suite_test.go +++ b/oci/suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/tag.go b/oci/tag.go similarity index 98% rename from oci/client/tag.go rename to oci/tag.go index 84b737cb6..8595f756d 100644 --- a/oci/client/tag.go +++ b/oci/tag.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/tag_test.go b/oci/tag_test.go similarity index 98% rename from oci/client/tag_test.go rename to oci/tag_test.go index 54fa61117..4215ba207 100644 --- a/oci/client/tag_test.go +++ b/oci/tag_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "context" diff --git a/oci/client/testdata/artifact/deploy/repo.yaml b/oci/testdata/artifact/deploy/repo.yaml similarity index 100% rename from oci/client/testdata/artifact/deploy/repo.yaml rename to oci/testdata/artifact/deploy/repo.yaml diff --git a/oci/client/testdata/artifact/deployment.yaml b/oci/testdata/artifact/deployment.yaml similarity index 100% rename from oci/client/testdata/artifact/deployment.yaml rename to oci/testdata/artifact/deployment.yaml diff --git a/oci/client/testdata/artifact/ignore-dir/deployment.yaml b/oci/testdata/artifact/ignore-dir/deployment.yaml similarity index 100% rename from oci/client/testdata/artifact/ignore-dir/deployment.yaml rename to oci/testdata/artifact/ignore-dir/deployment.yaml diff --git a/oci/client/testdata/artifact/ignore.txt b/oci/testdata/artifact/ignore.txt similarity index 100% rename from oci/client/testdata/artifact/ignore.txt rename to oci/testdata/artifact/ignore.txt diff --git a/oci/client/testdata/artifact/somedir/git/repo.yaml b/oci/testdata/artifact/somedir/git/repo.yaml similarity index 100% rename from oci/client/testdata/artifact/somedir/git/repo.yaml rename to oci/testdata/artifact/somedir/git/repo.yaml diff --git a/oci/client/testdata/artifact/somedir/repo.yaml b/oci/testdata/artifact/somedir/repo.yaml similarity index 100% rename from oci/client/testdata/artifact/somedir/repo.yaml rename to oci/testdata/artifact/somedir/repo.yaml diff --git a/oci/tests/integration/azure_test.go b/oci/tests/integration/azure_test.go index e445944cc..d6d488abf 100644 --- a/oci/tests/integration/azure_test.go +++ b/oci/tests/integration/azure_test.go @@ -21,6 +21,7 @@ package integration import ( "context" + "errors" "fmt" "log" "os" @@ -153,7 +154,7 @@ func grantPermissionsToGitRepositoryAzure(ctx context.Context, cfg *gitTestConfi time.Sleep(retryDelay) continue } else { - return fmt.Errorf(errMsg) + return errors.New(errMsg) } } uuid := responseValue.OperationResult.ServicePrincipalId diff --git a/oci/tests/integration/gcp_test.go b/oci/tests/integration/gcp_test.go index 77d2b2132..dfd31efc6 100644 --- a/oci/tests/integration/gcp_test.go +++ b/oci/tests/integration/gcp_test.go @@ -51,12 +51,6 @@ func registryLoginGCR(ctx context.Context, output map[string]*tfjson.StateOutput // with a new repository name. testRepos := map[string]string{} - repoURL := output["gcr_repository_url"].Value.(string) - if err := tftestenv.RegistryLoginGCR(ctx, repoURL); err != nil { - return nil, err - } - testRepos["gcr"] = repoURL + "/" + randStringRunes(5) - project := output["gcp_project"].Value.(string) region := output["gcp_region"].Value.(string) repositoryID := output["gcp_artifact_repository"].Value.(string) diff --git a/oci/tests/integration/git_test.go b/oci/tests/integration/git_test.go index 846a3dcf9..2cfc3e55e 100644 --- a/oci/tests/integration/git_test.go +++ b/oci/tests/integration/git_test.go @@ -39,7 +39,6 @@ func TestGitCloneUsingProvider(t *testing.T) { t.Run("Git oidc credential test", func(t *testing.T) { args := []string{ "-category=git", - "-oidc-login=true", fmt.Sprintf("-provider=%s", *targetProvider), fmt.Sprintf("-repo=%s", testGitCfg.applicationRepositoryWithoutUser), } diff --git a/oci/tests/integration/go.mod b/oci/tests/integration/go.mod index aae149427..4f35ad1b8 100644 --- a/oci/tests/integration/go.mod +++ b/oci/tests/integration/go.mod @@ -11,10 +11,9 @@ replace ( ) require ( - github.com/fluxcd/pkg/auth v0.11.0 - github.com/fluxcd/pkg/git v0.28.0 + github.com/fluxcd/pkg/auth v0.12.0 + github.com/fluxcd/pkg/git v0.29.0 github.com/fluxcd/pkg/git/gogit v0.23.0 - github.com/fluxcd/pkg/oci v0.43.1 github.com/fluxcd/test-infra/tftestenv v0.0.0-20240903092121-c783b14801d1 github.com/go-git/go-git/v5 v5.16.0 github.com/google/go-containerregistry v0.20.3 @@ -29,6 +28,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.6.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect @@ -109,6 +109,7 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect diff --git a/oci/tests/integration/go.sum b/oci/tests/integration/go.sum index a84a84a94..43db47f05 100644 --- a/oci/tests/integration/go.sum +++ b/oci/tests/integration/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= diff --git a/oci/tests/integration/oci_test.go b/oci/tests/integration/oci_test.go index 49c59840e..0439ee8e4 100644 --- a/oci/tests/integration/oci_test.go +++ b/oci/tests/integration/oci_test.go @@ -82,7 +82,6 @@ func TestOciOIDCLoginListTags(t *testing.T) { parts := strings.SplitN(repo, "/", 2) args := []string{ "-category=oci", - "-oidc-login=true", fmt.Sprintf("-registry=%s", parts[0]), fmt.Sprintf("-repo=%s", parts[1]), } @@ -91,7 +90,6 @@ func TestOciOIDCLoginListTags(t *testing.T) { // Registry + repo. args = []string{ "-category=oci", - "-oidc-login=true", fmt.Sprintf("-repo=%s", repo), } testjobExecutionWithArgs(t, args) diff --git a/oci/tests/integration/terraform/gcp/outputs.tf b/oci/tests/integration/terraform/gcp/outputs.tf index f56ed7606..e1e6538ad 100644 --- a/oci/tests/integration/terraform/gcp/outputs.tf +++ b/oci/tests/integration/terraform/gcp/outputs.tf @@ -11,10 +11,6 @@ output "gcp_region" { value = module.gke.region } -output "gcr_repository_url" { - value = module.gcr.gcr_repository_url -} - output "gcp_artifact_repository" { value = module.gcr.artifact_repository_id } diff --git a/oci/tests/integration/testapp/main.go b/oci/tests/integration/testapp/main.go index e16c5993f..0bf39112b 100644 --- a/oci/tests/integration/testapp/main.go +++ b/oci/tests/integration/testapp/main.go @@ -19,7 +19,6 @@ package main import ( "context" "flag" - "fmt" "log" "net/url" "os" @@ -34,11 +33,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/auth/aws" "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/gcp" + authutils "github.com/fluxcd/pkg/auth/utils" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/pkg/git/repository" - "github.com/fluxcd/pkg/oci/auth/login" ) // registry and repo flags are to facilitate testing of two login scenarios: @@ -47,11 +48,10 @@ import ( // - when the repository contains only the repository name and registry name // is provided separately, e.g. registry: foo.azurecr.io, repo: bar. var ( - registry = flag.String("registry", "", "registry of the repository") - repo = flag.String("repo", "", "git/oci repository to list") - oidcLogin = flag.Bool("oidc-login", false, "login with OIDCLogin function") - category = flag.String("category", "", "Test category to run - oci/git") - provider = flag.String("provider", "", "Supported git oidc provider - azure") + registry = flag.String("registry", "", "registry of the repository") + repo = flag.String("repo", "", "git/oci repository to list") + category = flag.String("category", "", "Test category to run - oci/git") + provider = flag.String("provider", "", "Supported git oidc provider - azure") ) func main() { @@ -69,18 +69,12 @@ func main() { } func checkOci(ctx context.Context) { - opts := login.ProviderOptions{ - AwsAutoLogin: true, - GcpAutoLogin: true, - AzureAutoLogin: true, - } - if *repo == "" { panic("must provide -repo value") } var loginURL string - var auth authn.Authenticator + var authenticator authn.Authenticator var ref name.Reference var err error @@ -99,10 +93,11 @@ func checkOci(ctx context.Context) { panic(err) } - if *oidcLogin { - auth, err = login.NewManager().OIDCLogin(ctx, fmt.Sprintf("https://%s", loginURL), opts) - } else { - auth, err = login.NewManager().Login(ctx, loginURL, ref, opts) + for _, provider := range []auth.Provider{aws.Provider{}, azure.Provider{}, gcp.Provider{}} { + if _, err = provider.ParseArtifactRepository(loginURL); err == nil { + authenticator, err = authutils.GetArtifactRegistryCredentials(ctx, provider.GetName(), loginURL) + break + } } if err != nil { @@ -111,7 +106,7 @@ func checkOci(ctx context.Context) { log.Println("logged in") var options []remote.Option - options = append(options, remote.WithAuth(auth)) + options = append(options, remote.WithAuth(authenticator)) options = append(options, remote.WithContext(ctx)) tags, err := remote.List(ref.Context(), options...) @@ -138,7 +133,7 @@ func checkGit(ctx context.Context) { auth.WithScopes(azure.ScopeDevOps), }, } - cloneDir, err := os.MkdirTemp("", fmt.Sprint("test-clone")) + cloneDir, err := os.MkdirTemp("", "test-clone") if err != nil { panic(err) } diff --git a/oci/client/url.go b/oci/url.go similarity index 83% rename from oci/client/url.go rename to oci/url.go index 63ec4cff1..0fa06a663 100644 --- a/oci/client/url.go +++ b/oci/url.go @@ -14,24 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package oci import ( "fmt" "strings" "github.com/google/go-containerregistry/pkg/name" - - "github.com/fluxcd/pkg/oci" ) // ParseArtifactURL validates the OCI URL and returns the address of the artifact. func ParseArtifactURL(ociURL string) (string, error) { - if !strings.HasPrefix(ociURL, oci.OCIRepositoryPrefix) { + if !strings.HasPrefix(ociURL, OCIRepositoryPrefix) { return "", fmt.Errorf("URL must be in format 'oci:////'") } - url := strings.TrimPrefix(ociURL, oci.OCIRepositoryPrefix) + url := strings.TrimPrefix(ociURL, OCIRepositoryPrefix) if _, err := name.ParseReference(url); err != nil { return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err) } @@ -41,11 +39,11 @@ func ParseArtifactURL(ociURL string) (string, error) { // ParseRepositoryURL validates the OCI URL and returns the address of the artifact repository. func ParseRepositoryURL(ociURL string) (string, error) { - if !strings.HasPrefix(ociURL, oci.OCIRepositoryPrefix) { + if !strings.HasPrefix(ociURL, OCIRepositoryPrefix) { return "", fmt.Errorf("URL must be in format 'oci:////'") } - url := strings.TrimPrefix(ociURL, oci.OCIRepositoryPrefix) + url := strings.TrimPrefix(ociURL, OCIRepositoryPrefix) ref, err := name.ParseReference(url) if err != nil { return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err)