Skip to content

Commit

Permalink
Implement OCI auth for cloud providers
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
  • Loading branch information
stefanprodan committed Aug 3, 2022
1 parent 8cc8798 commit d95db94
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 63 deletions.
91 changes: 59 additions & 32 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"

"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/events"
Expand All @@ -64,14 +66,6 @@ import (
"github.com/fluxcd/source-controller/internal/util"
)

const (
ClientCert = "certFile"
ClientKey = "keyFile"
CACert = "caFile"
OCISourceKey = "org.opencontainers.image.source"
OCIRevisionKey = "org.opencontainers.image.revision"
)

// ociRepositoryReadyCondition contains the information required to summarize a
// v1beta2.OCIRepository Ready Condition.
var ociRepositoryReadyCondition = summarize.Conditions{
Expand Down Expand Up @@ -297,7 +291,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel()

// Generate the registry credential keychain
options := r.craneOptions(ctxTimeout)

// Generate the registry credential keychain either from static credentials or using cloud OIDC
keychain, err := r.keychain(ctx, obj)
if err != nil {
e := serror.NewGeneric(
Expand All @@ -307,6 +303,22 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
options = append(options, crane.WithAuthFromKeychain(keychain))

if obj.Spec.Provider != sourcev1.GenericOCIProvider {
auth, authErr := r.oidcAuth(ctxTimeout, obj)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
e := serror.NewGeneric(
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
sourcev1.AuthenticationFailedReason,
)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
if auth != nil {
options = append(options, crane.WithAuth(auth))
}
}

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

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

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

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

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

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

if clientCert, ok := certSecret.Data[ClientCert]; ok {
if clientCert, ok := certSecret.Data[oci.ClientCert]; ok {
// parse and set client cert and secret
if clientKey, ok := certSecret.Data[ClientKey]; ok {
if clientKey, ok := certSecret.Data[oci.ClientKey]; ok {
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
return nil, err
}
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
} else {
return nil, fmt.Errorf("'%s' found in secret, but no %s", ClientCert, ClientKey)
return nil, fmt.Errorf("'%s' found in secret, but no %s", oci.ClientCert, oci.ClientKey)
}
}

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

}

// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Authenticator, error) {
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
ref, err := name.ParseReference(url)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
}

opts := login.ProviderOptions{}
switch obj.Spec.Provider {
case sourcev1.AmazonOCIProvider:
opts.AwsAutoLogin = true
case sourcev1.AzureOCIProvider:
opts.AzureAutoLogin = true
case sourcev1.GoogleOCIProvider:
opts.GcpAutoLogin = true
}

return login.NewManager().Login(ctx, url, ref, opts)
}

// craneOptions sets the auth headers, timeout and user agent
// for all operations against remote container registries.
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context,
keychain authn.Keychain, transport http.RoundTripper) []crane.Option {
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option {
options := []crane.Option{
crane.WithContext(ctx),
crane.WithUserAgent("flux/v2"),
crane.WithAuthFromKeychain(keychain),
crane.WithUserAgent(oci.UserAgent),
}

if transport != nil {
options = append(options, crane.WithTransport(transport))
}

return options
}

Expand Down Expand Up @@ -834,10 +861,10 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context,
// enrich message with upstream annotations if found
if info := newObj.GetArtifact().Metadata; info != nil {
var source, revision string
if val, ok := info[OCISourceKey]; ok {
if val, ok := info[oci.SourceAnnotation]; ok {
source = val
}
if val, ok := info[OCIRevisionKey]; ok {
if val, ok := info[oci.RevisionAnnotation]; ok {
revision = val
}
if source != "" && revision != "" {
Expand Down
38 changes: 21 additions & 17 deletions controllers/ocirepository_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,9 @@ import (
"testing"
"time"

corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"

"github.com/darkowlzz/controller-check/status"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/untar"
Expand All @@ -54,8 +52,10 @@ import (
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -172,8 +172,8 @@ func TestOCIRepository_Reconcile(t *testing.T) {
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest))

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

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

repoURL, err := r.getArtifactURL(ctx, obj, nil, nil)
opts := r.craneOptions(ctx)
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
repoURL, err := r.getArtifactURL(obj, opts)
g.Expect(err).To(BeNil())

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

tlsSecretClientCert := corev1.Secret{
StringData: map[string]string{
CACert: string(rootCertPEM),
ClientCert: string(clientCertPEM),
ClientKey: string(clientKeyPEM),
oci.CACert: string(rootCertPEM),
oci.ClientCert: string(clientCertPEM),
oci.ClientKey: string(clientKeyPEM),
},
}

Expand Down Expand Up @@ -601,9 +603,9 @@ func TestOCIRepository_CertSecret(t *testing.T) {
digest: pi.digest,
certSecret: &corev1.Secret{
StringData: map[string]string{
CACert: string(rootCertPEM),
ClientCert: string(clientCertPEM),
ClientKey: string("invalid-key"),
oci.CACert: string(rootCertPEM),
oci.ClientCert: string(clientCertPEM),
oci.ClientKey: string("invalid-key"),
},
},
expectreadyconition: false,
Expand Down Expand Up @@ -1049,7 +1051,9 @@ func TestOCIRepository_getArtifactURL(t *testing.T) {
obj.Spec.Reference = tt.reference
}

got, err := r.getArtifactURL(ctx, obj, authn.DefaultKeychain, nil)
opts := r.craneOptions(ctx)
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
got, err := r.getArtifactURL(obj, opts)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
return
Expand Down Expand Up @@ -1266,8 +1270,8 @@ func TestOCIRepositoryReconciler_notify(t *testing.T) {
Revision: "xxx",
Checksum: "yyy",
Metadata: map[string]string{
OCISourceKey: "https://github.com/stefanprodan/podinfo",
OCIRevisionKey: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872",
oci.SourceAnnotation: "https://github.com/stefanprodan/podinfo",
oci.RevisionAnnotation: "6.1.8/b3b00fe35424a45d373bf4c7214178bc36fd7872",
},
}
},
Expand Down Expand Up @@ -1438,8 +1442,8 @@ func pushMultiplePodinfoImages(serverURL string, versions ...string) (map[string

func setPodinfoImageAnnotations(img gcrv1.Image, tag string) gcrv1.Image {
metadata := map[string]string{
OCISourceKey: "https://github.com/stefanprodan/podinfo",
OCIRevisionKey: fmt.Sprintf("%s/SHA", tag),
oci.SourceAnnotation: "https://github.com/stefanprodan/podinfo",
oci.RevisionAnnotation: fmt.Sprintf("%s/SHA", tag),
}
return mutate.Annotations(img, metadata).(gcrv1.Image)
}
Expand Down
27 changes: 27 additions & 0 deletions docs/spec/v1beta2/ocirepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,33 @@ container image repository in the format `oci://<host>:<port>/<org-name>/<repo-n

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

### Provider

`.spec.provider` is an optional field that allows specifying an OIDC provider used for
authentication purposes.

Supported options are:

- `generic`
- `aws`
- `azure`
- `gcp`

The `generic` provider can be used for public repositories or when
static credentials are used for authentication, either with
`spec.secretRef` or `spec.serviceAccountName`.
If you do not specify `.spec.provider`, it defaults to `generic`.

The `aws` provider can be used when the source-controller service account
is associate with an AWS IAM Role using IRSA that grants read-only access to ECR.

The `azure` provider can be used when the source-controller pods are associate
with an Azure AAD Pod Identity that grants read-only access to ACR.

The `gcp` provider can be used when the source-controller service account
is associate with a GCP IAM Role using Workload Identity that grants
read-only access to Artifact Registry.

### Secret reference

`.spec.secretRef.name` is an optional field to specify a name reference to a
Expand Down
14 changes: 8 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20220623141421-5afb4c282135
github.com/cyphar/filepath-securejoin v0.2.3
github.com/darkowlzz/controller-check v0.0.0-20220325122359-11f5827b7981
github.com/distribution/distribution/v3 v3.0.0-20220702071910-8857a1948739
github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f
github.com/docker/cli v20.10.17+incompatible
github.com/docker/go-units v0.4.0
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021
Expand All @@ -37,6 +37,7 @@ require (
github.com/fluxcd/pkg/gitutil v0.1.0
github.com/fluxcd/pkg/helmtestserver v0.7.4
github.com/fluxcd/pkg/lockedfile v0.1.0
github.com/fluxcd/pkg/oci v0.2.0
github.com/fluxcd/pkg/runtime v0.16.2
github.com/fluxcd/pkg/ssh v0.5.0
github.com/fluxcd/pkg/testserver v0.2.0
Expand All @@ -55,7 +56,7 @@ require (
github.com/prometheus/client_golang v1.12.2
github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/net v0.0.0-20220706163947-c90051bbdb60
golang.org/x/net v0.0.0-20220708220712-1185a9018129
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
google.golang.org/api v0.86.0
gotest.tools v2.2.0+incompatible
Expand Down Expand Up @@ -108,6 +109,7 @@ require (
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.44.53 // indirect
github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.15.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.3 // indirect
Expand Down Expand Up @@ -218,7 +220,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
Expand All @@ -229,7 +231,7 @@ require (
github.com/russross/blackfriday v1.6.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/stretchr/testify v1.7.4 // indirect
Expand All @@ -247,8 +249,8 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
Expand Down
Loading

0 comments on commit d95db94

Please sign in to comment.