Merge pull request #1160 from fluxcd/helm-cert-secret
helmrepo: add `.spec.certSecretRef` for specifying TLS auth data
This commit is contained in:
commit
3840940354
|
|
@ -51,11 +51,18 @@ type HelmRepositorySpec struct {
|
||||||
// for the HelmRepository.
|
// for the HelmRepository.
|
||||||
// For HTTP/S basic auth the secret must contain 'username' and 'password'
|
// For HTTP/S basic auth the secret must contain 'username' and 'password'
|
||||||
// fields.
|
// fields.
|
||||||
// For TLS the secret must contain a 'certFile' and 'keyFile', and/or
|
// Support for TLS auth using the 'certFile' and 'keyFile', and/or 'caFile'
|
||||||
// 'caFile' fields.
|
// keys is deprecated. Please use `.spec.certSecretRef` instead.
|
||||||
// +optional
|
// +optional
|
||||||
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||||
|
|
||||||
|
// CertSecretRef specifies the Secret containing the TLS authentication
|
||||||
|
// data. The secret must contain a 'certFile' and 'keyFile', and/or 'caFile'
|
||||||
|
// fields. It takes precedence over the values specified in the Secret
|
||||||
|
// referred to by `.spec.secretRef`.
|
||||||
|
// +optional
|
||||||
|
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
|
||||||
|
|
||||||
// PassCredentials allows the credentials from the SecretRef to be passed
|
// PassCredentials allows the credentials from the SecretRef to be passed
|
||||||
// on to a host that does not match the host as defined in URL.
|
// on to a host that does not match the host as defined in URL.
|
||||||
// This may be required if the host of the advertised chart URLs in the
|
// This may be required if the host of the advertised chart URLs in the
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,11 @@ func (in *HelmRepositorySpec) DeepCopyInto(out *HelmRepositorySpec) {
|
||||||
*out = new(meta.LocalObjectReference)
|
*out = new(meta.LocalObjectReference)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.CertSecretRef != nil {
|
||||||
|
in, out := &in.CertSecretRef, &out.CertSecretRef
|
||||||
|
*out = new(meta.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
out.Interval = in.Interval
|
out.Interval = in.Interval
|
||||||
if in.Timeout != nil {
|
if in.Timeout != nil {
|
||||||
in, out := &in.Timeout, &out.Timeout
|
in, out := &in.Timeout, &out.Timeout
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,18 @@ spec:
|
||||||
required:
|
required:
|
||||||
- namespaceSelectors
|
- namespaceSelectors
|
||||||
type: object
|
type: object
|
||||||
|
certSecretRef:
|
||||||
|
description: CertSecretRef specifies the Secret containing the TLS
|
||||||
|
authentication data. The secret must contain a 'certFile' and 'keyFile',
|
||||||
|
and/or 'caFile' fields. It takes precedence over the values specified
|
||||||
|
in the Secret referred to by `.spec.secretRef`.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
interval:
|
interval:
|
||||||
description: Interval at which to check the URL for updates.
|
description: Interval at which to check the URL for updates.
|
||||||
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
|
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
|
||||||
|
|
@ -323,8 +335,9 @@ spec:
|
||||||
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
|
||||||
must contain 'username' and 'password' fields. For TLS the secret
|
must contain 'username' and 'password' fields. Support for TLS auth
|
||||||
must contain a 'certFile' and 'keyFile', and/or 'caFile' fields.
|
using the 'certFile' and 'keyFile', and/or 'caFile' keys is deprecated.
|
||||||
|
Please use `.spec.certSecretRef` instead.
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
description: Name of the referent.
|
description: Name of the referent.
|
||||||
|
|
|
||||||
|
|
@ -792,8 +792,25 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
for the HelmRepository.
|
for the HelmRepository.
|
||||||
For HTTP/S basic auth the secret must contain ‘username’ and ‘password’
|
For HTTP/S basic auth the secret must contain ‘username’ and ‘password’
|
||||||
fields.
|
fields.
|
||||||
For TLS the secret must contain a ‘certFile’ and ‘keyFile’, and/or
|
Support for TLS auth using the ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
||||||
‘caFile’ fields.</p>
|
keys is deprecated. Please use <code>.spec.certSecretRef</code> instead.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>certSecretRef</code><br>
|
||||||
|
<em>
|
||||||
|
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||||
|
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>CertSecretRef specifies the Secret containing the TLS authentication
|
||||||
|
data. The secret must contain a ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
||||||
|
fields. It takes precedence over the values specified in the Secret
|
||||||
|
referred to by <code>.spec.secretRef</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -2459,8 +2476,25 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
for the HelmRepository.
|
for the HelmRepository.
|
||||||
For HTTP/S basic auth the secret must contain ‘username’ and ‘password’
|
For HTTP/S basic auth the secret must contain ‘username’ and ‘password’
|
||||||
fields.
|
fields.
|
||||||
For TLS the secret must contain a ‘certFile’ and ‘keyFile’, and/or
|
Support for TLS auth using the ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
||||||
‘caFile’ fields.</p>
|
keys is deprecated. Please use <code>.spec.certSecretRef</code> instead.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>certSecretRef</code><br>
|
||||||
|
<em>
|
||||||
|
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
|
||||||
|
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>CertSecretRef specifies the Secret containing the TLS authentication
|
||||||
|
data. The secret must contain a ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
||||||
|
fields. It takes precedence over the values specified in the Secret
|
||||||
|
referred to by <code>.spec.secretRef</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -452,15 +452,37 @@ flux create secret oci ghcr-auth \
|
||||||
--password=${GITHUB_PAT}
|
--password=${GITHUB_PAT}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### TLS authentication
|
**Note:** Support for specifying TLS authentication data using this API has been
|
||||||
|
deprecated. Please use [`.spec.certSecretRef`](#cert-secret-reference) instead.
|
||||||
|
If the controller uses the secret specfied by this field to configure TLS, then
|
||||||
|
a deprecation warning will be logged.
|
||||||
|
|
||||||
|
### Cert secret reference
|
||||||
|
|
||||||
**Note:** TLS authentication is not yet supported by OCI Helm repositories.
|
**Note:** TLS authentication is not yet supported by OCI Helm repositories.
|
||||||
|
|
||||||
To provide TLS credentials to use while connecting with the Helm repository,
|
`.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
|
||||||
the referenced Secret is expected to contain `.data.certFile` and
|
certificate data. The secret can contain the following keys:
|
||||||
`.data.keyFile`, and/or `.data.caFile` values.
|
|
||||||
|
|
||||||
For example:
|
* `certFile` and `keyFile`, to specify the client certificate and private key used for
|
||||||
|
TLS client authentication. These must be used in conjunction, i.e. specifying one without
|
||||||
|
the other will lead to an error.
|
||||||
|
* `caFile`, to specify the CA certificate used to verify the server, which is required
|
||||||
|
if the server is using a self-signed certificate.
|
||||||
|
|
||||||
|
If the server is using a self-signed certificate and has TLS client authentication enabled,
|
||||||
|
all three values are required.
|
||||||
|
|
||||||
|
All the files in the secret are expected to be [PEM-encoded][pem-encoding]. Assuming you have
|
||||||
|
three files; `client.key`, `client.crt` and `ca.crt` for the client private key, client
|
||||||
|
certificate and the CA certificate respectively, you can generate the required secret using
|
||||||
|
the `flux creat secret helm` command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
flux create secret helm tls --key-file=client.key --cert-file=client.crt --ca-file=ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
|
|
@ -472,7 +494,7 @@ metadata:
|
||||||
spec:
|
spec:
|
||||||
interval: 5m0s
|
interval: 5m0s
|
||||||
url: https://example.com
|
url: https://example.com
|
||||||
secretRef:
|
certSecretRef:
|
||||||
name: example-tls
|
name: example-tls
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -28,7 +27,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
|
|
@ -54,7 +52,6 @@ import (
|
||||||
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
|
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"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/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
|
|
@ -68,7 +65,6 @@ import (
|
||||||
serror "github.com/fluxcd/source-controller/internal/error"
|
serror "github.com/fluxcd/source-controller/internal/error"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/chart"
|
"github.com/fluxcd/source-controller/internal/helm/chart"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/getter"
|
"github.com/fluxcd/source-controller/internal/helm/getter"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
|
||||||
"github.com/fluxcd/source-controller/internal/helm/repository"
|
"github.com/fluxcd/source-controller/internal/helm/repository"
|
||||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||||
|
|
@ -506,11 +502,6 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, sp *patch.Ser
|
||||||
// object, and returns early.
|
// object, and returns early.
|
||||||
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *helmv1.HelmChart,
|
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *helmv1.HelmChart,
|
||||||
repo *helmv1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
|
repo *helmv1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
|
||||||
var (
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
authenticator authn.Authenticator
|
|
||||||
keychain authn.Keychain
|
|
||||||
)
|
|
||||||
// Used to login with the repository declared provider
|
// Used to login with the repository declared provider
|
||||||
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -519,65 +510,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return chartRepoConfigErrorReturn(err, obj)
|
return chartRepoConfigErrorReturn(err, obj)
|
||||||
}
|
}
|
||||||
// Construct the Getter options from the HelmRepository data
|
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
|
||||||
clientOpts := []helmgetter.Option{
|
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
helmgetter.WithURL(normalizedURL),
|
|
||||||
helmgetter.WithTimeout(repo.Spec.Timeout.Duration),
|
|
||||||
helmgetter.WithPassCredentialsAll(repo.Spec.PassCredentials),
|
|
||||||
}
|
|
||||||
if secret, err := r.getHelmRepositorySecret(ctx, repo); secret != nil || err != nil {
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: fmt.Errorf("failed to get secret '%s': %w", repo.Spec.SecretRef.Name, err),
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Return error as the world as observed may change
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build client options from secret
|
|
||||||
opts, tlsCfg, err := r.clientOptionsFromSecret(secret, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: err,
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Requeue as content of secret might change
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
clientOpts = append(clientOpts, opts...)
|
|
||||||
tlsConfig = tlsCfg
|
|
||||||
|
|
||||||
// Build registryClient options from secret
|
|
||||||
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Requeue as content of secret might change
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
} else if repo.Spec.Provider != helmv1.GenericOCIProvider && repo.Spec.Type == helmv1.HelmRepositoryTypeOCI {
|
|
||||||
auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
|
||||||
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 {
|
|
||||||
authenticator = auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: err,
|
Err: err,
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
Reason: sourcev1.AuthenticationFailedReason,
|
||||||
|
|
@ -585,6 +519,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
getterOpts := clientOpts.GetterOpts
|
||||||
|
|
||||||
// Initialize the chart repository
|
// Initialize the chart repository
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
|
|
@ -599,7 +534,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
|
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
|
||||||
// TODO@souleb: remove this once the registry move to Oras v2
|
// TODO@souleb: remove this once the registry move to Oras v2
|
||||||
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
|
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to construct Helm client: %w", err),
|
Err: fmt.Errorf("failed to construct Helm client: %w", err),
|
||||||
|
|
@ -621,7 +556,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
var verifiers []soci.Verifier
|
var verifiers []soci.Verifier
|
||||||
if obj.Spec.Verify != nil {
|
if obj.Spec.Verify != nil {
|
||||||
provider := obj.Spec.Verify.Provider
|
provider := obj.Spec.Verify.Provider
|
||||||
verifiers, err = r.makeVerifiers(ctx, obj, authenticator, keychain)
|
verifiers, err = r.makeVerifiers(ctx, obj, *clientOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if obj.Spec.Verify.SecretRef == nil {
|
if obj.Spec.Verify.SecretRef == nil {
|
||||||
provider = fmt.Sprintf("%s keyless", provider)
|
provider = fmt.Sprintf("%s keyless", provider)
|
||||||
|
|
@ -636,21 +571,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the chart repository to use the OCI client with the configured getter
|
// Tell the chart repository to use the OCI client with the configured getter
|
||||||
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
|
getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
|
||||||
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
|
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
|
||||||
repository.WithOCIGetter(r.Getters),
|
repository.WithOCIGetter(r.Getters),
|
||||||
repository.WithOCIGetterOptions(clientOpts),
|
repository.WithOCIGetterOptions(getterOpts),
|
||||||
repository.WithOCIRegistryClient(registryClient),
|
repository.WithOCIRegistryClient(registryClient),
|
||||||
repository.WithVerifiers(verifiers))
|
repository.WithVerifiers(verifiers))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return chartRepoConfigErrorReturn(err, obj)
|
return chartRepoConfigErrorReturn(err, obj)
|
||||||
}
|
}
|
||||||
chartRepo = ociChartRepo
|
|
||||||
|
|
||||||
// If login options are configured, use them to login to the registry
|
// If login options are configured, use them to login to the registry
|
||||||
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
||||||
if loginOpt != nil {
|
if clientOpts.RegLoginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpt)
|
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to login to OCI registry: %w", err),
|
Err: fmt.Errorf("failed to login to OCI registry: %w", err),
|
||||||
|
|
@ -660,8 +594,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chartRepo = ociChartRepo
|
||||||
default:
|
default:
|
||||||
httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, tlsConfig, clientOpts...)
|
httpChartRepo, err := repository.NewChartRepository(normalizedURL, r.Storage.LocalPath(*repo.GetArtifact()), r.Getters, clientOpts.TlsConfig, getterOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return chartRepoConfigErrorReturn(err, obj)
|
return chartRepoConfigErrorReturn(err, obj)
|
||||||
}
|
}
|
||||||
|
|
@ -1024,12 +959,6 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *helmv1.He
|
||||||
// The callback returns an object with a state, so the caller has to do the necessary cleanup.
|
// The callback returns an object with a state, so the caller has to do the necessary cleanup.
|
||||||
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
|
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
|
||||||
return func(url string) (repository.Downloader, error) {
|
return func(url string) (repository.Downloader, error) {
|
||||||
var (
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
authenticator authn.Authenticator
|
|
||||||
keychain authn.Keychain
|
|
||||||
)
|
|
||||||
|
|
||||||
normalizedURL, err := repository.NormalizeURL(url)
|
normalizedURL, err := repository.NormalizeURL(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1052,61 +981,28 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
clientOpts := []helmgetter.Option{
|
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
|
||||||
helmgetter.WithURL(normalizedURL),
|
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
|
|
||||||
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
|
|
||||||
}
|
|
||||||
if secret, err := r.getHelmRepositorySecret(ctx, obj); secret != nil || err != nil {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build client options from secret
|
|
||||||
opts, tlsCfg, err := r.clientOptionsFromSecret(secret, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientOpts = append(clientOpts, opts...)
|
|
||||||
tlsConfig = tlsCfg
|
|
||||||
|
|
||||||
// Build registryClient options from secret
|
|
||||||
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", obj.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
|
|
||||||
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
|
||||||
return nil, fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
|
||||||
}
|
|
||||||
if auth != nil {
|
|
||||||
authenticator = auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
getterOpts := clientOpts.GetterOpts
|
||||||
|
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
if helmreg.IsOCI(normalizedURL) {
|
if helmreg.IsOCI(normalizedURL) {
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", obj.Name, err)
|
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
// Tell the chart repository to use the OCI client with the configured getter
|
// Tell the chart repository to use the OCI client with the configured getter
|
||||||
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
|
getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
|
||||||
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
|
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
|
||||||
repository.WithOCIGetterOptions(clientOpts),
|
repository.WithOCIGetterOptions(getterOpts),
|
||||||
repository.WithOCIRegistryClient(registryClient),
|
repository.WithOCIRegistryClient(registryClient),
|
||||||
repository.WithCredentialsFile(credentialsFile))
|
repository.WithCredentialsFile(credentialsFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to create OCI chart repository for HelmRepository '%s': %w", obj.Name, err))
|
errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
|
||||||
// clean up the credentialsFile
|
// clean up the credentialsFile
|
||||||
if credentialsFile != "" {
|
if credentialsFile != "" {
|
||||||
if err := os.Remove(credentialsFile); err != nil {
|
if err := os.Remove(credentialsFile); err != nil {
|
||||||
|
|
@ -1118,10 +1014,10 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
|
|
||||||
// If login options are configured, use them to login to the registry
|
// If login options are configured, use them to login to the registry
|
||||||
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
||||||
if loginOpt != nil {
|
if clientOpts.RegLoginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpt)
|
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", obj.Name, err))
|
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
|
||||||
// clean up the credentialsFile
|
// clean up the credentialsFile
|
||||||
errs = append(errs, ociChartRepo.Clear())
|
errs = append(errs, ociChartRepo.Clear())
|
||||||
return nil, kerrors.NewAggregate(errs)
|
return nil, kerrors.NewAggregate(errs)
|
||||||
|
|
@ -1130,7 +1026,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
|
|
||||||
chartRepo = ociChartRepo
|
chartRepo = ociChartRepo
|
||||||
} else {
|
} else {
|
||||||
httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, tlsConfig, clientOpts...)
|
httpChartRepo, err := repository.NewChartRepository(normalizedURL, "", r.Getters, clientOpts.TlsConfig, getterOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1178,36 +1074,6 @@ func (r *HelmChartReconciler) resolveDependencyRepository(ctx context.Context, u
|
||||||
return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace)
|
return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HelmChartReconciler) clientOptionsFromSecret(secret *corev1.Secret, normalizedURL string) ([]helmgetter.Option, *tls.Config, error) {
|
|
||||||
opts, err := getter.ClientOptionsFromSecret(*secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig, err := getter.TLSClientConfigFromSecret(*secret, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to create TLS client config with secret data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts, tlsConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *helmv1.HelmRepository) (*corev1.Secret, error) {
|
|
||||||
if repository.Spec.SecretRef == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
name := types.NamespacedName{
|
|
||||||
Namespace: repository.GetNamespace(),
|
|
||||||
Name: repository.Spec.SecretRef.Name,
|
|
||||||
}
|
|
||||||
var secret corev1.Secret
|
|
||||||
err := r.Client.Get(ctx, name, &secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &secret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string {
|
func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string {
|
||||||
repo, ok := o.(*helmv1.HelmRepository)
|
repo, ok := o.(*helmv1.HelmRepository)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -1412,13 +1278,14 @@ func chartRepoConfigErrorReturn(err error, obj *helmv1.HelmChart) (sreconcile.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeVerifiers returns a list of verifiers for the given chart.
|
// makeVerifiers returns a list of verifiers for the given chart.
|
||||||
func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) ([]soci.Verifier, error) {
|
func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *helmv1.HelmChart, clientOpts getter.ClientOpts) ([]soci.Verifier, error) {
|
||||||
var verifiers []soci.Verifier
|
var verifiers []soci.Verifier
|
||||||
verifyOpts := []remote.Option{}
|
verifyOpts := []remote.Option{}
|
||||||
if auth != nil {
|
|
||||||
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
|
if clientOpts.Authenticator != nil {
|
||||||
|
verifyOpts = append(verifyOpts, remote.WithAuth(clientOpts.Authenticator))
|
||||||
} else {
|
} else {
|
||||||
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
|
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(clientOpts.Keychain))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch obj.Spec.Verify.Provider {
|
switch obj.Spec.Verify.Provider {
|
||||||
|
|
|
||||||
|
|
@ -922,12 +922,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
want: sreconcile.ResultEmpty,
|
want: sreconcile.ResultEmpty,
|
||||||
wantErr: &serror.Event{Err: errors.New("failed to get secret 'invalid'")},
|
wantErr: &serror.Event{Err: errors.New("failed to get authentication secret '/invalid'")},
|
||||||
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
|
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
|
||||||
g.Expect(build.Complete()).To(BeFalse())
|
g.Expect(build.Complete()).To(BeFalse())
|
||||||
|
|
||||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret 'invalid'"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1190,12 +1190,12 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
want: sreconcile.ResultEmpty,
|
want: sreconcile.ResultEmpty,
|
||||||
wantErr: &serror.Event{Err: errors.New("failed to get secret 'invalid'")},
|
wantErr: &serror.Event{Err: errors.New("failed to get authentication secret '/invalid'")},
|
||||||
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
|
assertFunc: func(g *WithT, obj *helmv1.HelmChart, build chart.Build) {
|
||||||
g.Expect(build.Complete()).To(BeFalse())
|
g.Expect(build.Complete()).To(BeFalse())
|
||||||
|
|
||||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret 'invalid'"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1649,83 +1649,6 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHelmChartReconciler_getHelmRepositorySecret(t *testing.T) {
|
|
||||||
mock := &corev1.Secret{
|
|
||||||
TypeMeta: metav1.TypeMeta{
|
|
||||||
Kind: "Secret",
|
|
||||||
APIVersion: "v1",
|
|
||||||
},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "secret",
|
|
||||||
Namespace: "foo",
|
|
||||||
},
|
|
||||||
Data: map[string][]byte{
|
|
||||||
"key": []byte("bar"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &HelmChartReconciler{
|
|
||||||
Client: fakeclient.NewClientBuilder().
|
|
||||||
WithObjects(mock).
|
|
||||||
Build(),
|
|
||||||
patchOptions: getPatchOptions(helmChartReadyCondition.Owned, "sc"),
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
repository *helmv1.HelmRepository
|
|
||||||
want *corev1.Secret
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Existing secret reference",
|
|
||||||
repository: &helmv1.HelmRepository{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: mock.Namespace,
|
|
||||||
},
|
|
||||||
Spec: helmv1.HelmRepositorySpec{
|
|
||||||
SecretRef: &meta.LocalObjectReference{
|
|
||||||
Name: mock.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: mock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty secret reference",
|
|
||||||
repository: &helmv1.HelmRepository{
|
|
||||||
Spec: helmv1.HelmRepositorySpec{
|
|
||||||
SecretRef: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error on client error",
|
|
||||||
repository: &helmv1.HelmRepository{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: "different",
|
|
||||||
},
|
|
||||||
Spec: helmv1.HelmRepositorySpec{
|
|
||||||
SecretRef: &meta.LocalObjectReference{
|
|
||||||
Name: mock.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
got, err := r.getHelmRepositorySecret(context.TODO(), tt.repository)
|
|
||||||
g.Expect(err != nil).To(Equal(tt.wantErr))
|
|
||||||
g.Expect(got).To(Equal(tt.want))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelmChartReconciler_getSource(t *testing.T) {
|
func TestHelmChartReconciler_getSource(t *testing.T) {
|
||||||
mocks := []client.Object{
|
mocks := []client.Object{
|
||||||
&helmv1.HelmRepository{
|
&helmv1.HelmRepository{
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -29,7 +28,6 @@ import (
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
kuberecorder "k8s.io/client-go/tools/record"
|
kuberecorder "k8s.io/client-go/tools/record"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
@ -390,59 +388,33 @@ func (r *HelmRepositoryReconciler) reconcileStorage(ctx context.Context, sp *pat
|
||||||
// pointer is set to the newly fetched index.
|
// pointer is set to the newly fetched index.
|
||||||
func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher,
|
func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher,
|
||||||
obj *helmv1.HelmRepository, artifact *sourcev1.Artifact, chartRepo *repository.ChartRepository) (sreconcile.Result, error) {
|
obj *helmv1.HelmRepository, artifact *sourcev1.Artifact, chartRepo *repository.ChartRepository) (sreconcile.Result, error) {
|
||||||
var tlsConfig *tls.Config
|
normalizedURL, err := repository.NormalizeURL(obj.Spec.URL)
|
||||||
|
if err != nil {
|
||||||
// Configure Helm client to access repository
|
e := &serror.Stalling{
|
||||||
clientOpts := []helmgetter.Option{
|
Err: fmt.Errorf("invalid Helm repository URL: %w", err),
|
||||||
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
|
Reason: sourcev1.URLInvalidReason,
|
||||||
helmgetter.WithURL(obj.Spec.URL),
|
}
|
||||||
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure any authentication related options
|
clientOpts, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
|
||||||
if obj.Spec.SecretRef != nil {
|
if err != nil {
|
||||||
// Attempt to retrieve secret
|
if errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
name := types.NamespacedName{
|
ctrl.LoggerFrom(ctx).
|
||||||
Namespace: obj.GetNamespace(),
|
Info("warning: specifying TLS authentication data via `.spec.secretRef` is deprecated, please use `.spec.certSecretRef` instead")
|
||||||
Name: obj.Spec.SecretRef.Name,
|
} else {
|
||||||
}
|
|
||||||
var secret corev1.Secret
|
|
||||||
if err := r.Client.Get(ctx, name, &secret); err != nil {
|
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to get secret '%s': %w", name.String(), err),
|
Err: err,
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
Reason: sourcev1.AuthenticationFailedReason,
|
||||||
}
|
}
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct actual options
|
|
||||||
opts, err := getter.ClientOptionsFromSecret(secret)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Return err as the content of the secret may change.
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
clientOpts = append(clientOpts, opts...)
|
|
||||||
|
|
||||||
tlsConfig, err = getter.TLSClientConfigFromSecret(secret, obj.Spec.URL)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: fmt.Errorf("failed to create TLS client config with secret data: %w", err),
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Requeue as content of secret might change
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct Helm chart repository with options and download index
|
// Construct Helm chart repository with options and download index
|
||||||
newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, tlsConfig, clientOpts...)
|
newChartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", r.Getters, clientOpts.TlsConfig, clientOpts.GetterOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *url.Error:
|
case *url.Error:
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import (
|
||||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/repository"
|
"github.com/fluxcd/source-controller/internal/helm/repository"
|
||||||
"github.com/fluxcd/source-controller/internal/object"
|
"github.com/fluxcd/source-controller/internal/object"
|
||||||
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
intpredicates "github.com/fluxcd/source-controller/internal/predicates"
|
intpredicates "github.com/fluxcd/source-controller/internal/predicates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -318,7 +319,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
|
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
|
||||||
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
auth, authErr := soci.OIDCAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
assertConditions []metav1.Condition
|
assertConditions []metav1.Condition
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "HTTPS with secretRef pointing to CA cert but public repo URL succeeds",
|
name: "HTTPS with certSecretRef pointing to CA cert but public repo URL succeeds",
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
url: "https://stefanprodan.github.io/podinfo",
|
url: "https://stefanprodan.github.io/podinfo",
|
||||||
want: sreconcile.ResultSuccess,
|
want: sreconcile.ResultSuccess,
|
||||||
|
|
@ -400,6 +400,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
"caFile": tlsCA,
|
"caFile": tlsCA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
|
||||||
|
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
|
||||||
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
|
|
@ -450,37 +453,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HTTPS with CAFile secret makes ArtifactOutdated=True",
|
name: "HTTPS with invalid CAFile in certSecretRef makes FetchFailed=True and returns error",
|
||||||
protocol: "https",
|
|
||||||
server: options{
|
|
||||||
publicKey: tlsPublicKey,
|
|
||||||
privateKey: tlsPrivateKey,
|
|
||||||
ca: tlsCA,
|
|
||||||
},
|
|
||||||
secret: &corev1.Secret{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "ca-file",
|
|
||||||
},
|
|
||||||
Data: map[string][]byte{
|
|
||||||
"caFile": tlsCA,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
|
|
||||||
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
|
|
||||||
},
|
|
||||||
want: sreconcile.ResultSuccess,
|
|
||||||
assertConditions: []metav1.Condition{
|
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
|
||||||
},
|
|
||||||
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
|
|
||||||
t.Expect(chartRepo.Path).ToNot(BeEmpty())
|
|
||||||
t.Expect(chartRepo.Index).ToNot(BeNil())
|
|
||||||
t.Expect(artifact.Revision).ToNot(BeEmpty())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "HTTPS with invalid CAFile secret makes FetchFailed=True and returns error",
|
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
server: options{
|
server: options{
|
||||||
publicKey: tlsPublicKey,
|
publicKey: tlsPublicKey,
|
||||||
|
|
@ -496,13 +469,13 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev, dig digest.Digest) {
|
||||||
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
|
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
|
||||||
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
|
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
|
||||||
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
|
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to create TLS client config with secret data: cannot append certificate into certificate pool: invalid caFile"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid caFile"),
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
||||||
},
|
},
|
||||||
|
|
@ -766,32 +739,32 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the artifact digest for valid repos configurations.
|
// Calculate the artifact digest for valid repos configurations.
|
||||||
clientOpts := []helmgetter.Option{
|
getterOpts := []helmgetter.Option{
|
||||||
helmgetter.WithURL(server.URL()),
|
helmgetter.WithURL(server.URL()),
|
||||||
}
|
}
|
||||||
var newChartRepo *repository.ChartRepository
|
var newChartRepo *repository.ChartRepository
|
||||||
var tOpts *tls.Config
|
var tlsConf *tls.Config
|
||||||
validSecret := true
|
validSecret := true
|
||||||
if secret != nil {
|
if secret != nil {
|
||||||
// Extract the client options from secret, ignoring any invalid
|
// Extract the client options from secret, ignoring any invalid
|
||||||
// value. validSecret is used to determine if the index digest
|
// value. validSecret is used to determine if the index digest
|
||||||
// should be calculated below.
|
// should be calculated below.
|
||||||
var cOpts []helmgetter.Option
|
var gOpts []helmgetter.Option
|
||||||
var serr error
|
var serr error
|
||||||
cOpts, serr = getter.ClientOptionsFromSecret(*secret)
|
gOpts, serr = getter.GetterOptionsFromSecret(*secret)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
validSecret = false
|
validSecret = false
|
||||||
}
|
}
|
||||||
clientOpts = append(clientOpts, cOpts...)
|
getterOpts = append(getterOpts, gOpts...)
|
||||||
repoURL := server.URL()
|
repoURL := server.URL()
|
||||||
if tt.url != "" {
|
if tt.url != "" {
|
||||||
repoURL = tt.url
|
repoURL = tt.url
|
||||||
}
|
}
|
||||||
tOpts, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
|
tlsConf, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
validSecret = false
|
validSecret = false
|
||||||
}
|
}
|
||||||
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tOpts, clientOpts...)
|
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
|
||||||
} else {
|
} else {
|
||||||
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
|
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
|
||||||
}
|
}
|
||||||
|
|
@ -807,9 +780,6 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
g.Expect(newChartRepo.LoadFromPath()).To(Succeed())
|
g.Expect(newChartRepo.LoadFromPath()).To(Succeed())
|
||||||
rev = newChartRepo.Digest(intdigest.Canonical)
|
rev = newChartRepo.Digest(intdigest.Canonical)
|
||||||
}
|
}
|
||||||
if tt.beforeFunc != nil {
|
|
||||||
tt.beforeFunc(g, obj, rev, dig)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &HelmRepositoryReconciler{
|
r := &HelmRepositoryReconciler{
|
||||||
EventRecorder: record.NewFakeRecorder(32),
|
EventRecorder: record.NewFakeRecorder(32),
|
||||||
|
|
@ -818,6 +788,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
Getters: testGetters,
|
Getters: testGetters,
|
||||||
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
|
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
|
||||||
}
|
}
|
||||||
|
if tt.beforeFunc != nil {
|
||||||
|
tt.beforeFunc(g, obj, rev, dig)
|
||||||
|
}
|
||||||
|
|
||||||
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
|
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ import (
|
||||||
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
|
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/oci"
|
"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"
|
||||||
|
|
@ -345,7 +344,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
|
||||||
|
|
||||||
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != ociv1.GenericOCIProvider && ok {
|
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != ociv1.GenericOCIProvider && ok {
|
||||||
var authErr error
|
var authErr error
|
||||||
auth, authErr = oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
auth, authErr = soci.OIDCAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := serror.NewGeneric(
|
e := serror.NewGeneric(
|
||||||
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
|
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
|
||||||
|
|
@ -870,27 +869,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *ociv1.OCIR
|
||||||
return transport, nil
|
return transport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
|
||||||
func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
|
|
||||||
u := strings.TrimPrefix(url, ociv1.OCIRepositoryPrefix)
|
|
||||||
ref, err := name.ParseReference(u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := login.ProviderOptions{}
|
|
||||||
switch provider {
|
|
||||||
case ociv1.AmazonOCIProvider:
|
|
||||||
opts.AwsAutoLogin = true
|
|
||||||
case ociv1.AzureOCIProvider:
|
|
||||||
opts.AzureAutoLogin = true
|
|
||||||
case ociv1.GoogleOCIProvider:
|
|
||||||
opts.GcpAutoLogin = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return login.NewManager().Login(ctx, u, ref, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconcileStorage ensures the current state of the storage matches the
|
// reconcileStorage ensures the current state of the storage matches the
|
||||||
// desired and previously observed state.
|
// desired and previously observed state.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package getter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/oci"
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
|
helmreg "helm.sh/helm/v3/pkg/registry"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||||
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
|
||||||
|
|
||||||
|
// ClientOpts contains the various options to use while constructing
|
||||||
|
// a Helm repository client.
|
||||||
|
type ClientOpts struct {
|
||||||
|
Authenticator authn.Authenticator
|
||||||
|
Keychain authn.Keychain
|
||||||
|
RegLoginOpt helmreg.LoginOption
|
||||||
|
TlsConfig *tls.Config
|
||||||
|
GetterOpts []helmgetter.Option
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientOpts uses the provided HelmRepository object and a normalized
|
||||||
|
// URL to construct a HelmClientOpts object. If obj is an OCI HelmRepository,
|
||||||
|
// then the returned options object will also contain the required registry
|
||||||
|
// auth mechanisms.
|
||||||
|
func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmRepository, url string) (*ClientOpts, error) {
|
||||||
|
hrOpts := &ClientOpts{
|
||||||
|
GetterOpts: []helmgetter.Option{
|
||||||
|
helmgetter.WithURL(url),
|
||||||
|
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
|
||||||
|
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ociRepo := obj.Spec.Type == helmv1.HelmRepositoryTypeOCI
|
||||||
|
|
||||||
|
var certSecret *corev1.Secret
|
||||||
|
var err error
|
||||||
|
// Check `.spec.certSecretRef` first for any TLS auth data.
|
||||||
|
if obj.Spec.CertSecretRef != nil {
|
||||||
|
certSecret, err = fetchSecret(ctx, c, obj.Spec.CertSecretRef.Name, obj.GetNamespace())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get TLS authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.CertSecretRef.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hrOpts.TlsConfig, err = TLSClientConfigFromSecret(*certSecret, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var authSecret *corev1.Secret
|
||||||
|
var deprecatedTLSConfig bool
|
||||||
|
if obj.Spec.SecretRef != nil {
|
||||||
|
authSecret, err = fetchSecret(ctx, c, obj.Spec.SecretRef.Name, obj.GetNamespace())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.SecretRef.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct actual Helm client options.
|
||||||
|
opts, err := GetterOptionsFromSecret(*authSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to configure Helm client: %w", err)
|
||||||
|
}
|
||||||
|
hrOpts.GetterOpts = append(hrOpts.GetterOpts, opts...)
|
||||||
|
|
||||||
|
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
|
||||||
|
// then try to use `.spec.certSecretRef`.
|
||||||
|
if hrOpts.TlsConfig == nil {
|
||||||
|
hrOpts.TlsConfig, err = TLSClientConfigFromSecret(*authSecret, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
||||||
|
}
|
||||||
|
// Constructing a TLS config using the auth secret is deprecated behavior.
|
||||||
|
if hrOpts.TlsConfig != nil {
|
||||||
|
deprecatedTLSConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ociRepo {
|
||||||
|
hrOpts.Keychain, err = registry.LoginOptionFromSecret(url, *authSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to configure login options: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI && ociRepo {
|
||||||
|
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
|
||||||
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
|
return nil, fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
|
||||||
|
}
|
||||||
|
if authenticator != nil {
|
||||||
|
hrOpts.Authenticator = authenticator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ociRepo {
|
||||||
|
hrOpts.RegLoginOpt, err = registry.NewLoginOption(hrOpts.Authenticator, hrOpts.Keychain, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deprecatedTLSConfig {
|
||||||
|
err = ErrDeprecatedTLSConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return hrOpts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
|
||||||
|
key := types.NamespacedName{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
var secret corev1.Secret
|
||||||
|
if err := c.Get(ctx, key, &secret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSClientConfigFromSecret attempts to construct a TLS client config
|
||||||
|
// for the given v1.Secret. It returns the TLS client config or an error.
|
||||||
|
//
|
||||||
|
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
||||||
|
// certBytes OR keyBytes is defined it returns an error.
|
||||||
|
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, error) {
|
||||||
|
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
||||||
|
switch {
|
||||||
|
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
||||||
|
return nil, nil
|
||||||
|
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
||||||
|
return nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
||||||
|
secret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf := &tls.Config{}
|
||||||
|
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
||||||
|
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(caBytes) > 0 {
|
||||||
|
cp, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
||||||
|
}
|
||||||
|
if !cp.AppendCertsFromPEM(caBytes) {
|
||||||
|
return nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.RootCAs = cp
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.BuildNameToCertificate()
|
||||||
|
|
||||||
|
u, err := url.Parse(repositoryUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.ServerName = u.Hostname()
|
||||||
|
|
||||||
|
return tlsConf, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package getter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
|
||||||
|
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetClientOpts(t *testing.T) {
|
||||||
|
tlsCA, err := os.ReadFile("../../controller/testdata/certs/ca.pem")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not read CA file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
certSecret *corev1.Secret
|
||||||
|
authSecret *corev1.Secret
|
||||||
|
afterFunc func(t *WithT, hcOpts *ClientOpts)
|
||||||
|
oci bool
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HelmRepository with certSecretRef discards TLS config in secretRef",
|
||||||
|
certSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ca-file",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": tlsCA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
"caFile": []byte("invalid"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
|
||||||
|
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
|
||||||
|
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HelmRepository with TLS config only in secretRef is marked as deprecated",
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-tls",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
"caFile": tlsCA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
|
||||||
|
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
|
||||||
|
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
|
||||||
|
},
|
||||||
|
err: ErrDeprecatedTLSConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OCI HelmRepository with secretRef has auth configured",
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-oci",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
|
||||||
|
repo, err := name.NewRepository("ghcr.io/dummy")
|
||||||
|
t.Expect(err).ToNot(HaveOccurred())
|
||||||
|
authenticator, err := hcOpts.Keychain.Resolve(repo)
|
||||||
|
t.Expect(err).ToNot(HaveOccurred())
|
||||||
|
config, err := authenticator.Authorization()
|
||||||
|
t.Expect(err).ToNot(HaveOccurred())
|
||||||
|
t.Expect(config.Username).To(Equal("user"))
|
||||||
|
t.Expect(config.Password).To(Equal("pass"))
|
||||||
|
},
|
||||||
|
oci: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
helmRepo := &helmv1.HelmRepository{
|
||||||
|
Spec: helmv1.HelmRepositorySpec{
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if tt.oci {
|
||||||
|
helmRepo.Spec.Type = helmv1.HelmRepositoryTypeOCI
|
||||||
|
}
|
||||||
|
|
||||||
|
clientBuilder := fakeclient.NewClientBuilder()
|
||||||
|
if tt.authSecret != nil {
|
||||||
|
clientBuilder.WithObjects(tt.authSecret.DeepCopy())
|
||||||
|
helmRepo.Spec.SecretRef = &meta.LocalObjectReference{
|
||||||
|
Name: tt.authSecret.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tt.certSecret != nil {
|
||||||
|
clientBuilder.WithObjects(tt.certSecret.DeepCopy())
|
||||||
|
helmRepo.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||||
|
Name: tt.certSecret.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := clientBuilder.Build()
|
||||||
|
|
||||||
|
clientOpts, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
|
||||||
|
if tt.err != nil {
|
||||||
|
g.Expect(err).To(Equal(tt.err))
|
||||||
|
} else {
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
tt.afterFunc(g, clientOpts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_tlsClientConfigFromSecret(t *testing.T) {
|
||||||
|
tlsSecretFixture := validTlsSecret(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
secret corev1.Secret
|
||||||
|
modify func(secret *corev1.Secret)
|
||||||
|
wantErr bool
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{"certFile, keyFile and caFile", tlsSecretFixture, nil, false, false},
|
||||||
|
{"without certFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "certFile") }, true, true},
|
||||||
|
{"without keyFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "keyFile") }, true, true},
|
||||||
|
{"without caFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "caFile") }, false, false},
|
||||||
|
{"empty", corev1.Secret{}, nil, false, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
secret := tt.secret.DeepCopy()
|
||||||
|
if tt.modify != nil {
|
||||||
|
tt.modify(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := TLSClientConfigFromSecret(*secret, "")
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.wantNil && got != nil {
|
||||||
|
t.Error("TLSClientConfigFromSecret() != nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
||||||
|
// valid from a syntax (minimum requirements) perspective.
|
||||||
|
func validTlsSecret(t *testing.T) corev1.Secret {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Private key cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
certTemplate := x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1337),
|
||||||
|
}
|
||||||
|
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Certificate cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ca := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(7331),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("CA private key cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("CA certificate cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
})
|
||||||
|
|
||||||
|
certPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert,
|
||||||
|
})
|
||||||
|
|
||||||
|
caPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: caBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
return corev1.Secret{
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"certFile": []byte(certPem),
|
||||||
|
"keyFile": []byte(keyPem),
|
||||||
|
"caFile": []byte(caPem),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,20 +17,17 @@ limitations under the License.
|
||||||
package getter
|
package getter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/getter"
|
"helm.sh/helm/v3/pkg/getter"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientOptionsFromSecret constructs a getter.Option slice for the given secret.
|
// GetterOptionsFromSecret constructs a getter.Option slice for the given secret.
|
||||||
// It returns the slice, or an error.
|
// It returns the slice, or an error.
|
||||||
func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
|
func GetterOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
|
||||||
var opts []getter.Option
|
var opts []getter.Option
|
||||||
basicAuth, err := BasicAuthFromSecret(secret)
|
basicAuth, err := basicAuthFromSecret(secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return opts, err
|
return opts, err
|
||||||
}
|
}
|
||||||
|
|
@ -40,12 +37,12 @@ func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
|
||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicAuthFromSecret attempts to construct a basic auth getter.Option for the
|
// basicAuthFromSecret attempts to construct a basic auth getter.Option for the
|
||||||
// given v1.Secret and returns the result.
|
// given v1.Secret and returns the result.
|
||||||
//
|
//
|
||||||
// Secrets with no username AND password are ignored, if only one is defined it
|
// Secrets with no username AND password are ignored, if only one is defined it
|
||||||
// returns an error.
|
// returns an error.
|
||||||
func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
|
func basicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
|
||||||
username, password := string(secret.Data["username"]), string(secret.Data["password"])
|
username, password := string(secret.Data["username"]), string(secret.Data["password"])
|
||||||
switch {
|
switch {
|
||||||
case username == "" && password == "":
|
case username == "" && password == "":
|
||||||
|
|
@ -55,51 +52,3 @@ func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
|
||||||
}
|
}
|
||||||
return getter.WithBasicAuth(username, password), nil
|
return getter.WithBasicAuth(username, password), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSClientConfigFromSecret attempts to construct a TLS client config
|
|
||||||
// for the given v1.Secret. It returns the TLS client config or an error.
|
|
||||||
//
|
|
||||||
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
|
||||||
// certBytes OR keyBytes is defined it returns an error.
|
|
||||||
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, error) {
|
|
||||||
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
|
||||||
switch {
|
|
||||||
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
|
||||||
return nil, nil
|
|
||||||
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
|
||||||
return nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
|
||||||
secret.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := &tls.Config{}
|
|
||||||
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
|
||||||
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(caBytes) > 0 {
|
|
||||||
cp, err := x509.SystemCertPool()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
|
||||||
}
|
|
||||||
if !cp.AppendCertsFromPEM(caBytes) {
|
|
||||||
return nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.RootCAs = cp
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.BuildNameToCertificate()
|
|
||||||
|
|
||||||
u, err := url.Parse(repositoryUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.ServerName = u.Hostname()
|
|
||||||
|
|
||||||
return tlsConf, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,6 @@ limitations under the License.
|
||||||
package getter
|
package getter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -36,7 +31,7 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClientOptionsFromSecret(t *testing.T) {
|
func TestGetterOptionsFromSecret(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
secrets []corev1.Secret
|
secrets []corev1.Secret
|
||||||
|
|
@ -53,7 +48,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := ClientOptionsFromSecret(secret)
|
got, err := GetterOptionsFromSecret(secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("ClientOptionsFromSecret() error = %v", err)
|
t.Errorf("ClientOptionsFromSecret() error = %v", err)
|
||||||
return
|
return
|
||||||
|
|
@ -65,7 +60,7 @@ func TestClientOptionsFromSecret(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuthFromSecret(t *testing.T) {
|
func Test_basicAuthFromSecret(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
secret corev1.Secret
|
secret corev1.Secret
|
||||||
|
|
@ -84,7 +79,7 @@ func TestBasicAuthFromSecret(t *testing.T) {
|
||||||
if tt.modify != nil {
|
if tt.modify != nil {
|
||||||
tt.modify(secret)
|
tt.modify(secret)
|
||||||
}
|
}
|
||||||
got, err := BasicAuthFromSecret(*secret)
|
got, err := basicAuthFromSecret(*secret)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|
@ -96,96 +91,3 @@ func TestBasicAuthFromSecret(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSClientConfigFromSecret(t *testing.T) {
|
|
||||||
tlsSecretFixture := validTlsSecret(t)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
secret corev1.Secret
|
|
||||||
modify func(secret *corev1.Secret)
|
|
||||||
wantErr bool
|
|
||||||
wantNil bool
|
|
||||||
}{
|
|
||||||
{"certFile, keyFile and caFile", tlsSecretFixture, nil, false, false},
|
|
||||||
{"without certFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "certFile") }, true, true},
|
|
||||||
{"without keyFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "keyFile") }, true, true},
|
|
||||||
{"without caFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "caFile") }, false, false},
|
|
||||||
{"empty", corev1.Secret{}, nil, false, true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
secret := tt.secret.DeepCopy()
|
|
||||||
if tt.modify != nil {
|
|
||||||
tt.modify(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := TLSClientConfigFromSecret(*secret, "")
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.wantNil && got != nil {
|
|
||||||
t.Error("TLSClientConfigFromSecret() != nil")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
|
||||||
// valid from a syntax (minimum requirements) perspective.
|
|
||||||
func validTlsSecret(t *testing.T) corev1.Secret {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Private key cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
certTemplate := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1337),
|
|
||||||
}
|
|
||||||
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Certificate cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
ca := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(7331),
|
|
||||||
IsCA: true,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
||||||
}
|
|
||||||
|
|
||||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("CA private key cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("CA certificate cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
||||||
})
|
|
||||||
|
|
||||||
certPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: cert,
|
|
||||||
})
|
|
||||||
|
|
||||||
caPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: caBytes,
|
|
||||||
})
|
|
||||||
|
|
||||||
return corev1.Secret{
|
|
||||||
Data: map[string][]byte{
|
|
||||||
"certFile": []byte(certPem),
|
|
||||||
"keyFile": []byte(keyPem),
|
|
||||||
"caFile": []byte(caPem),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/fluxcd/source-controller/internal/oci"
|
"github.com/fluxcd/source-controller/internal/oci"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
|
helmreg "helm.sh/helm/v3/pkg/registry"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -139,3 +140,17 @@ func (r stringResource) String() string {
|
||||||
func (r stringResource) RegistryStr() string {
|
func (r stringResource) RegistryStr() string {
|
||||||
return r.registry
|
return r.registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewLoginOption returns a registry login option for the given HelmRepository.
|
||||||
|
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
|
||||||
|
func NewLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (helmreg.LoginOption, error) {
|
||||||
|
if auth != nil {
|
||||||
|
return AuthAdaptHelper(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keychain != nil {
|
||||||
|
return KeychainAdaptHelper(keychain)(registryURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,17 @@ limitations under the License.
|
||||||
|
|
||||||
package oci
|
package oci
|
||||||
|
|
||||||
import "github.com/google/go-containerregistry/pkg/authn"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/oci/auth/login"
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
)
|
||||||
|
|
||||||
// Anonymous is an authn.AuthConfig that always returns an anonymous
|
// Anonymous is an authn.AuthConfig that always returns an anonymous
|
||||||
// authenticator. It is useful for registries that do not require authentication
|
// authenticator. It is useful for registries that do not require authentication
|
||||||
|
|
@ -28,3 +38,24 @@ type Anonymous authn.AuthConfig
|
||||||
func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) {
|
func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) {
|
||||||
return authn.Anonymous, nil
|
return authn.Anonymous, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OIDCAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
||||||
|
func OIDCAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
|
||||||
|
u := strings.TrimPrefix(url, sourcev1.OCIRepositoryPrefix)
|
||||||
|
ref, err := name.ParseReference(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := login.ProviderOptions{}
|
||||||
|
switch provider {
|
||||||
|
case sourcev1.AmazonOCIProvider:
|
||||||
|
opts.AwsAutoLogin = true
|
||||||
|
case sourcev1.AzureOCIProvider:
|
||||||
|
opts.AzureAutoLogin = true
|
||||||
|
case sourcev1.GoogleOCIProvider:
|
||||||
|
opts.GcpAutoLogin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return login.NewManager().Login(ctx, u, ref, opts)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue