From 772131a6d0782b8d7f69ac5d3be33cab673e468e Mon Sep 17 00:00:00 2001
From: Dipti Pai
Date: Thu, 22 Aug 2024 22:41:55 -0700
Subject: [PATCH] Enable Azure OIDC for Azure DevOps Respository
- Add a new provider field to GitRepository API spec which can be set to azure to enable passwordless authentication to Azure DevOps repositories.
- API docs for new provider field and guidance to setup Azure environment with workload identity.
- Controller changes to set the provider options in git authoptions to fetch credential while cloning the repository.
- Add unit tests for testing provider
Signed-off-by: Dipti Pai
---
api/v1/gitrepository_types.go | 15 ++++
...rce.toolkit.fluxcd.io_gitrepositories.yaml | 9 ++
docs/api/v1/source.md | 26 ++++++
docs/spec/v1/gitrepositories.md | 85 +++++++++++++++++++
go.mod | 7 ++
go.sum | 10 ++-
.../controller/gitrepository_controller.go | 14 +++
.../gitrepository_controller_test.go | 48 +++++++++++
8 files changed, 210 insertions(+), 4 deletions(-)
diff --git a/api/v1/gitrepository_types.go b/api/v1/gitrepository_types.go
index 0d3b3abea..bfe309871 100644
--- a/api/v1/gitrepository_types.go
+++ b/api/v1/gitrepository_types.go
@@ -27,6 +27,14 @@ import (
const (
// GitRepositoryKind is the string representation of a GitRepository.
GitRepositoryKind = "GitRepository"
+
+ // GitProviderGeneric provides support for authentication using
+ // credentials specified in secretRef.
+ GitProviderGeneric string = "generic"
+
+ // GitProviderAzure provides support for authentication to azure
+ // repositories using Managed Identity.
+ GitProviderAzure string = "azure"
)
const (
@@ -80,6 +88,13 @@ type GitRepositorySpec struct {
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
+ // Provider used for authentication, can be 'azure', 'generic'.
+ // When not specified, defaults to 'generic'.
+ // +kubebuilder:validation:Enum=generic;azure
+ // +kubebuilder:default:=generic
+ // +optional
+ Provider string `json:"provider,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 f12533ad6..e7a48ee25 100644
--- a/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml
+++ b/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml
@@ -103,6 +103,15 @@ spec:
efficient use of resources.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
+ provider:
+ default: generic
+ description: |-
+ Provider used for authentication, can be 'azure', 'generic'.
+ When not specified, defaults to 'generic'.
+ enum:
+ - generic
+ - azure
+ type: string
proxySecretRef:
description: |-
ProxySecretRef specifies the Secret containing the proxy configuration
diff --git a/docs/api/v1/source.md b/docs/api/v1/source.md
index 1424cdecc..521dddc14 100644
--- a/docs/api/v1/source.md
+++ b/docs/api/v1/source.md
@@ -383,6 +383,19 @@ and ‘known_hosts’ fields.
+provider
+
+string
+
+ |
+
+(Optional)
+ Provider used for authentication, can be ‘azure’, ‘generic’.
+When not specified, defaults to ‘generic’.
+ |
+
+
+
interval
@@ -1710,6 +1723,19 @@ and ‘known_hosts’ fields.
|
+provider
+
+string
+
+ |
+
+(Optional)
+ Provider used for authentication, can be ‘azure’, ‘generic’.
+When not specified, defaults to ‘generic’.
+ |
+
+
+
interval
diff --git a/docs/spec/v1/gitrepositories.md b/docs/spec/v1/gitrepositories.md
index 4170d9f1b..e78aee74a 100644
--- a/docs/spec/v1/gitrepositories.md
+++ b/docs/spec/v1/gitrepositories.md
@@ -212,6 +212,91 @@ For password-protected SSH private keys, the password must be provided
via an additional `password` field in the secret. Flux CLI also supports
this via the `--password` flag.
+### Provider
+
+`.spec.provider` is an optional field that allows specifying an OIDC provider
+used for authentication purposes.
+
+Supported options are:
+
+- `generic`
+- `azure`
+
+When provider is not specified, it defaults to `generic` indicating that
+mechanisms using `spec.secretRef` are used for authentication.
+
+#### Azure
+
+The `azure` provider can be used to authenticate to Azure DevOps repositories
+automatically using Workload Identity.
+
+##### Pre-requisites
+
+- Ensure that your Azure DevOps Organization is
+ [connected](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops)
+ to Microsoft Entra.
+- Ensure Workload Identity is properly [set up on your
+ cluster](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster#create-an-aks-cluster).
+
+##### Configure Flux controller
+
+- Create a managed identity to access Azure DevOps. Establish a federated
+ identity credential between the managed identity and the source-controller
+ service account. In the default installation, the source-controller service
+ account is located in the `flux-system` namespace with name
+ `source-controller`. Ensure the federated credential uses the correct
+ namespace and name of the source-controller service account. For more details,
+ please refer to this
+ [guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html#6-establish-federated-identity-credential-between-the-identity-and-the-service-account-issuer--subject).
+
+- Add the managed identity to the Azure DevOps organization as a user. Ensure
+ that the managed identity has the necessary permissions to access the Azure
+ DevOps repository as described
+ [here](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#2-add-and-manage-service-principals-in-an-azure-devops-organization).
+
+- Add the following patch to your bootstrap repository in
+ `flux-system/kustomization.yaml` file:
+
+
+```yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - gotk-components.yaml
+ - gotk-sync.yaml
+patches:
+ - patch: |-
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: source-controller
+ namespace: flux-system
+ annotations:
+ azure.workload.identity/client-id:
+ labels:
+ azure.workload.identity/use: "true"
+ - patch: |-
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: source-controller
+ namespace: flux-system
+ labels:
+ azure.workload.identity/use: "true"
+ spec:
+ template:
+ metadata:
+ labels:
+ azure.workload.identity/use: "true"
+```
+
+**Note:** When azure `provider` is used with `GitRepository`, the `.spec.url`
+must follow this format:
+
+```
+https://dev.azure.com/{your-organization}/{your-project}/_git/{your-repository}
+```
+
### Interval
`.spec.interval` is a required field that specifies the interval at which the
diff --git a/go.mod b/go.mod
index 9386f8588..4d8d4d7da 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
github.com/fluxcd/cli-utils v0.36.0-flux.9
github.com/fluxcd/pkg/apis/event v0.10.0
github.com/fluxcd/pkg/apis/meta v1.6.0
+ github.com/fluxcd/pkg/auth v0.0.0-00010101000000-000000000000
github.com/fluxcd/pkg/git v0.20.0
github.com/fluxcd/pkg/git/gogit v0.20.0
github.com/fluxcd/pkg/gittestserver v0.13.0
@@ -406,3 +407,9 @@ require (
)
retract v0.32.0 // Refers to incorrect ./api version.
+
+replace github.com/fluxcd/pkg/auth => github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028
+
+replace github.com/fluxcd/pkg/git/gogit => github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028
+
+replace github.com/fluxcd/pkg/git => github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028
diff --git a/go.sum b/go.sum
index fea630678..a2c1f6244 100644
--- a/go.sum
+++ b/go.sum
@@ -291,6 +291,12 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
+github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028 h1:Ohfv7mzT0aecvS4DJqDgBCGiKccsH8YcfOJ1fwdWi5g=
+github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028/go.mod h1:0VS8EHPXNoB9q84OJg+t2LlkdIvWzttUPXhSxMKavGk=
+github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028 h1:H9PpGshNFcO5yenhJDJOHXF6x5jgof64YI5l+AYkpEQ=
+github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028/go.mod h1:XTZfxHFy96sbGzbhN68u8+L6IKjqAxLax/dCq9gaUk4=
+github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028 h1:Ekj1aPhfud5phbZq9rwZeN43YE/IL1RzFghoRDJKC6I=
+github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028/go.mod h1:pX0wDKVhNINddJ3vtUS6ripizHTqjc+kk93CLO0UDmM=
github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic=
github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -351,10 +357,6 @@ github.com/fluxcd/pkg/apis/meta v1.6.0 h1:93TcRpiph0OCoQh+cI+PM7E35kBW9dScuas9tW
github.com/fluxcd/pkg/apis/meta v1.6.0/go.mod h1:ZOeHcvyVdZDC5ZOGV7YuwplIvAx6LvmpeyhfTcNZCnc=
github.com/fluxcd/pkg/cache v0.0.3 h1:VK5joG/p+amh5Ob+r1OFOx0cCYiswEf8mX1/J1BG7Mw=
github.com/fluxcd/pkg/cache v0.0.3/go.mod h1:UU6oFhV+mG0A5/RwIlvXhyuKlJwQEkk92jVB3vKMLtk=
-github.com/fluxcd/pkg/git v0.20.0 h1:byUbxLLZ9AyVYmK16mvxY/iA/ZhNwA30GHKPKNh7pik=
-github.com/fluxcd/pkg/git v0.20.0/go.mod h1:YnBOFhX7zzyVjg/u1Et1xBqXs30kb2sWWesIl3/glhw=
-github.com/fluxcd/pkg/git/gogit v0.20.0 h1:ZlWq//I465lv9aEEWaJhjJaTiTtnjcH+Td0fg1rPXWU=
-github.com/fluxcd/pkg/git/gogit v0.20.0/go.mod h1:ZA4WsKr28cj1yuplxOw9vHgCL4OCNJJLib1cJ77Tp9o=
github.com/fluxcd/pkg/gittestserver v0.13.0 h1:6rvD9Z7+4zBcNT+LK0z4H0z6mDaw1Zd8ZaLh/dw8dzI=
github.com/fluxcd/pkg/gittestserver v0.13.0/go.mod h1:LDw32Wo9mTmKNmJq4g7LRVBqPXlpMIWFBDOrRRh/+As=
github.com/fluxcd/pkg/helmtestserver v0.19.0 h1:DbidD46we8iLp/Sxn2TO8twtlP5gxFQaP3XTNJC0bl8=
diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go
index aadbb6267..39e43fa9d 100644
--- a/internal/controller/gitrepository_controller.go
+++ b/internal/controller/gitrepository_controller.go
@@ -27,6 +27,7 @@ import (
"time"
securejoin "github.com/cyphar/filepath-securejoin"
+ "github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/runtime/logger"
"github.com/go-git/go-git/v5/plumbing/transport"
corev1 "k8s.io/api/core/v1"
@@ -647,6 +648,19 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
if err != nil {
return nil, err
}
+
+ // Configure provider authentication if specified in spec
+ if obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.GitProviderGeneric {
+ if obj.Spec.Provider == sourcev1.GitProviderAzure {
+ authOpts.ProviderOpts = &git.ProviderOptions{
+ Name: obj.Spec.Provider,
+ AzureOpts: []azure.OptFunc{
+ azure.WithAzureDevOpsScope(),
+ },
+ }
+ }
+ }
+
return authOpts, nil
}
diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go
index 800c65577..a81235553 100644
--- a/internal/controller/gitrepository_controller_test.go
+++ b/internal/controller/gitrepository_controller_test.go
@@ -683,6 +683,54 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
}
}
+func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) {
+ tests := []struct {
+ name string
+ beforeFunc func(obj *sourcev1.GitRepository)
+ wantProviderOptsName string
+ }{
+ {
+ name: "azure provider",
+ beforeFunc: func(obj *sourcev1.GitRepository) {
+ obj.Spec.Provider = sourcev1.GitProviderAzure
+ },
+ wantProviderOptsName: sourcev1.GitProviderAzure,
+ },
+ {
+ name: "generic provider",
+ beforeFunc: func(obj *sourcev1.GitRepository) {
+ obj.Spec.Provider = sourcev1.GitProviderGeneric
+ },
+ },
+ {
+ name: "no provider",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ obj := &sourcev1.GitRepository{}
+ r := &GitRepositoryReconciler{}
+ url, _ := url.Parse("https://dev.azure.com/foo/bar/_git/baz")
+
+ if tt.beforeFunc != nil {
+ tt.beforeFunc(obj)
+ }
+ opts, err := r.getAuthOpts(context.TODO(), obj, *url)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(opts).ToNot(BeNil())
+ if tt.wantProviderOptsName != "" {
+ g.Expect(opts.ProviderOpts).ToNot(BeNil())
+ g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName))
+ } else {
+ g.Expect(opts.ProviderOpts).To(BeNil())
+ }
+ })
+ }
+}
+
func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) {
g := NewWithT(t)
|