Skip to content

Commit 9f129ce

Browse files
committed
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 <diptipai89@outlook.com>
1 parent dd144ac commit 9f129ce

File tree

8 files changed

+207
-4
lines changed

8 files changed

+207
-4
lines changed

api/v1/gitrepository_types.go

+15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ import (
2727
const (
2828
// GitRepositoryKind is the string representation of a GitRepository.
2929
GitRepositoryKind = "GitRepository"
30+
31+
// GitProviderGeneric provides support for authentication using
32+
// credentials specified in secretRef.
33+
GitProviderGeneric string = "generic"
34+
35+
// GitProviderAzure provides support for authentication to azure
36+
// repositories using Managed Identity.
37+
GitProviderAzure string = "azure"
3038
)
3139

3240
const (
@@ -80,6 +88,13 @@ type GitRepositorySpec struct {
8088
// +optional
8189
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
8290

91+
// Provider used for authentication, can be 'azure', 'generic'.
92+
// When not specified, defaults to 'generic'.
93+
// +kubebuilder:validation:Enum=generic;azure
94+
// +kubebuilder:default:=generic
95+
// +optional
96+
Provider string `json:"provider,omitempty"`
97+
8398
// Interval at which the GitRepository URL is checked for updates.
8499
// This interval is approximate and may be subject to jitter to ensure
85100
// efficient use of resources.

config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ spec:
103103
efficient use of resources.
104104
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
105105
type: string
106+
provider:
107+
default: generic
108+
description: |-
109+
Provider used for authentication, can be 'azure', 'generic'.
110+
When not specified, defaults to 'generic'.
111+
enum:
112+
- generic
113+
- azure
114+
type: string
106115
proxySecretRef:
107116
description: |-
108117
ProxySecretRef specifies the Secret containing the proxy configuration

docs/api/v1/source.md

+26
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,19 @@ and &lsquo;known_hosts&rsquo; fields.</p>
383383
</tr>
384384
<tr>
385385
<td>
386+
<code>provider</code><br>
387+
<em>
388+
string
389+
</em>
390+
</td>
391+
<td>
392+
<em>(Optional)</em>
393+
<p>Provider used for authentication, can be &lsquo;azure&rsquo;, &lsquo;generic&rsquo;.
394+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
395+
</td>
396+
</tr>
397+
<tr>
398+
<td>
386399
<code>interval</code><br>
387400
<em>
388401
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
@@ -1710,6 +1723,19 @@ and &lsquo;known_hosts&rsquo; fields.</p>
17101723
</tr>
17111724
<tr>
17121725
<td>
1726+
<code>provider</code><br>
1727+
<em>
1728+
string
1729+
</em>
1730+
</td>
1731+
<td>
1732+
<em>(Optional)</em>
1733+
<p>Provider used for authentication, can be &lsquo;azure&rsquo;, &lsquo;generic&rsquo;.
1734+
When not specified, defaults to &lsquo;generic&rsquo;.</p>
1735+
</td>
1736+
</tr>
1737+
<tr>
1738+
<td>
17131739
<code>interval</code><br>
17141740
<em>
17151741
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">

docs/spec/v1/gitrepositories.md

+82
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,88 @@ For password-protected SSH private keys, the password must be provided
212212
via an additional `password` field in the secret. Flux CLI also supports
213213
this via the `--password` flag.
214214

215+
### Provider
216+
217+
`.spec.provider` is an optional field that allows specifying an OIDC provider
218+
used for authentication purposes.
219+
220+
Supported options are:
221+
222+
- `generic`
223+
- `azure`
224+
225+
When provider is not specified, it defaults to `generic` indicating that
226+
mechanisms using `spec.secretRef` are used for authentication.
227+
228+
#### Azure
229+
230+
The `azure` provider can be used to authenticate to Azure DevOps repositories
231+
automatically using Workload Identity.
232+
233+
##### Pre-requisites
234+
235+
- Ensure that your Azure DevOps Organization is
236+
[connected](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops)
237+
to Microsoft Entra
238+
- Ensure Workload Identity is properly [set up on your
239+
cluster](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster#create-an-aks-cluster)
240+
241+
##### Configure Flux controller
242+
243+
- Create a managed identity to access Azure DevOps. Establish a federated
244+
identity credential between the managed identity and the source-controller
245+
service account. In the default installation, the source-controller service
246+
account is located in the `flux-system` namespace with name
247+
`source-controller`. Ensure the federated credential uses the correct
248+
namespace and name of the source-controller service account. For more details,
249+
please refer to this
250+
[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)
251+
252+
- Add the managed identity to the Azure DevOps organization as a user. Ensure
253+
that the managed identity has the necessary permissions to access the Azure
254+
DevOps repository as described
255+
[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)
256+
257+
- Add the following patch in `flux-system/kustomization.yaml` file:
258+
259+
260+
```yaml
261+
apiVersion: kustomize.config.k8s.io/v1beta1
262+
kind: Kustomization
263+
resources:
264+
- gotk-components.yaml
265+
- gotk-sync.yaml
266+
patches:
267+
- patch: |-
268+
apiVersion: v1
269+
kind: ServiceAccount
270+
metadata:
271+
name: source-controller
272+
namespace: flux-system
273+
annotations:
274+
azure.workload.identity/client-id: <AZURE_CLIENT_ID>
275+
labels:
276+
azure.workload.identity/use: "true"
277+
- patch: |-
278+
apiVersion: apps/v1
279+
kind: Deployment
280+
metadata:
281+
name: source-controller
282+
namespace: flux-system
283+
labels:
284+
azure.workload.identity/use: "true"
285+
spec:
286+
template:
287+
metadata:
288+
labels:
289+
azure.workload.identity/use: "true"
290+
```
291+
292+
**Note:** When `provider` is used with `GitRepository`, the `.spec.url` must
293+
follow this format:
294+
295+
> https://dev.azure.com/{your-organization}/{your-project}/_git/{your-repository}.
296+
215297
### Interval
216298

217299
`.spec.interval` is a required field that specifies the interval at which the

go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
github.com/fluxcd/cli-utils v0.36.0-flux.9
2525
github.com/fluxcd/pkg/apis/event v0.10.0
2626
github.com/fluxcd/pkg/apis/meta v1.6.0
27+
github.com/fluxcd/pkg/auth v0.0.0-00010101000000-000000000000
2728
github.com/fluxcd/pkg/git v0.20.0
2829
github.com/fluxcd/pkg/git/gogit v0.20.0
2930
github.com/fluxcd/pkg/gittestserver v0.13.0
@@ -406,3 +407,9 @@ require (
406407
)
407408

408409
retract v0.32.0 // Refers to incorrect ./api version.
410+
411+
replace github.com/fluxcd/pkg/auth => github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028
412+
413+
replace github.com/fluxcd/pkg/git/gogit => github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028
414+
415+
replace github.com/fluxcd/pkg/git => github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028

go.sum

+6-4
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1G
291291
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
292292
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
293293
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
294+
github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028 h1:Ohfv7mzT0aecvS4DJqDgBCGiKccsH8YcfOJ1fwdWi5g=
295+
github.com/dipti-pai/pkg/auth v0.0.0-20240910203859-abee735aa028/go.mod h1:0VS8EHPXNoB9q84OJg+t2LlkdIvWzttUPXhSxMKavGk=
296+
github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028 h1:H9PpGshNFcO5yenhJDJOHXF6x5jgof64YI5l+AYkpEQ=
297+
github.com/dipti-pai/pkg/git v0.0.0-20240910203859-abee735aa028/go.mod h1:XTZfxHFy96sbGzbhN68u8+L6IKjqAxLax/dCq9gaUk4=
298+
github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028 h1:Ekj1aPhfud5phbZq9rwZeN43YE/IL1RzFghoRDJKC6I=
299+
github.com/dipti-pai/pkg/git/gogit v0.0.0-20240910203859-abee735aa028/go.mod h1:pX0wDKVhNINddJ3vtUS6ripizHTqjc+kk93CLO0UDmM=
294300
github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic=
295301
github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs=
296302
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
351357
github.com/fluxcd/pkg/apis/meta v1.6.0/go.mod h1:ZOeHcvyVdZDC5ZOGV7YuwplIvAx6LvmpeyhfTcNZCnc=
352358
github.com/fluxcd/pkg/cache v0.0.3 h1:VK5joG/p+amh5Ob+r1OFOx0cCYiswEf8mX1/J1BG7Mw=
353359
github.com/fluxcd/pkg/cache v0.0.3/go.mod h1:UU6oFhV+mG0A5/RwIlvXhyuKlJwQEkk92jVB3vKMLtk=
354-
github.com/fluxcd/pkg/git v0.20.0 h1:byUbxLLZ9AyVYmK16mvxY/iA/ZhNwA30GHKPKNh7pik=
355-
github.com/fluxcd/pkg/git v0.20.0/go.mod h1:YnBOFhX7zzyVjg/u1Et1xBqXs30kb2sWWesIl3/glhw=
356-
github.com/fluxcd/pkg/git/gogit v0.20.0 h1:ZlWq//I465lv9aEEWaJhjJaTiTtnjcH+Td0fg1rPXWU=
357-
github.com/fluxcd/pkg/git/gogit v0.20.0/go.mod h1:ZA4WsKr28cj1yuplxOw9vHgCL4OCNJJLib1cJ77Tp9o=
358360
github.com/fluxcd/pkg/gittestserver v0.13.0 h1:6rvD9Z7+4zBcNT+LK0z4H0z6mDaw1Zd8ZaLh/dw8dzI=
359361
github.com/fluxcd/pkg/gittestserver v0.13.0/go.mod h1:LDw32Wo9mTmKNmJq4g7LRVBqPXlpMIWFBDOrRRh/+As=
360362
github.com/fluxcd/pkg/helmtestserver v0.19.0 h1:DbidD46we8iLp/Sxn2TO8twtlP5gxFQaP3XTNJC0bl8=

internal/controller/gitrepository_controller.go

+14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"time"
2828

2929
securejoin "github.com/cyphar/filepath-securejoin"
30+
"github.com/fluxcd/pkg/auth/azure"
3031
"github.com/fluxcd/pkg/runtime/logger"
3132
"github.com/go-git/go-git/v5/plumbing/transport"
3233
corev1 "k8s.io/api/core/v1"
@@ -647,6 +648,19 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1
647648
if err != nil {
648649
return nil, err
649650
}
651+
652+
// Configure provider authentication if specified in spec
653+
if obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.GitProviderGeneric {
654+
if obj.Spec.Provider == sourcev1.GitProviderAzure {
655+
authOpts.ProviderOpts = &git.ProviderOptions{
656+
Name: obj.Spec.Provider,
657+
AzureOpts: []azure.OptFunc{
658+
azure.WithAzureDevOpsScope(),
659+
},
660+
}
661+
}
662+
}
663+
650664
return authOpts, nil
651665
}
652666

internal/controller/gitrepository_controller_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,54 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
683683
}
684684
}
685685

686+
func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) {
687+
tests := []struct {
688+
name string
689+
beforeFunc func(obj *sourcev1.GitRepository)
690+
wantProviderOptsName string
691+
}{
692+
{
693+
name: "azure provider",
694+
beforeFunc: func(obj *sourcev1.GitRepository) {
695+
obj.Spec.Provider = sourcev1.GitProviderAzure
696+
},
697+
wantProviderOptsName: sourcev1.GitProviderAzure,
698+
},
699+
{
700+
name: "generic provider",
701+
beforeFunc: func(obj *sourcev1.GitRepository) {
702+
obj.Spec.Provider = sourcev1.GitProviderGeneric
703+
},
704+
},
705+
{
706+
name: "no provider",
707+
},
708+
}
709+
710+
for _, tt := range tests {
711+
t.Run(tt.name, func(t *testing.T) {
712+
g := NewWithT(t)
713+
obj := &sourcev1.GitRepository{}
714+
r := &GitRepositoryReconciler{}
715+
url, _ := url.Parse("https://dev.azure.com/foo/bar/_git/baz")
716+
717+
if tt.beforeFunc != nil {
718+
tt.beforeFunc(obj)
719+
}
720+
opts, err := r.getAuthOpts(context.TODO(), obj, *url)
721+
722+
g.Expect(err).ToNot(HaveOccurred())
723+
g.Expect(opts).ToNot(BeNil())
724+
if tt.wantProviderOptsName != "" {
725+
g.Expect(opts.ProviderOpts).ToNot(BeNil())
726+
g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName))
727+
} else {
728+
g.Expect(opts.ProviderOpts).To(BeNil())
729+
}
730+
})
731+
}
732+
}
733+
686734
func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) {
687735
g := NewWithT(t)
688736

0 commit comments

Comments
 (0)