Skip to content

Commit d95db94

Browse files
committed
Implement OCI auth for cloud providers
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
1 parent 8cc8798 commit d95db94

File tree

5 files changed

+132
-63
lines changed

5 files changed

+132
-63
lines changed

controllers/ocirepository_controller.go

+59-32
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import (
5050
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
5151

5252
"github.com/fluxcd/pkg/apis/meta"
53+
"github.com/fluxcd/pkg/oci"
54+
"github.com/fluxcd/pkg/oci/auth/login"
5355
"github.com/fluxcd/pkg/runtime/conditions"
5456
helper "github.com/fluxcd/pkg/runtime/controller"
5557
"github.com/fluxcd/pkg/runtime/events"
@@ -64,14 +66,6 @@ import (
6466
"github.com/fluxcd/source-controller/internal/util"
6567
)
6668

67-
const (
68-
ClientCert = "certFile"
69-
ClientKey = "keyFile"
70-
CACert = "caFile"
71-
OCISourceKey = "org.opencontainers.image.source"
72-
OCIRevisionKey = "org.opencontainers.image.revision"
73-
)
74-
7569
// ociRepositoryReadyCondition contains the information required to summarize a
7670
// v1beta2.OCIRepository Ready Condition.
7771
var ociRepositoryReadyCondition = summarize.Conditions{
@@ -297,7 +291,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
297291
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
298292
defer cancel()
299293

300-
// Generate the registry credential keychain
294+
options := r.craneOptions(ctxTimeout)
295+
296+
// Generate the registry credential keychain either from static credentials or using cloud OIDC
301297
keychain, err := r.keychain(ctx, obj)
302298
if err != nil {
303299
e := serror.NewGeneric(
@@ -307,6 +303,22 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
307303
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
308304
return sreconcile.ResultEmpty, e
309305
}
306+
options = append(options, crane.WithAuthFromKeychain(keychain))
307+
308+
if obj.Spec.Provider != sourcev1.GenericOCIProvider {
309+
auth, authErr := r.oidcAuth(ctxTimeout, obj)
310+
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
311+
e := serror.NewGeneric(
312+
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
313+
sourcev1.AuthenticationFailedReason,
314+
)
315+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
316+
return sreconcile.ResultEmpty, e
317+
}
318+
if auth != nil {
319+
options = append(options, crane.WithAuth(auth))
320+
}
321+
}
310322

311323
// Generate the transport for remote operations
312324
transport, err := r.transport(ctx, obj)
@@ -318,9 +330,12 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
318330
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
319331
return sreconcile.ResultEmpty, e
320332
}
333+
if transport != nil {
334+
options = append(options, crane.WithTransport(transport))
335+
}
321336

322337
// Determine which artifact revision to pull
323-
url, err := r.getArtifactURL(ctxTimeout, obj, keychain, transport)
338+
url, err := r.getArtifactURL(obj, options)
324339
if err != nil {
325340
e := serror.NewGeneric(
326341
fmt.Errorf("failed to determine the artifact address for '%s': %w", obj.Spec.URL, err),
@@ -330,7 +345,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
330345
}
331346

332347
// Pull artifact from the remote container registry
333-
img, err := crane.Pull(url, r.craneOptions(ctxTimeout, keychain, transport)...)
348+
img, err := crane.Pull(url, options...)
334349
if err != nil {
335350
e := serror.NewGeneric(
336351
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
@@ -441,8 +456,7 @@ func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository
441456
}
442457

443458
// getArtifactURL determines which tag or digest should be used and returns the OCI artifact FQN.
444-
func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context,
445-
obj *sourcev1.OCIRepository, keychain authn.Keychain, transport http.RoundTripper) (string, error) {
459+
func (r *OCIRepositoryReconciler) getArtifactURL(obj *sourcev1.OCIRepository, options []crane.Option) (string, error) {
446460
url, err := r.parseRepositoryURL(obj)
447461
if err != nil {
448462
return "", err
@@ -454,7 +468,7 @@ func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context,
454468
}
455469

456470
if obj.Spec.Reference.SemVer != "" {
457-
tag, err := r.getTagBySemver(ctx, url, obj.Spec.Reference.SemVer, keychain, transport)
471+
tag, err := r.getTagBySemver(url, obj.Spec.Reference.SemVer, options)
458472
if err != nil {
459473
return "", err
460474
}
@@ -471,9 +485,8 @@ func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context,
471485

472486
// getTagBySemver call the remote container registry, fetches all the tags from the repository,
473487
// and returns the latest tag according to the semver expression.
474-
func (r *OCIRepositoryReconciler) getTagBySemver(ctx context.Context,
475-
url, exp string, keychain authn.Keychain, transport http.RoundTripper) (string, error) {
476-
tags, err := crane.ListTags(url, r.craneOptions(ctx, keychain, transport)...)
488+
func (r *OCIRepositoryReconciler) getTagBySemver(url, exp string, options []crane.Option) (string, error) {
489+
tags, err := crane.ListTags(url, options...)
477490
if err != nil {
478491
return "", err
479492
}
@@ -567,20 +580,20 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
567580
transport := remote.DefaultTransport.Clone()
568581
tlsConfig := transport.TLSClientConfig
569582

570-
if clientCert, ok := certSecret.Data[ClientCert]; ok {
583+
if clientCert, ok := certSecret.Data[oci.ClientCert]; ok {
571584
// parse and set client cert and secret
572-
if clientKey, ok := certSecret.Data[ClientKey]; ok {
585+
if clientKey, ok := certSecret.Data[oci.ClientKey]; ok {
573586
cert, err := tls.X509KeyPair(clientCert, clientKey)
574587
if err != nil {
575588
return nil, err
576589
}
577590
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
578591
} else {
579-
return nil, fmt.Errorf("'%s' found in secret, but no %s", ClientCert, ClientKey)
592+
return nil, fmt.Errorf("'%s' found in secret, but no %s", oci.ClientCert, oci.ClientKey)
580593
}
581594
}
582595

583-
if caCert, ok := certSecret.Data[CACert]; ok {
596+
if caCert, ok := certSecret.Data[oci.CACert]; ok {
584597
syscerts, err := x509.SystemCertPool()
585598
if err != nil {
586599
return nil, err
@@ -592,20 +605,34 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
592605

593606
}
594607

608+
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
609+
func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Authenticator, error) {
610+
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
611+
ref, err := name.ParseReference(url)
612+
if err != nil {
613+
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
614+
}
615+
616+
opts := login.ProviderOptions{}
617+
switch obj.Spec.Provider {
618+
case sourcev1.AmazonOCIProvider:
619+
opts.AwsAutoLogin = true
620+
case sourcev1.AzureOCIProvider:
621+
opts.AzureAutoLogin = true
622+
case sourcev1.GoogleOCIProvider:
623+
opts.GcpAutoLogin = true
624+
}
625+
626+
return login.NewManager().Login(ctx, url, ref, opts)
627+
}
628+
595629
// craneOptions sets the auth headers, timeout and user agent
596630
// for all operations against remote container registries.
597-
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context,
598-
keychain authn.Keychain, transport http.RoundTripper) []crane.Option {
631+
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option {
599632
options := []crane.Option{
600633
crane.WithContext(ctx),
601-
crane.WithUserAgent("flux/v2"),
602-
crane.WithAuthFromKeychain(keychain),
634+
crane.WithUserAgent(oci.UserAgent),
603635
}
604-
605-
if transport != nil {
606-
options = append(options, crane.WithTransport(transport))
607-
}
608-
609636
return options
610637
}
611638

@@ -834,10 +861,10 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
834861
// enrich message with upstream annotations if found
835862
if info := newObj.GetArtifact().Metadata; info != nil {
836863
var source, revision string
837-
if val, ok := info[OCISourceKey]; ok {
864+
if val, ok := info[oci.SourceAnnotation]; ok {
838865
source = val
839866
}
840-
if val, ok := info[OCIRevisionKey]; ok {
867+
if val, ok := info[oci.RevisionAnnotation]; ok {
841868
revision = val
842869
}
843870
if source != "" && revision != "" {

controllers/ocirepository_controller_test.go

+21-17
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ import (
3636
"testing"
3737
"time"
3838

39-
corev1 "k8s.io/api/core/v1"
40-
"k8s.io/client-go/tools/record"
41-
4239
"github.com/darkowlzz/controller-check/status"
4340
"github.com/fluxcd/pkg/apis/meta"
41+
"github.com/fluxcd/pkg/oci"
4442
"github.com/fluxcd/pkg/runtime/conditions"
4543
"github.com/fluxcd/pkg/runtime/patch"
4644
"github.com/fluxcd/pkg/untar"
@@ -54,8 +52,10 @@ import (
5452
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
5553
"github.com/google/go-containerregistry/pkg/v1/mutate"
5654
. "github.com/onsi/gomega"
55+
corev1 "k8s.io/api/core/v1"
5756
apierrors "k8s.io/apimachinery/pkg/api/errors"
5857
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
58+
"k8s.io/client-go/tools/record"
5959
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
6060
"sigs.k8s.io/controller-runtime/pkg/client"
6161
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -172,8 +172,8 @@ func TestOCIRepository_Reconcile(t *testing.T) {
172172
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest))
173173

174174
// Check if the metadata matches the expected annotations
175-
g.Expect(obj.Status.Artifact.Metadata[OCISourceKey]).To(ContainSubstring("podinfo"))
176-
g.Expect(obj.Status.Artifact.Metadata[OCIRevisionKey]).To(ContainSubstring(tt.tag))
175+
g.Expect(obj.Status.Artifact.Metadata[oci.SourceAnnotation]).To(ContainSubstring("podinfo"))
176+
g.Expect(obj.Status.Artifact.Metadata[oci.RevisionAnnotation]).To(ContainSubstring(tt.tag))
177177

178178
// Check if the artifact storage path matches the expected file path
179179
localPath := testStorage.LocalPath(*obj.Status.Artifact)
@@ -516,7 +516,9 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
516516
Storage: testStorage,
517517
}
518518

519-
repoURL, err := r.getArtifactURL(ctx, obj, nil, nil)
519+
opts := r.craneOptions(ctx)
520+
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
521+
repoURL, err := r.getArtifactURL(obj, opts)
520522
g.Expect(err).To(BeNil())
521523

522524
assertConditions := tt.assertConditions
@@ -566,9 +568,9 @@ func TestOCIRepository_CertSecret(t *testing.T) {
566568

567569
tlsSecretClientCert := corev1.Secret{
568570
StringData: map[string]string{
569-
CACert: string(rootCertPEM),
570-
ClientCert: string(clientCertPEM),
571-
ClientKey: string(clientKeyPEM),
571+
oci.CACert: string(rootCertPEM),
572+
oci.ClientCert: string(clientCertPEM),
573+
oci.ClientKey: string(clientKeyPEM),
572574
},
573575
}
574576

@@ -601,9 +603,9 @@ func TestOCIRepository_CertSecret(t *testing.T) {
601603
digest: pi.digest,
602604
certSecret: &corev1.Secret{
603605
StringData: map[string]string{
604-
CACert: string(rootCertPEM),
605-
ClientCert: string(clientCertPEM),
606-
ClientKey: string("invalid-key"),
606+
oci.CACert: string(rootCertPEM),
607+
oci.ClientCert: string(clientCertPEM),
608+
oci.ClientKey: string("invalid-key"),
607609
},
608610
},
609611
expectreadyconition: false,
@@ -1049,7 +1051,9 @@ func TestOCIRepository_getArtifactURL(t *testing.T) {
10491051
obj.Spec.Reference = tt.reference
10501052
}
10511053

1052-
got, err := r.getArtifactURL(ctx, obj, authn.DefaultKeychain, nil)
1054+
opts := r.craneOptions(ctx)
1055+
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
1056+
got, err := r.getArtifactURL(obj, opts)
10531057
if tt.wantErr {
10541058
g.Expect(err).To(HaveOccurred())
10551059
return
@@ -1266,8 +1270,8 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) {
12661270
Revision: "xxx",
12671271
Checksum: "yyy",
12681272
Metadata: map[string]string{
1269-
OCISourceKey: "https://github.com/stefanprodan/podinfo",
1270-
OCIRevisionKey: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872",
1273+
oci.SourceAnnotation: "https://github.com/stefanprodan/podinfo",
1274+
oci.RevisionAnnotation: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872",
12711275
},
12721276
}
12731277
},
@@ -1438,8 +1442,8 @@ func pushMultiplePodinfoImages(serverURL string, versions ...string) (map[string
14381442

14391443
func setPodinfoImageAnnotations(img gcrv1.Image, tag string) gcrv1.Image {
14401444
metadata := map[string]string{
1441-
OCISourceKey: "https://github.com/stefanprodan/podinfo",
1442-
OCIRevisionKey: fmt.Sprintf("%s/SHA", tag),
1445+
oci.SourceAnnotation: "https://github.com/stefanprodan/podinfo",
1446+
oci.RevisionAnnotation: fmt.Sprintf("%s/SHA", tag),
14431447
}
14441448
return mutate.Annotations(img, metadata).(gcrv1.Image)
14451449
}

docs/spec/v1beta2/ocirepositories.md

+27
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,33 @@ container image repository in the format `oci://<host>:<port>/<org-name>/<repo-n
101101

102102
**Note:** that specifying a tag or digest is not in accepted for this field.
103103

104+
### Provider
105+
106+
`.spec.provider` is an optional field that allows specifying an OIDC provider used for
107+
authentication purposes.
108+
109+
Supported options are:
110+
111+
- `generic`
112+
- `aws`
113+
- `azure`
114+
- `gcp`
115+
116+
The `generic` provider can be used for public repositories or when
117+
static credentials are used for authentication, either with
118+
`spec.secretRef` or `spec.serviceAccountName`.
119+
If you do not specify `.spec.provider`, it defaults to `generic`.
120+
121+
The `aws` provider can be used when the source-controller service account
122+
is associate with an AWS IAM Role using IRSA that grants read-only access to ECR.
123+
124+
The `azure` provider can be used when the source-controller pods are associate
125+
with an Azure AAD Pod Identity that grants read-only access to ACR.
126+
127+
The `gcp` provider can be used when the source-controller service account
128+
is associate with a GCP IAM Role using Workload Identity that grants
129+
read-only access to Artifact Registry.
130+
104131
### Secret reference
105132

106133
`.spec.secretRef.name` is an optional field to specify a name reference to a

go.mod

+8-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ require (
2727
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135
2828
github.com/cyphar/filepath-securejoin v0.2.3
2929
github.com/darkowlzz/controller-check v0.0.0-20220325122359-11f5827b7981
30-
github.com/distribution/distribution/v3 v3.0.0-20220702071910-8857a1948739
30+
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f
3131
github.com/docker/cli v20.10.17+incompatible
3232
github.com/docker/go-units v0.4.0
3333
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021
@@ -37,6 +37,7 @@ require (
3737
github.com/fluxcd/pkg/gitutil v0.1.0
3838
github.com/fluxcd/pkg/helmtestserver v0.7.4
3939
github.com/fluxcd/pkg/lockedfile v0.1.0
40+
github.com/fluxcd/pkg/oci v0.2.0
4041
github.com/fluxcd/pkg/runtime v0.16.2
4142
github.com/fluxcd/pkg/ssh v0.5.0
4243
github.com/fluxcd/pkg/testserver v0.2.0
@@ -55,7 +56,7 @@ require (
5556
github.com/prometheus/client_golang v1.12.2
5657
github.com/spf13/pflag v1.0.5
5758
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
58-
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60
59+
golang.org/x/net v0.0.0-20220708220712-1185a9018129
5960
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
6061
google.golang.org/api v0.86.0
6162
gotest.tools v2.2.0+incompatible
@@ -108,6 +109,7 @@ require (
108109
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
109110
github.com/acomagu/bufpipe v1.0.3 // indirect
110111
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
112+
github.com/aws/aws-sdk-go v1.44.53 // indirect
111113
github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
112114
github.com/aws/aws-sdk-go-v2/config v1.15.8 // indirect
113115
github.com/aws/aws-sdk-go-v2/credentials v1.12.3 // indirect
@@ -218,7 +220,7 @@ require (
218220
github.com/opencontainers/go-digest v1.0.0 // indirect
219221
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
220222
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
221-
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
223+
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
222224
github.com/pkg/errors v0.9.1 // indirect
223225
github.com/pmezard/go-difflib v1.0.0 // indirect
224226
github.com/prometheus/client_model v0.2.0 // indirect
@@ -229,7 +231,7 @@ require (
229231
github.com/russross/blackfriday v1.6.0 // indirect
230232
github.com/sergi/go-diff v1.2.0 // indirect
231233
github.com/shopspring/decimal v1.2.0 // indirect
232-
github.com/sirupsen/logrus v1.8.1 // indirect
234+
github.com/sirupsen/logrus v1.9.0 // indirect
233235
github.com/spf13/cast v1.4.1 // indirect
234236
github.com/spf13/cobra v1.5.0 // indirect
235237
github.com/stretchr/testify v1.7.4 // indirect
@@ -247,8 +249,8 @@ require (
247249
go.uber.org/atomic v1.7.0 // indirect
248250
go.uber.org/multierr v1.6.0 // indirect
249251
go.uber.org/zap v1.21.0 // indirect
250-
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
251-
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
252+
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
253+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
252254
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
253255
golang.org/x/text v0.3.7 // indirect
254256
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect

0 commit comments

Comments
 (0)