[RFC-0010] Add multi-tenant workload identity support for Azure GitRepository

Signed-off-by: Dipti Pai <diptipai89@outlook.com>
This commit is contained in:
Dipti Pai 2025-08-14 16:31:13 -07:00
parent 5f9702bb01
commit 4fe3434ee8
6 changed files with 91 additions and 0 deletions

View File

@ -77,6 +77,7 @@ const (
// GitRepositorySpec specifies the required configuration to produce an // GitRepositorySpec specifies the required configuration to produce an
// Artifact for a Git repository. // 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 { type GitRepositorySpec struct {
// URL specifies the Git repository URL, it can be an HTTP/S or SSH address. // URL specifies the Git repository URL, it can be an HTTP/S or SSH address.
// +kubebuilder:validation:Pattern="^(http|https|ssh)://.*$" // +kubebuilder:validation:Pattern="^(http|https|ssh)://.*$"
@ -98,6 +99,11 @@ type GitRepositorySpec struct {
// +optional // +optional
Provider string `json:"provider,omitempty"` 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. // Interval at which the GitRepository URL is checked for updates.
// This interval is approximate and may be subject to jitter to ensure // This interval is approximate and may be subject to jitter to ensure
// efficient use of resources. // efficient use of resources.

View File

@ -174,6 +174,11 @@ spec:
required: required:
- name - name
type: object 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: sparseCheckout:
description: |- description: |-
SparseCheckout specifies a list of directories to checkout when cloning SparseCheckout specifies a list of directories to checkout when cloning
@ -235,6 +240,10 @@ spec:
- interval - interval
- url - url
type: object 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: status:
default: default:
observedGeneration: -1 observedGeneration: -1

View File

@ -413,6 +413,19 @@ When not specified, defaults to &lsquo;generic&rsquo;.</p>
</tr> </tr>
<tr> <tr>
<td> <td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
authenticate to the GitRepository. This field is only supported for &lsquo;azure&rsquo; provider.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br> <code>interval</code><br>
<em> <em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration"> <a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
@ -2067,6 +2080,19 @@ When not specified, defaults to &lsquo;generic&rsquo;.</p>
</tr> </tr>
<tr> <tr>
<td> <td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
authenticate to the GitRepository. This field is only supported for &lsquo;azure&rsquo; provider.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br> <code>interval</code><br>
<em> <em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration"> <a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">

View File

@ -393,6 +393,24 @@ flux create secret githubapp ghapp-secret \
--app-private-key=~/private-key.pem --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 ### Interval
`.spec.interval` is a required field that specifies the interval at which the `.spec.interval` is a required field that specifies the interval at which the

View File

@ -663,6 +663,22 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
getCreds = func() (*authutils.GitCredentials, error) { getCreds = func() (*authutils.GitCredentials, error) {
var opts []auth.Option 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 { if r.TokenCache != nil {
involvedObject := cache.InvolvedObject{ involvedObject := cache.InvolvedObject{
Kind: sourcev1.GitRepositoryKind, Kind: sourcev1.GitRepositoryKind,
@ -742,6 +758,12 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
if getCreds != nil { if getCreds != nil {
creds, err := getCreds() creds, err := getCreds()
if err != nil { 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( e := serror.NewGeneric(
fmt.Errorf("failed to configure authentication options: %w", err), fmt.Errorf("failed to configure authentication options: %w", err),
sourcev1.AuthenticationFailedReason, sourcev1.AuthenticationFailedReason,

View File

@ -48,6 +48,7 @@ import (
kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/github" "github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/pkg/gittestserver" "github.com/fluxcd/pkg/gittestserver"
@ -919,6 +920,15 @@ func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) {
}, },
wantErr: "ManagedIdentityCredential", 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", name: "github provider with no secret ref",
url: "https://github.com/org/repo.git", url: "https://github.com/org/repo.git",