Skip to content

Commit 081fa40

Browse files
committed
[RFC-0010] Add aws auth library
Signed-off-by: Matheus Pimenta <[email protected]>
1 parent 7329be9 commit 081fa40

File tree

9 files changed

+625
-46
lines changed

9 files changed

+625
-46
lines changed

auth/aws/credentials_provider.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package aws
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/aws/aws-sdk-go-v2/aws"
24+
25+
"github.com/fluxcd/pkg/auth"
26+
)
27+
28+
type credentialsProvider struct {
29+
opts []auth.Option
30+
}
31+
32+
// NewCredentialsProvider creates a new credentials provider for the given options.
33+
func NewCredentialsProvider(opts ...auth.Option) aws.CredentialsProvider {
34+
return &credentialsProvider{opts}
35+
}
36+
37+
// Retrieve implements aws.CredentialsProvider.
38+
func (c *credentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
39+
token, err := auth.GetToken(ctx, Provider{}, c.opts...)
40+
if err != nil {
41+
return aws.Credentials{}, err
42+
}
43+
awsToken, ok := token.(*Token)
44+
if !ok {
45+
return aws.Credentials{}, fmt.Errorf("failed to cast token to AWS token: %T", token)
46+
}
47+
return aws.Credentials{
48+
AccessKeyID: *awsToken.AccessKeyId,
49+
SecretAccessKey: *awsToken.SecretAccessKey,
50+
SessionToken: *awsToken.SessionToken,
51+
Expires: *awsToken.Expiration,
52+
CanExpire: true,
53+
}, nil
54+
}

auth/aws/options.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package aws
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"regexp"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
)
26+
27+
func getRegion() string {
28+
// The AWS_REGION is usually automatically set in EKS clusters.
29+
// If not set users can set it manually (e.g. Fargate).
30+
return os.Getenv("AWS_REGION")
31+
}
32+
33+
const roleARNPattern = `^arn:aws:iam::[0-9]{1,30}:role/.{1,200}$`
34+
35+
var roleARNRegex = regexp.MustCompile(roleARNPattern)
36+
37+
func getRoleARN(serviceAccount corev1.ServiceAccount) (string, error) {
38+
arn := serviceAccount.Annotations["eks.amazonaws.com/role-arn"]
39+
if arn == "" {
40+
return "", nil
41+
}
42+
if !roleARNRegex.MatchString(arn) {
43+
return "", fmt.Errorf("invalid AWS role ARN: '%s'. must match %s",
44+
arn, roleARNPattern)
45+
}
46+
return arn, nil
47+
}
48+
49+
func getRoleSessionName(serviceAccount corev1.ServiceAccount) string {
50+
name := serviceAccount.Name
51+
namespace := serviceAccount.Namespace
52+
region := getRegion()
53+
return fmt.Sprintf("%s.%s.%s.fluxcd.io", name, namespace, region)
54+
}
55+
56+
// This regex is sourced from the AWS ECR Credential Helper (https://github.com/awslabs/amazon-ecr-credential-helper).
57+
// It covers both public AWS partitions like amazonaws.com, China partitions like amazonaws.com.cn, and non-public partitions.
58+
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)`)
59+
60+
// ParseRegistry returns the AWS account ID and region and `true` if
61+
// the image registry/repository is hosted in AWS's Elastic Container Registry,
62+
// otherwise empty strings and `false`.
63+
func ParseRegistry(registry string) (accountId, awsEcrRegion string, ok bool) {
64+
registryParts := registryPartRe.FindAllStringSubmatch(registry, -1)
65+
if len(registryParts) < 1 || len(registryParts[0]) < 3 {
66+
return "", "", false
67+
}
68+
return registryParts[0][1], registryParts[0][2], true
69+
}

auth/aws/options_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package aws_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
24+
"github.com/fluxcd/pkg/auth/aws"
25+
)
26+
27+
func TestParseRegistry(t *testing.T) {
28+
tests := []struct {
29+
registry string
30+
wantAccountID string
31+
wantRegion string
32+
wantOK bool
33+
}{
34+
{
35+
registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1",
36+
wantAccountID: "012345678901",
37+
wantRegion: "us-east-1",
38+
wantOK: true,
39+
},
40+
{
41+
registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo",
42+
wantAccountID: "012345678901",
43+
wantRegion: "us-east-1",
44+
wantOK: true,
45+
},
46+
{
47+
registry: "012345678901.dkr.ecr.us-east-1.amazonaws.com",
48+
wantAccountID: "012345678901",
49+
wantRegion: "us-east-1",
50+
wantOK: true,
51+
},
52+
{
53+
registry: "https://012345678901.dkr.ecr.us-east-1.amazonaws.com/v2/part/part",
54+
wantAccountID: "012345678901",
55+
wantRegion: "us-east-1",
56+
wantOK: true,
57+
},
58+
{
59+
registry: "012345678901.dkr.ecr.cn-north-1.amazonaws.com.cn/foo",
60+
wantAccountID: "012345678901",
61+
wantRegion: "cn-north-1",
62+
wantOK: true,
63+
},
64+
{
65+
registry: "012345678901.dkr.ecr-fips.us-gov-west-1.amazonaws.com",
66+
wantAccountID: "012345678901",
67+
wantRegion: "us-gov-west-1",
68+
wantOK: true,
69+
},
70+
{
71+
registry: "012345678901.dkr.ecr.us-secret-region.sc2s.sgov.gov",
72+
wantAccountID: "012345678901",
73+
wantRegion: "us-secret-region",
74+
wantOK: true,
75+
},
76+
{
77+
registry: "012345678901.dkr.ecr-fips.us-ts-region.c2s.ic.gov",
78+
wantAccountID: "012345678901",
79+
wantRegion: "us-ts-region",
80+
wantOK: true,
81+
},
82+
{
83+
registry: "012345678901.dkr.ecr.uk-region.cloud.adc-e.uk",
84+
wantAccountID: "012345678901",
85+
wantRegion: "uk-region",
86+
wantOK: true,
87+
},
88+
{
89+
registry: "012345678901.dkr.ecr.us-ts-region.csp.hci.ic.gov",
90+
wantAccountID: "012345678901",
91+
wantRegion: "us-ts-region",
92+
wantOK: true,
93+
},
94+
// TODO: Fix: this invalid registry is allowed by the regex.
95+
// {
96+
// registry: ".dkr.ecr.error.amazonaws.com",
97+
// wantOK: false,
98+
// },
99+
{
100+
registry: "gcr.io/foo/bar:baz",
101+
wantOK: false,
102+
},
103+
}
104+
105+
for _, tt := range tests {
106+
t.Run(tt.registry, func(t *testing.T) {
107+
g := NewWithT(t)
108+
109+
accId, region, ok := aws.ParseRegistry(tt.registry)
110+
g.Expect(ok).To(Equal(tt.wantOK), "unexpected OK")
111+
g.Expect(accId).To(Equal(tt.wantAccountID), "unexpected account IDs")
112+
g.Expect(region).To(Equal(tt.wantRegion), "unexpected regions")
113+
})
114+
}
115+
}

0 commit comments

Comments
 (0)