diff --git a/api/v1/gitrepository_types.go b/api/v1/gitrepository_types.go index 590f1a38..01efec29 100644 --- a/api/v1/gitrepository_types.go +++ b/api/v1/gitrepository_types.go @@ -77,6 +77,7 @@ const ( // GitRepositorySpec specifies the required configuration to produce an // Artifact for a Git repository. +// +kubebuilder:validation:XValidation:rule="!has(self.serviceAccountName) || (has(self.provider) && self.provider == 'azure')",message="serviceAccountName can only be set when provider is 'azure'" type GitRepositorySpec struct { // URL specifies the Git repository URL, it can be an HTTP/S or SSH address. // +kubebuilder:validation:Pattern="^(http|https|ssh)://.*$" @@ -98,6 +99,11 @@ type GitRepositorySpec struct { // +optional Provider string `json:"provider,omitempty"` + // ServiceAccountName is the name of the Kubernetes ServiceAccount used to + // authenticate to the GitRepository. This field is only supported for 'azure' provider. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + // Interval at which the GitRepository URL is checked for updates. // This interval is approximate and may be subject to jitter to ensure // efficient use of resources. diff --git a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml index cffdf747..7ba4c96e 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml @@ -174,6 +174,11 @@ spec: required: - name type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to + authenticate to the GitRepository. This field is only supported for 'azure' provider. + type: string sparseCheckout: description: |- SparseCheckout specifies a list of directories to checkout when cloning @@ -235,6 +240,10 @@ spec: - interval - url type: object + x-kubernetes-validations: + - message: serviceAccountName can only be set when provider is 'azure' + rule: '!has(self.serviceAccountName) || (has(self.provider) && self.provider + == ''azure'')' status: default: observedGeneration: -1 diff --git a/docs/api/v1/source.md b/docs/api/v1/source.md index 772fb100..fc0de302 100644 --- a/docs/api/v1/source.md +++ b/docs/api/v1/source.md @@ -413,6 +413,19 @@ When not specified, defaults to ‘generic’.

+serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName is the name of the Kubernetes ServiceAccount used to +authenticate to the GitRepository. This field is only supported for ‘azure’ provider.

+ + + + interval
@@ -2067,6 +2080,19 @@ When not specified, defaults to ‘generic’.

+serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName is the name of the Kubernetes ServiceAccount used to +authenticate to the GitRepository. This field is only supported for ‘azure’ provider.

+ + + + interval
diff --git a/docs/spec/v1/gitrepositories.md b/docs/spec/v1/gitrepositories.md index be17a1b4..d39ee73d 100644 --- a/docs/spec/v1/gitrepositories.md +++ b/docs/spec/v1/gitrepositories.md @@ -393,6 +393,24 @@ flux create secret githubapp ghapp-secret \ --app-private-key=~/private-key.pem ``` +### Service Account reference + +`.spec.serviceAccountName` is an optional field to specify a Service Account +in the same namespace as GitRepository with purpose depending on the value of +the `.spec.provider` field: + +- When `.spec.provider` is set to `azure`, the Service Account + will be used for Workload Identity authentication. In this case, the controller + feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the + controller will error out. For Azure DevOps specific setup, see the + [Azure DevOps integration guide](https://fluxcd.io/flux/integrations/azure/#for-azure-devops). + +**Note:** that for a publicly accessible git repository, you don't need to +provide a `secretRef` nor `serviceAccountName`. + +For a complete guide on how to set up authentication for cloud providers, +see the integration [docs](/flux/integrations/). + ### Interval `.spec.interval` is a required field that specifies the interval at which the diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index 7f32abc0..35ce52e4 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -663,6 +663,22 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1 getCreds = func() (*authutils.GitCredentials, error) { var opts []auth.Option + if obj.Spec.ServiceAccountName != "" { + // Check object-level workload identity feature gate. + if !auth.IsObjectLevelWorkloadIdentityEnabled() { + const gate = auth.FeatureGateObjectLevelWorkloadIdentity + const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller" + err := serror.NewStalling(fmt.Errorf(msgFmt, gate), meta.FeatureGateDisabledReason) + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, meta.FeatureGateDisabledReason, "%s", err) + return nil, err + } + 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: sourcev1.GitRepositoryKind, @@ -742,6 +758,12 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1 if getCreds != nil { creds, err := getCreds() if err != nil { + // Check if it's already a structured error and preserve it + switch err.(type) { + case *serror.Stalling, *serror.Generic: + return nil, err + } + e := serror.NewGeneric( fmt.Errorf("failed to configure authentication options: %w", err), sourcev1.AuthenticationFailedReason, diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 13693499..23ee8084 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -48,6 +48,7 @@ import ( kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/auth" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/github" "github.com/fluxcd/pkg/gittestserver" @@ -919,6 +920,15 @@ func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) { }, wantErr: "ManagedIdentityCredential", }, + { + name: "azure provider with service account and feature gate for object-level identity disabled", + url: "https://dev.azure.com/foo/bar/_git/baz", + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.Provider = sourcev1.GitProviderAzure + obj.Spec.ServiceAccountName = "azure-sa" + }, + wantErr: auth.FeatureGateObjectLevelWorkloadIdentity, + }, { name: "github provider with no secret ref", url: "https://github.com/org/repo.git",