Skip to content

Commit 96c2e90

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 <[email protected]>
1 parent 7318343 commit 96c2e90

File tree

8 files changed

+197
-10
lines changed

8 files changed

+197
-10
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

+66
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,72 @@ 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 [connected to Microsoft
236+
Entra](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops)
237+
- Ensure Workload Identity is properly set up on your cluster and the mutating
238+
webhook is installed.
239+
- Create a managed identity and federated identity between the source-controller
240+
service account and managed identity. Please take a look at this
241+
[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)
242+
- Ensure that the managed identity has required permissions to access the Azure
243+
DevOps repository as described
244+
[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)
245+
246+
Add the following patch in `flux-system/kustomization.yaml` file:
247+
248+
```yaml
249+
apiVersion: kustomize.config.k8s.io/v1beta1
250+
kind: Kustomization
251+
resources:
252+
- gotk-components.yaml
253+
- gotk-sync.yaml
254+
patches:
255+
- patch: |-
256+
apiVersion: v1
257+
kind: ServiceAccount
258+
metadata:
259+
name: source-controller
260+
namespace: flux-system
261+
annotations:
262+
azure.workload.identity/client-id: <AZURE_CLIENT_ID>
263+
labels:
264+
azure.workload.identity/use: "true"
265+
- patch: |-
266+
apiVersion: apps/v1
267+
kind: Deployment
268+
metadata:
269+
name: source-controller
270+
namespace: flux-system
271+
labels:
272+
azure.workload.identity/use: "true"
273+
spec:
274+
template:
275+
metadata:
276+
labels:
277+
azure.workload.identity/use: "true"
278+
```
279+
280+
215281
### Interval
216282

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

go.mod

+9-2
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
@@ -179,7 +180,7 @@ require (
179180
github.com/felixge/httpsnoop v1.0.4 // indirect
180181
github.com/fluxcd/gitkit v0.6.0 // indirect
181182
github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect
182-
github.com/fluxcd/pkg/cache v0.0.2 // indirect
183+
github.com/fluxcd/pkg/cache v0.0.3 // indirect
183184
github.com/fsnotify/fsnotify v1.7.0 // indirect
184185
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
185186
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
@@ -373,7 +374,7 @@ require (
373374
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
374375
golang.org/x/mod v0.20.0 // indirect
375376
golang.org/x/net v0.28.0 // indirect
376-
golang.org/x/sys v0.24.0 // indirect
377+
golang.org/x/sys v0.25.0 // indirect
377378
golang.org/x/term v0.23.0 // indirect
378379
golang.org/x/text v0.17.0 // indirect
379380
golang.org/x/time v0.6.0 // indirect
@@ -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-20240906172542-16adfc971f2e
412+
413+
replace github.com/fluxcd/pkg/git => github.com/dipti-pai/pkg/git v0.0.0-20240906172542-16adfc971f2e
414+
415+
replace github.com/fluxcd/pkg/git/gogit => github.com/dipti-pai/pkg/git/gogit v0.0.0-20240906172542-16adfc971f2e

go.sum

+10-8
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-20240906172542-16adfc971f2e h1:2nIwoRVKfcm8a5vJXtcY3hhvJrSHzJwIfZiFKS685sM=
295+
github.com/dipti-pai/pkg/auth v0.0.0-20240906172542-16adfc971f2e/go.mod h1:0VS8EHPXNoB9q84OJg+t2LlkdIvWzttUPXhSxMKavGk=
296+
github.com/dipti-pai/pkg/git v0.0.0-20240906172542-16adfc971f2e h1:fdh8qioq01d2F6TV2/KnFglyhDQ+3fU8QC/omPMHTJA=
297+
github.com/dipti-pai/pkg/git v0.0.0-20240906172542-16adfc971f2e/go.mod h1:RDOIm/0qU6akK3F7JLcNY+TxaNJQOTuz3MIc/aXXPfU=
298+
github.com/dipti-pai/pkg/git/gogit v0.0.0-20240906172542-16adfc971f2e h1:1z0CTzAuJRvb8JjzmRZ0GyndgzYOObxXIZiSI39UUP4=
299+
github.com/dipti-pai/pkg/git/gogit v0.0.0-20240906172542-16adfc971f2e/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=
@@ -349,12 +355,8 @@ github.com/fluxcd/pkg/apis/event v0.10.0 h1:eMYXjMnLQ9jctPkTauuiBmEI127RjCKDf1zf
349355
github.com/fluxcd/pkg/apis/event v0.10.0/go.mod h1:pG/3gbSBLNy6YGZP2eajiyVgkEQDvva789t46PY6NFE=
350356
github.com/fluxcd/pkg/apis/meta v1.6.0 h1:93TcRpiph0OCoQh+cI+PM7E35kBW9dScuas9tWc90Dw=
351357
github.com/fluxcd/pkg/apis/meta v1.6.0/go.mod h1:ZOeHcvyVdZDC5ZOGV7YuwplIvAx6LvmpeyhfTcNZCnc=
352-
github.com/fluxcd/pkg/cache v0.0.2 h1:+x1VCNDQbTQ5AbrOpMH3ps3NGek+qt52+6z7UjUP818=
353-
github.com/fluxcd/pkg/cache v0.0.2/go.mod h1:Xo09Wdo2YIiqyNrQbwvp83hIzxevznsvhcy+6xFjbcM=
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=
358+
github.com/fluxcd/pkg/cache v0.0.3 h1:VK5joG/p+amh5Ob+r1OFOx0cCYiswEf8mX1/J1BG7Mw=
359+
github.com/fluxcd/pkg/cache v0.0.3/go.mod h1:UU6oFhV+mG0A5/RwIlvXhyuKlJwQEkk92jVB3vKMLtk=
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=
@@ -1212,8 +1214,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
12121214
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
12131215
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
12141216
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1215-
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
1216-
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
1217+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
1218+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
12171219
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
12181220
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
12191221
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

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)