Merge pull request #1790 from fluxcd/rfc-0010-oci

[RFC-0010] Introduce object-level workload identity for OCIRepository
This commit is contained in:
Matheus Pimenta 2025-05-07 18:57:08 +01:00 committed by GitHub
commit e2538552af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 72 additions and 59 deletions

View File

@ -19,6 +19,12 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- source.toolkit.fluxcd.io
resources:

8
go.mod
View File

@ -24,15 +24,15 @@ require (
github.com/fluxcd/cli-utils v0.36.0-flux.13
github.com/fluxcd/pkg/apis/event v0.17.0
github.com/fluxcd/pkg/apis/meta v1.11.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/gogit v0.30.0
github.com/fluxcd/pkg/git v0.29.0
github.com/fluxcd/pkg/git/gogit v0.31.0
github.com/fluxcd/pkg/gittestserver v0.17.0
github.com/fluxcd/pkg/helmtestserver v0.24.0
github.com/fluxcd/pkg/lockedfile v0.6.0
github.com/fluxcd/pkg/masktoken v0.7.0
github.com/fluxcd/pkg/oci v0.47.0
github.com/fluxcd/pkg/oci v0.48.0
github.com/fluxcd/pkg/runtime v0.59.0
github.com/fluxcd/pkg/sourceignore v0.12.0
github.com/fluxcd/pkg/ssh v0.18.0

16
go.sum
View File

@ -374,14 +374,14 @@ github.com/fluxcd/pkg/apis/event v0.17.0 h1:foEINE++pCJlWVhWjYDXfkVmGKu8mQ4BDBlb
github.com/fluxcd/pkg/apis/event v0.17.0/go.mod h1:0fLhLFiHlRTDKPDXdRnv+tS7mCMIQ0fJxnEfmvGM/5A=
github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
github.com/fluxcd/pkg/auth v0.11.0 h1:1BC6fQ71lCLFKz7juGlvWq9ysR2HVl5JPOWoxy4RMWE=
github.com/fluxcd/pkg/auth v0.11.0/go.mod h1:BJVrbanLH0AoUBzOH7u016D21Zl3dvEd0AnAWVOo5Vs=
github.com/fluxcd/pkg/auth v0.12.0 h1:35o0ziYMLZVgJwNvJBGsv/wd903B2fMagcrnm1ptUjc=
github.com/fluxcd/pkg/auth v0.12.0/go.mod h1:gQD2VT5OhIR1E8ZTEsTaho3bDQZidr9P10smH/awcew=
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/fluxcd/pkg/git v0.28.0 h1:by7XTOvj4ZUPH1alYMJtDCVryhHue+UfjhrnPuJt5vA=
github.com/fluxcd/pkg/git v0.28.0/go.mod h1:VPv6O3mYnYvn79LOdWAFCl4fE8o651cxW/p/yxBoq2g=
github.com/fluxcd/pkg/git/gogit v0.30.0 h1:tdKRT4EDV8Cc2tBX+bg4H4gdcND7M4OEl6DQy1jSJmo=
github.com/fluxcd/pkg/git/gogit v0.30.0/go.mod h1:UCm/fOBuvX43BNz7Rc61Sukp2gBG/qxlOASaBkwMFvc=
github.com/fluxcd/pkg/git v0.29.0 h1:MHQ4F53e6Xt8a/POkd/fiChgysnd/XqiuK7vOWXAXLk=
github.com/fluxcd/pkg/git v0.29.0/go.mod h1:Ygn+LfrK6Ok+85uiq6s3NWG5LcHS4KY7mzES2JDJsGY=
github.com/fluxcd/pkg/git/gogit v0.31.0 h1:A56cmtgJBkWAj+gXSOdhPMQVTx0VF91S0PUaqpMXN4g=
github.com/fluxcd/pkg/git/gogit v0.31.0/go.mod h1:ya8z22xTvAAdW12HycxKYv4S+G+lqu5Kx/LyO/jWz8Y=
github.com/fluxcd/pkg/gittestserver v0.17.0 h1:JlBvWZQTDOI+np5Z+084m3DkeAH1hMusEybyRUDF63k=
github.com/fluxcd/pkg/gittestserver v0.17.0/go.mod h1:E/40EmLoXcMqd6gLuLDC9F6KJxqHVGbBBeMNKk5XdxU=
github.com/fluxcd/pkg/helmtestserver v0.24.0 h1:9sSfRG17GnDIup4sI8V+fdvKROtunU4JyIo34uvXq3Q=
@ -390,8 +390,8 @@ github.com/fluxcd/pkg/lockedfile v0.6.0 h1:64RRMiPv3ZK9Y4sjI8c78kZAdfEo+Sjr2iP8a
github.com/fluxcd/pkg/lockedfile v0.6.0/go.mod h1:gpdUVm7+05NIT1ZvzuNnHfnT81OhZtIySlxxkZ68pXk=
github.com/fluxcd/pkg/masktoken v0.7.0 h1:pitmyOg2pUVdW+nn2Lk/xqm2TaA08uxvOC0ns3sz6bM=
github.com/fluxcd/pkg/masktoken v0.7.0/go.mod h1:Lc1uoDjO1GY6+YdkK+ZqqBIBWquyV58nlSJ5S1N1IYU=
github.com/fluxcd/pkg/oci v0.47.0 h1:eQ7syqy91Xcfd7Sgf64v5n+dfRAju/OBiXuOhZsgQAg=
github.com/fluxcd/pkg/oci v0.47.0/go.mod h1:XBnI8+T6YFnIW4uEFojg7iIgHjKH7LXMpZARXJ9qmZk=
github.com/fluxcd/pkg/oci v0.48.0 h1:iSK4JDM0nx9plSlOGx2aI4td6aQdV/awrfXK/bzI35I=
github.com/fluxcd/pkg/oci v0.48.0/go.mod h1:rnUC8EOpzQp4rugpmopYFMnG3+CR1wqEV3356gHUtSY=
github.com/fluxcd/pkg/runtime v0.59.0 h1:3OrFkMJB39NcQ2vhhoxqls59sQVSn8U+thhyLbsQoA4=
github.com/fluxcd/pkg/runtime v0.59.0/go.mod h1:MFbfyNyyoYRgPxpdwC9/dCOkzo7Yxhu/cQ9NKyhvqc0=
github.com/fluxcd/pkg/sourceignore v0.12.0 h1:jCIe6d50rQ3wdXPF0+PhhqN0XrTRIq3upMomPelI8Mw=

View File

@ -132,19 +132,17 @@ type GitRepositoryReconciler struct {
Storage *Storage
ControllerName string
TokenCache *cache.TokenCache
requeueDependency time.Duration
features map[string]bool
patchOptions []patch.Option
tokenCache *cache.TokenCache
}
type GitRepositoryReconcilerOptions struct {
DependencyRequeueInterval time.Duration
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
TokenCache *cache.TokenCache
}
// gitRepositoryReconcileFunc is the function type for all the
@ -164,8 +162,6 @@ func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
r.features = features.FeatureGates()
}
r.tokenCache = opts.TokenCache
return ctrl.NewControllerManagedBy(mgr).
For(&sourcev1.GitRepository{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
@ -689,14 +685,14 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
var authOpts []auth.Option
if r.tokenCache != nil {
if r.TokenCache != nil {
involvedObject := cache.InvolvedObject{
Kind: sourcev1.GitRepositoryKind,
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Operation: cache.OperationReconcile,
}
authOpts = append(authOpts, auth.WithCache(*r.tokenCache, involvedObject))
authOpts = append(authOpts, auth.WithCache(*r.TokenCache, involvedObject))
}
if proxyURL != nil {
@ -726,7 +722,7 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
GitHubOpts: []github.OptFunc{
github.WithAppData(authData),
github.WithProxyURL(proxyURL),
github.WithCache(r.tokenCache, sourcev1.GitRepositoryKind,
github.WithCache(r.TokenCache, sourcev1.GitRepositoryKind,
obj.GetName(), obj.GetNamespace(), cache.OperationReconcile),
},
}
@ -1150,7 +1146,7 @@ func (r *GitRepositoryReconciler) reconcileDelete(ctx context.Context, obj *sour
controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer)
// Cleanup caches.
r.tokenCache.DeleteEventsForObject(sourcev1.GitRepositoryKind,
r.TokenCache.DeleteEventsForObject(sourcev1.GitRepositoryKind,
obj.GetName(), obj.GetNamespace(), cache.OperationReconcile)
// Stop reconciliation as the object is being deleted

View File

@ -70,7 +70,6 @@ import (
"github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/repository"
"github.com/fluxcd/source-controller/internal/oci"
soci "github.com/fluxcd/source-controller/internal/oci"
scosign "github.com/fluxcd/source-controller/internal/oci/cosign"
"github.com/fluxcd/source-controller/internal/oci/notation"
@ -1255,7 +1254,7 @@ func observeChartBuild(ctx context.Context, sp *patch.SerialPatcher, pOpts []pat
if build.Complete() {
conditions.Delete(obj, sourcev1.FetchFailedCondition)
conditions.Delete(obj, sourcev1.BuildFailedCondition)
if build.VerifiedResult == oci.VerificationResultSuccess {
if build.VerifiedResult == soci.VerificationResultSuccess {
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version %s", build.Version)
}
}

View File

@ -51,6 +51,8 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/oci"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
@ -141,6 +143,7 @@ type OCIRepositoryReconciler struct {
Storage *Storage
ControllerName string
TokenCache *cache.TokenCache
requeueDependency time.Duration
patchOptions []patch.Option
@ -175,6 +178,7 @@ func (r *OCIRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories/finalizers,verbs=get;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
func (r *OCIRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
start := time.Now()
@ -328,7 +332,7 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Seria
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher,
obj *ociv1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
var auth authn.Authenticator
var authenticator authn.Authenticator
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel()
@ -363,9 +367,29 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
}
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != ociv1.GenericOCIProvider && ok {
var opts []auth.Option
if obj.Spec.ServiceAccountName != "" {
serviceAccount := client.ObjectKey{
Name: obj.Spec.ServiceAccountName,
Namespace: obj.GetNamespace(),
}
opts = append(opts, auth.WithServiceAccount(serviceAccount, r.Client))
}
if r.TokenCache != nil {
involvedObject := cache.InvolvedObject{
Kind: ociv1.OCIRepositoryKind,
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Operation: cache.OperationReconcile,
}
opts = append(opts, auth.WithCache(*r.TokenCache, involvedObject))
}
if proxyURL != nil {
opts = append(opts, auth.WithProxyURL(*proxyURL))
}
var authErr error
auth, authErr = soci.OIDCAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider, proxyURL)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
authenticator, authErr = soci.OIDCAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider, opts...)
if authErr != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
sourcev1.AuthenticationFailedReason,
@ -386,7 +410,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
return sreconcile.ResultEmpty, e
}
opts := makeRemoteOptions(ctx, transport, keychain, auth)
opts := makeRemoteOptions(ctx, transport, keychain, authenticator)
// Determine which artifact revision to pull
ref, err := r.getArtifactRef(obj, opts)
@ -446,7 +470,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
result, err := r.verifySignature(ctx, obj, ref, keychain, auth, transport, opts...)
result, err := r.verifySignature(ctx, obj, ref, keychain, authenticator, transport, opts...)
if err != nil {
provider := obj.Spec.Verify.Provider
if obj.Spec.Verify.SecretRef == nil && obj.Spec.Verify.Provider == "cosign" {
@ -1225,6 +1249,10 @@ func (r *OCIRepositoryReconciler) reconcileDelete(ctx context.Context, obj *ociv
// Remove our finalizer from the list
controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer)
// Cleanup caches.
r.TokenCache.DeleteEventsForObject(ociv1.OCIRepositoryKind,
obj.GetName(), obj.GetNamespace(), cache.OperationReconcile)
// Stop reconciliation as the object is being deleted
return sreconcile.ResultEmpty, nil
}

View File

@ -872,9 +872,9 @@ func TestOCIRepository_CertSecret(t *testing.T) {
tlsSecretClientCert := corev1.Secret{
Data: map[string][]byte{
oci.CACert: tlsCA,
oci.ClientCert: clientPublicKey,
oci.ClientKey: clientPrivateKey,
"caFile": tlsCA,
"certFile": clientPublicKey,
"keyFile": clientPrivateKey,
},
}
@ -907,9 +907,9 @@ func TestOCIRepository_CertSecret(t *testing.T) {
digest: pi.digest,
certSecret: &corev1.Secret{
Data: map[string][]byte{
oci.CACert: tlsCA,
oci.ClientCert: clientPublicKey,
oci.ClientKey: []byte("invalid-key"),
"caFile": tlsCA,
"certFile": clientPublicKey,
"keyFile": []byte("invalid-key"),
},
},
expectreadyconition: false,

View File

@ -24,7 +24,6 @@ import (
"os"
"path"
"github.com/fluxcd/pkg/oci"
"github.com/google/go-containerregistry/pkg/authn"
helmgetter "helm.sh/helm/v3/pkg/getter"
helmreg "helm.sh/helm/v3/pkg/registry"
@ -137,8 +136,8 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *sourcev1.HelmRepos
}
}
} else if obj.Spec.Provider != sourcev1beta2.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI && ociRepo {
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider, nil)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil {
return nil, "", fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
}
if authenticator != nil {

View File

@ -18,13 +18,12 @@ package oci
import (
"context"
"fmt"
"net/url"
"strings"
"github.com/fluxcd/pkg/oci/auth/login"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/fluxcd/pkg/auth"
authutils "github.com/fluxcd/pkg/auth/utils"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
@ -41,22 +40,7 @@ func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) {
}
// OIDCAuth generates the OIDC credential authenticator based on the specified cloud provider.
func OIDCAuth(ctx context.Context, url, provider string, proxyURL *url.URL) (authn.Authenticator, error) {
func OIDCAuth(ctx context.Context, url, provider string, opts ...auth.Option) (authn.Authenticator, error) {
u := strings.TrimPrefix(url, sourcev1.OCIRepositoryPrefix)
ref, err := name.ParseReference(u)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
}
opts := login.ProviderOptions{}
switch provider {
case sourcev1.AmazonOCIProvider:
opts.AwsAutoLogin = true
case sourcev1.AzureOCIProvider:
opts.AzureAutoLogin = true
case sourcev1.GoogleOCIProvider:
opts.GcpAutoLogin = true
}
return login.NewManager(login.WithProxyURL(proxyURL)).Login(ctx, u, ref, opts)
return authutils.GetArtifactRegistryCredentials(ctx, provider, u, opts...)
}

View File

@ -216,10 +216,10 @@ func main() {
Metrics: metrics,
Storage: storage,
ControllerName: controllerName,
TokenCache: tokenCache,
}).SetupWithManagerAndOptions(mgr, controller.GitRepositoryReconcilerOptions{
DependencyRequeueInterval: requeueDependency,
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
TokenCache: tokenCache,
}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", v1.GitRepositoryKind)
os.Exit(1)
@ -278,6 +278,7 @@ func main() {
Storage: storage,
EventRecorder: eventRecorder,
ControllerName: controllerName,
TokenCache: tokenCache,
Metrics: metrics,
}).SetupWithManagerAndOptions(mgr, controller.OCIRepositoryReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),