Enable contextual login for helm OCI
If implemented, this pr will enable user to use the auto login feature in order to automatically login to their provider of choice's container registry (i.e. aws, gcr, acr). Signed-off-by: Soule BA <soule@weave.works>
This commit is contained in:
parent
2010eef374
commit
ad3eb5ca47
|
@ -68,7 +68,9 @@ type HelmRepositorySpec struct {
|
||||||
// +required
|
// +required
|
||||||
Interval metav1.Duration `json:"interval"`
|
Interval metav1.Duration `json:"interval"`
|
||||||
|
|
||||||
// Timeout of the index fetch operation, defaults to 60s.
|
// Timeout is used for the index fetch operation for an HTTPS helm repository,
|
||||||
|
// and for remote OCI Repository operations like pulling for an OCI helm repository.
|
||||||
|
// Its default value is 60s.
|
||||||
// +kubebuilder:default:="60s"
|
// +kubebuilder:default:="60s"
|
||||||
// +optional
|
// +optional
|
||||||
Timeout *metav1.Duration `json:"timeout,omitempty"`
|
Timeout *metav1.Duration `json:"timeout,omitempty"`
|
||||||
|
@ -89,6 +91,14 @@ type HelmRepositorySpec struct {
|
||||||
// +kubebuilder:validation:Enum=default;oci
|
// +kubebuilder:validation:Enum=default;oci
|
||||||
// +optional
|
// +optional
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
|
|
||||||
|
// Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
|
||||||
|
// This field is optional, and only taken into account if the .spec.type field is set to 'oci'.
|
||||||
|
// When not specified, defaults to 'generic'.
|
||||||
|
// +kubebuilder:validation:Enum=generic;aws;azure;gcp
|
||||||
|
// +kubebuilder:default:=generic
|
||||||
|
// +optional
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelmRepositoryStatus records the observed state of the HelmRepository.
|
// HelmRepositoryStatus records the observed state of the HelmRepository.
|
||||||
|
|
|
@ -310,6 +310,18 @@ spec:
|
||||||
be done with caution, as it can potentially result in credentials
|
be done with caution, as it can potentially result in credentials
|
||||||
getting stolen in a MITM-attack.
|
getting stolen in a MITM-attack.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
provider:
|
||||||
|
default: generic
|
||||||
|
description: Provider used for authentication, can be 'aws', 'azure',
|
||||||
|
'gcp' or 'generic'. This field is optional, and only taken into
|
||||||
|
account if the .spec.type field is set to 'oci'. When not specified,
|
||||||
|
defaults to 'generic'.
|
||||||
|
enum:
|
||||||
|
- generic
|
||||||
|
- aws
|
||||||
|
- azure
|
||||||
|
- gcp
|
||||||
|
type: string
|
||||||
secretRef:
|
secretRef:
|
||||||
description: SecretRef specifies the Secret containing authentication
|
description: SecretRef specifies the Secret containing authentication
|
||||||
credentials for the HelmRepository. For HTTP/S basic auth the secret
|
credentials for the HelmRepository. For HTTP/S basic auth the secret
|
||||||
|
@ -328,7 +340,9 @@ spec:
|
||||||
type: boolean
|
type: boolean
|
||||||
timeout:
|
timeout:
|
||||||
default: 60s
|
default: 60s
|
||||||
description: Timeout of the index fetch operation, defaults to 60s.
|
description: Timeout is used for the index fetch operation for an
|
||||||
|
HTTPS helm repository, and for remote OCI Repository operations
|
||||||
|
like pulling for an OCI helm repository. Its default value is 60s.
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
description: Type of the HelmRepository. When this field is set to "oci",
|
description: Type of the HelmRepository. When this field is set to "oci",
|
||||||
|
|
|
@ -50,6 +50,7 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/fluxcd/pkg/oci"
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||||
"github.com/fluxcd/pkg/runtime/events"
|
"github.com/fluxcd/pkg/runtime/events"
|
||||||
|
@ -463,6 +464,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
loginOpts []helmreg.LoginOption
|
loginOpts []helmreg.LoginOption
|
||||||
)
|
)
|
||||||
|
// Used to login with the repository declared provider
|
||||||
|
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
normalizedURL := repository.NormalizeURL(repo.Spec.URL)
|
normalizedURL := repository.NormalizeURL(repo.Spec.URL)
|
||||||
// Construct the Getter options from the HelmRepository data
|
// Construct the Getter options from the HelmRepository data
|
||||||
|
@ -521,6 +525,21 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuth(ctxTimeout, repo)
|
||||||
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
|
e := &serror.Event{
|
||||||
|
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
|
||||||
|
Reason: sourcev1.AuthenticationFailedReason,
|
||||||
|
}
|
||||||
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
if auth != nil {
|
||||||
|
loginOpts = append([]helmreg.LoginOption{}, auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the chart repository
|
// Initialize the chart repository
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
switch repo.Spec.Type {
|
switch repo.Spec.Type {
|
||||||
|
@ -947,6 +966,11 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to login with the repository declared provider
|
||||||
|
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
clientOpts := []helmgetter.Option{
|
clientOpts := []helmgetter.Option{
|
||||||
helmgetter.WithURL(normalizedURL),
|
helmgetter.WithURL(normalizedURL),
|
||||||
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
|
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
|
||||||
|
@ -976,6 +1000,16 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuth(ctxTimeout, repo)
|
||||||
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
|
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
|
||||||
|
}
|
||||||
|
if auth != nil {
|
||||||
|
loginOpts = append([]helmreg.LoginOption{}, auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
if helmreg.IsOCI(normalizedURL) {
|
if helmreg.IsOCI(normalizedURL) {
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil)
|
||||||
|
|
|
@ -1085,9 +1085,10 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
|
||||||
GenerateName: "helmrepository-",
|
GenerateName: "helmrepository-",
|
||||||
},
|
},
|
||||||
Spec: sourcev1.HelmRepositorySpec{
|
Spec: sourcev1.HelmRepositorySpec{
|
||||||
URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost),
|
URL: fmt.Sprintf("oci://%s/testrepo", testRegistryServer.registryHost),
|
||||||
Timeout: &metav1.Duration{Duration: timeout},
|
Timeout: &metav1.Duration{Duration: timeout},
|
||||||
Type: sourcev1.HelmRepositoryTypeOCI,
|
Provider: sourcev1.GenericOCIProvider,
|
||||||
|
Type: sourcev1.HelmRepositoryTypeOCI,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
obj := &sourcev1.HelmChart{
|
obj := &sourcev1.HelmChart{
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
|
@ -41,10 +42,13 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/fluxcd/pkg/oci"
|
||||||
|
"github.com/fluxcd/pkg/oci/auth/login"
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
"github.com/fluxcd/pkg/runtime/predicates"
|
"github.com/fluxcd/pkg/runtime/predicates"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/api/v1beta2"
|
"github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
@ -204,6 +208,9 @@ func (r *HelmRepositoryOCIReconciler) Reconcile(ctx context.Context, req ctrl.Re
|
||||||
// block at the very end to summarize the conditions to be in a consistent
|
// block at the very end to summarize the conditions to be in a consistent
|
||||||
// state.
|
// state.
|
||||||
func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta2.HelmRepository) (result ctrl.Result, retErr error) {
|
func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta2.HelmRepository) (result ctrl.Result, retErr error) {
|
||||||
|
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
oldObj := obj.DeepCopy()
|
oldObj := obj.DeepCopy()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -296,6 +303,19 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
auth, authErr := oidcAuth(ctxTimeout, obj)
|
||||||
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
|
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
||||||
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
result, retErr = ctrl.Result{}, e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if auth != nil {
|
||||||
|
loginOpts = append(loginOpts, auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create registry client and login if needed.
|
// Create registry client and login if needed.
|
||||||
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
|
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -366,3 +386,42 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
|
||||||
}
|
}
|
||||||
r.Eventf(obj, eventType, reason, msg)
|
r.Eventf(obj, eventType, reason, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
||||||
|
func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) {
|
||||||
|
url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
|
||||||
|
ref, err := name.ParseReference(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginOpt, err := loginWithManager(ctx, obj.Spec.Provider, url, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginOpt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithManager(ctx context.Context, provider, url string, ref name.Reference) (helmreg.LoginOption, error) {
|
||||||
|
opts := login.ProviderOptions{}
|
||||||
|
switch provider {
|
||||||
|
case sourcev1.AmazonOCIProvider:
|
||||||
|
opts.AwsAutoLogin = true
|
||||||
|
case sourcev1.AzureOCIProvider:
|
||||||
|
opts.AzureAutoLogin = true
|
||||||
|
case sourcev1.GoogleOCIProvider:
|
||||||
|
opts.GcpAutoLogin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := login.NewManager().Login(ctx, url, ref, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.OIDCAdaptHelper(auth)
|
||||||
|
}
|
||||||
|
|
|
@ -94,7 +94,8 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
||||||
SecretRef: &meta.LocalObjectReference{
|
SecretRef: &meta.LocalObjectReference{
|
||||||
Name: secret.Name,
|
Name: secret.Name,
|
||||||
},
|
},
|
||||||
Type: sourcev1.HelmRepositoryTypeOCI,
|
Provider: sourcev1.GenericOCIProvider,
|
||||||
|
Type: sourcev1.HelmRepositoryTypeOCI,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
||||||
|
|
|
@ -818,7 +818,9 @@ Kubernetes meta/v1.Duration
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
<p>Timeout of the index fetch operation, defaults to 60s.</p>
|
<p>Timeout is used for the index fetch operation for an HTTPS helm repository,
|
||||||
|
and for remote OCI Repository operations like pulling for an OCI helm repository.
|
||||||
|
Its default value is 60s.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -863,6 +865,20 @@ string
|
||||||
When this field is set to “oci”, the URL field value must be prefixed with “oci://”.</p>
|
When this field is set to “oci”, the URL field value must be prefixed with “oci://”.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>provider</code><br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’.
|
||||||
|
This field is optional, and only taken into account if the .spec.type field is set to ‘oci’.
|
||||||
|
When not specified, defaults to ‘generic’.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -2347,7 +2363,9 @@ Kubernetes meta/v1.Duration
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
<p>Timeout of the index fetch operation, defaults to 60s.</p>
|
<p>Timeout is used for the index fetch operation for an HTTPS helm repository,
|
||||||
|
and for remote OCI Repository operations like pulling for an OCI helm repository.
|
||||||
|
Its default value is 60s.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -2392,6 +2410,20 @@ string
|
||||||
When this field is set to “oci”, the URL field value must be prefixed with “oci://”.</p>
|
When this field is set to “oci”, the URL field value must be prefixed with “oci://”.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>provider</code><br>
|
||||||
|
<em>
|
||||||
|
string
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Provider used for authentication, can be ‘aws’, ‘azure’, ‘gcp’ or ‘generic’.
|
||||||
|
This field is optional, and only taken into account if the .spec.type field is set to ‘oci’.
|
||||||
|
When not specified, defaults to ‘generic’.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -162,6 +162,134 @@ A HelmRepository also needs a
|
||||||
|
|
||||||
Possible values are `default` for a Helm HTTP/S repository, or `oci` for an OCI Helm repository.
|
Possible values are `default` for a Helm HTTP/S repository, or `oci` for an OCI Helm repository.
|
||||||
|
|
||||||
|
|
||||||
|
### Provider
|
||||||
|
|
||||||
|
`.spec.provider` is an optional field that allows specifying an OIDC provider used
|
||||||
|
for authentication purposes.
|
||||||
|
|
||||||
|
Supported options are:
|
||||||
|
- `generic`
|
||||||
|
- `aws`
|
||||||
|
- `azure`
|
||||||
|
- `gcp`
|
||||||
|
|
||||||
|
The `generic` provider can be used for public repositories or when static credentials
|
||||||
|
are used for authentication. If you do not specify `.spec.provider`, it defaults
|
||||||
|
to `generic`.
|
||||||
|
|
||||||
|
**Note**: The provider field is supported only for Helm OCI repositories. The `spec.type`
|
||||||
|
field must be set to `oci`.
|
||||||
|
|
||||||
|
#### AWS
|
||||||
|
|
||||||
|
The `aws` provider can be used to authenticate automatically using the EKS worker
|
||||||
|
node IAM role or IAM Role for Service Accounts (IRSA), and by extension gain access
|
||||||
|
to ECR.
|
||||||
|
|
||||||
|
When the worker node IAM role has access to ECR, source-controller running on it
|
||||||
|
will also have access to ECR.
|
||||||
|
|
||||||
|
When using IRSA to enable access to ECR, add the following patch to your bootstrap
|
||||||
|
repository, in the `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
|
||||||
|
annotations:
|
||||||
|
eks.amazonaws.com/role-arn: <role arn>
|
||||||
|
target:
|
||||||
|
kind: ServiceAccount
|
||||||
|
name: source-controller
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that you can attach the AWS managed policy `arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly`
|
||||||
|
to the IAM role when using IRSA.
|
||||||
|
|
||||||
|
#### Azure
|
||||||
|
|
||||||
|
The `azure` provider can be used to authenticate automatically using kubelet managed
|
||||||
|
identity or Azure Active Directory pod-managed identity (aad-pod-identity), and
|
||||||
|
by extension gain access to ACR.
|
||||||
|
|
||||||
|
When the kubelet managed identity has access to ACR, source-controller running on
|
||||||
|
it will also have access to ACR.
|
||||||
|
|
||||||
|
When using aad-pod-identity to enable access to ACR, add the following patch to
|
||||||
|
your bootstrap repository, in the `flux-system/kustomization.yaml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- gotk-components.yaml
|
||||||
|
- gotk-sync.yaml
|
||||||
|
patches:
|
||||||
|
- patch: |
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/metadata/labels/aadpodidbinding
|
||||||
|
value: <identity-name>
|
||||||
|
target:
|
||||||
|
kind: Deployment
|
||||||
|
name: source-controller
|
||||||
|
```
|
||||||
|
|
||||||
|
When using pod-managed identity on an AKS cluster, AAD Pod Identity has to be used
|
||||||
|
to give the `source-controller` pod access to the ACR. To do this, you have to install
|
||||||
|
`aad-pod-identity` on your cluster, create a managed identity that has access to the
|
||||||
|
container registry (this can also be the Kubelet identity if it has `AcrPull` role
|
||||||
|
assignment on the ACR), create an `AzureIdentity` and `AzureIdentityBinding` that describe
|
||||||
|
the managed identity and then label the `source-controller` pods with the name of the
|
||||||
|
AzureIdentity as shown in the patch above. Please take a look at [this guide](https://azure.github.io/aad-pod-identity/docs/)
|
||||||
|
or [this one](https://docs.microsoft.com/en-us/azure/aks/use-azure-ad-pod-identity)
|
||||||
|
if you want to use AKS pod-managed identities add-on that is in preview.
|
||||||
|
|
||||||
|
#### GCP
|
||||||
|
|
||||||
|
The `gcp` provider can be used to authenticate automatically using OAuth scopes or
|
||||||
|
Workload Identity, and by extension gain access to GCR or Artifact Registry.
|
||||||
|
|
||||||
|
When the GKE nodes have the appropriate OAuth scope for accessing GCR and Artifact Registry,
|
||||||
|
source-controller running on it will also have access to them.
|
||||||
|
|
||||||
|
When using Workload Identity to enable access to GCR or Artifact Registry, add the
|
||||||
|
following patch to your bootstrap repository, in the `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
|
||||||
|
annotations:
|
||||||
|
iam.gke.io/gcp-service-account: <identity-name>
|
||||||
|
target:
|
||||||
|
kind: ServiceAccount
|
||||||
|
name: source-controller
|
||||||
|
```
|
||||||
|
|
||||||
|
The Artifact Registry service uses the permission `artifactregistry.repositories.downloadArtifacts`
|
||||||
|
that is located under the Artifact Registry Reader role. If you are using Google Container Registry service,
|
||||||
|
the needed permission is instead `storage.objects.list` which can be bound as part
|
||||||
|
of the Container Registry Service Agent role. Take a look at [this guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)
|
||||||
|
for more information about setting up GKE Workload Identity.
|
||||||
|
|
||||||
### Interval
|
### Interval
|
||||||
|
|
||||||
`.spec.interval` is a required field that specifies the interval which the
|
`.spec.interval` is a required field that specifies the interval which the
|
||||||
|
|
|
@ -161,7 +161,7 @@ and by extension gain access to ACR.
|
||||||
When the kubelet managed identity has access to ACR, source-controller running
|
When the kubelet managed identity has access to ACR, source-controller running
|
||||||
on it will also have access to ACR.
|
on it will also have access to ACR.
|
||||||
|
|
||||||
When using aad-pod-identity to enable access to ECR, add the following patch to
|
When using aad-pod-identity to enable access to ACR, add the following patch to
|
||||||
your bootstrap repository, in the `flux-system/kustomization.yaml` file:
|
your bootstrap repository, in the `flux-system/kustomization.yaml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
@ -68,3 +69,25 @@ func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.L
|
||||||
}
|
}
|
||||||
return registry.LoginOptBasicAuth(username, password), nil
|
return registry.LoginOptBasicAuth(username, password), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDCAdaptHelper returns an ORAS credentials callback configured with the authorization data
|
||||||
|
// from the given authn authenticator. This allows for example to make use of credential helpers from
|
||||||
|
// cloud providers.
|
||||||
|
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
|
||||||
|
func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, error) {
|
||||||
|
authConfig, err := authenticator.Authorization()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := authConfig.Username
|
||||||
|
password := authConfig.Password
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case username == "" && password == "":
|
||||||
|
return nil, nil
|
||||||
|
case username == "" || password == "":
|
||||||
|
return nil, fmt.Errorf("invalid auth data: required fields 'username' and 'password'")
|
||||||
|
}
|
||||||
|
return registry.LoginOptBasicAuth(username, password), nil
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package registry
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
@ -129,3 +130,54 @@ func TestLoginOptionFromSecret(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOIDCAdaptHelper(t *testing.T) {
|
||||||
|
auth := &authn.Basic{
|
||||||
|
Username: "flux",
|
||||||
|
Password: "flux_password",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
auth authn.Authenticator
|
||||||
|
expectedLogin bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Login from basic auth with empty auth",
|
||||||
|
auth: &authn.Basic{},
|
||||||
|
expectedLogin: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login from basic auth",
|
||||||
|
auth: auth,
|
||||||
|
expectedLogin: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login with missing password",
|
||||||
|
auth: &authn.Basic{Username: "flux"},
|
||||||
|
expectedLogin: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
loginOpt, err := OIDCAdaptHelper(tt.auth)
|
||||||
|
if tt.wantErr {
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
if tt.expectedLogin {
|
||||||
|
g.Expect(loginOpt).ToNot(BeNil())
|
||||||
|
} else {
|
||||||
|
g.Expect(loginOpt).To(BeNil())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue