[RFC-0010] Add multi-tenant workload identity support for Azure GitRepository
Signed-off-by: Dipti Pai <diptipai89@outlook.com>
This commit is contained in:
parent
5f9702bb01
commit
4fe3434ee8
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -413,6 +413,19 @@ When not specified, defaults to ‘generic’.</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 ‘azure’ 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 ‘generic’.</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 ‘azure’ 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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue