Merge pull request #1849 from cappyzawa/feat/helm-repository-runtime-secrets-migration

Migrate HelmRepository to runtime/secrets
This commit is contained in:
Matheus Pimenta 2025-07-18 13:40:13 +01:00 committed by GitHub
commit 173a1cccc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 392 additions and 342 deletions

4
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/elazarl/goproxy v1.7.2
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/fluxcd/pkg/apis/event v0.18.0
github.com/fluxcd/pkg/apis/meta v1.17.0
github.com/fluxcd/pkg/apis/meta v1.18.0
github.com/fluxcd/pkg/auth v0.21.0
github.com/fluxcd/pkg/cache v0.10.0
github.com/fluxcd/pkg/git v0.34.0
@ -38,7 +38,7 @@ require (
github.com/fluxcd/pkg/lockedfile v0.6.0
github.com/fluxcd/pkg/masktoken v0.7.0
github.com/fluxcd/pkg/oci v0.51.0
github.com/fluxcd/pkg/runtime v0.69.0
github.com/fluxcd/pkg/runtime v0.73.0
github.com/fluxcd/pkg/sourceignore v0.13.0
github.com/fluxcd/pkg/ssh v0.20.0
github.com/fluxcd/pkg/tar v0.13.0

8
go.sum
View File

@ -376,8 +376,8 @@ github.com/fluxcd/pkg/apis/acl v0.8.0 h1:mZNl4mOQQf5/cdMCYgKcrZTZRndCtMtkI0BDfNO
github.com/fluxcd/pkg/apis/acl v0.8.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ=
github.com/fluxcd/pkg/apis/event v0.18.0 h1:PNbWk9gvX8gMIi6VsJapnuDO+giLEeY+6olLVXvXFkk=
github.com/fluxcd/pkg/apis/event v0.18.0/go.mod h1:7S/DGboLolfbZ6stO6dcDhG1SfkPWQ9foCULvbiYpiA=
github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E=
github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs=
github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
github.com/fluxcd/pkg/auth v0.21.0 h1:ckAQqP12wuptXEkMY18SQKWEY09m9e6yI0mEMsDV15M=
github.com/fluxcd/pkg/auth v0.21.0/go.mod h1:MXmpsXT97c874HCw5hnfqFUP7TsG8/Ss1vFrk8JccfM=
github.com/fluxcd/pkg/cache v0.10.0 h1:M+OGDM4da1cnz7q+sZSBtkBJHpiJsLnKVmR9OdMWxEY=
@ -398,8 +398,8 @@ github.com/fluxcd/pkg/masktoken v0.7.0 h1:pitmyOg2pUVdW+nn2Lk/xqm2TaA08uxvOC0ns3
github.com/fluxcd/pkg/masktoken v0.7.0/go.mod h1:Lc1uoDjO1GY6+YdkK+ZqqBIBWquyV58nlSJ5S1N1IYU=
github.com/fluxcd/pkg/oci v0.51.0 h1:9oYnm+T4SCVSBif9gn80ALJkMGSERabVMDJiaMIdr7Y=
github.com/fluxcd/pkg/oci v0.51.0/go.mod h1:5J6IhHoDVYCVeBEC+4E3nPeKh7d0kjJ8IEL6NVCiTx4=
github.com/fluxcd/pkg/runtime v0.69.0 h1:5gPY95NSFI34GlQTj0+NHjOFpirSwviCUb9bM09b5nA=
github.com/fluxcd/pkg/runtime v0.69.0/go.mod h1:ug+pat+I4wfOBuCy2E/pLmBNd3kOOo4cP2jxnxefPwY=
github.com/fluxcd/pkg/runtime v0.73.0 h1:BV3qEwMT3lfHA2lterT3Es62z6EkJr2ST/jkyBmmskQ=
github.com/fluxcd/pkg/runtime v0.73.0/go.mod h1:iGhdaEq+lMJQTJNAFEPOU4gUJ7kt3yeDcJPZy7O9IUw=
github.com/fluxcd/pkg/sourceignore v0.13.0 h1:ZvkzX2WsmyZK9cjlqOFFW1onHVzhPZIqDbCh96rPqbU=
github.com/fluxcd/pkg/sourceignore v0.13.0/go.mod h1:Z9H1GoBx0ljOhptnzoV0PL6Nd/UzwKcSphP27lqb4xI=
github.com/fluxcd/pkg/ssh v0.20.0 h1:Ak0laIYIc/L8lEfqls/LDWRW8wYPESGaravQsCRGLb8=

View File

@ -1035,12 +1035,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) {
}
},
want: sreconcile.ResultEmpty,
wantErr: &serror.Generic{Err: errors.New("failed to get authentication secret '/invalid'")},
wantErr: &serror.Generic{Err: errors.New("failed to get authentication secret: secrets \"invalid\" not found")},
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
g.Expect(build.Complete()).To(BeFalse())
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret: secrets \"invalid\" not found"),
}))
},
},
@ -1304,12 +1304,12 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
}
},
want: sreconcile.ResultEmpty,
wantErr: &serror.Generic{Err: errors.New("failed to get authentication secret '/invalid'")},
wantErr: &serror.Generic{Err: errors.New("failed to get authentication secret: secrets \"invalid\" not found")},
assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) {
g.Expect(build.Complete()).To(BeFalse())
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret '/invalid'"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get authentication secret: secrets \"invalid\" not found"),
}))
},
},
@ -2515,7 +2515,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
},
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to construct Helm client's TLS config: cannot append certificate into certificate pool: invalid CA certificate"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to construct Helm client's TLS config: failed to parse CA certificate"),
},
},
{

View File

@ -18,7 +18,6 @@ package controller
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@ -48,16 +47,15 @@ import (
"github.com/fluxcd/pkg/runtime/conditions"
conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/secrets"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/source-controller/internal/cache"
intdigest "github.com/fluxcd/source-controller/internal/digest"
"github.com/fluxcd/source-controller/internal/helm/getter"
"github.com/fluxcd/source-controller/internal/helm/repository"
intpredicates "github.com/fluxcd/source-controller/internal/predicates"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
stls "github.com/fluxcd/source-controller/internal/tls"
)
func TestHelmRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
@ -420,31 +418,43 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
server options
url string
secret *corev1.Secret
beforeFunc func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest)
beforeFunc func(t *WithT, obj *sourcev1.HelmRepository)
revFunc func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest
afterFunc func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository)
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "HTTPS with certSecretRef pointing to CA cert but public repo URL succeeds",
name: "HTTPS with certSecretRef pointing to non-matching CA cert but public repo URL fails",
protocol: "http",
url: "https://stefanprodan.github.io/podinfo",
want: sreconcile.ResultSuccess,
want: sreconcile.ResultEmpty,
wantErr: true,
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
Name: "ca-file",
Namespace: "default",
},
Data: map[string][]byte{
"ca.crt": tlsCA,
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
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"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FailedReason, "tls: failed to verify certificate: x509: certificate signed by unknown authority"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
afterFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
// No repo index due to fetch fail.
t.Expect(chartRepo.Path).To(BeEmpty())
t.Expect(chartRepo.Index).To(BeNil())
t.Expect(artifact.Revision).To(BeEmpty())
},
},
{
@ -457,15 +467,37 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
Name: "ca-file",
Namespace: "default",
},
Data: map[string][]byte{
"ca.crt": tlsCA,
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
tlsConfig, err := secrets.TLSConfigFromSecret(context.TODO(), secret)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, tlsConfig, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -487,15 +519,37 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
Name: "ca-file",
Namespace: "default",
},
Data: map[string][]byte{
"caFile": tlsCA,
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
tlsConfig, err := secrets.TLSConfigFromSecret(context.TODO(), secret)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, tlsConfig, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -518,16 +572,38 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
Name: "ca-file",
Namespace: "default",
},
Data: map[string][]byte{
"caFile": tlsCA,
},
Type: corev1.SecretTypeDockerConfigJson,
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
tlsConfig, err := secrets.TLSConfigFromSecret(context.TODO(), secret)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, tlsConfig, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -542,7 +618,25 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "HTTP without secretRef makes ArtifactOutdated=True",
protocol: "http",
want: sreconcile.ResultSuccess,
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, nil, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
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"),
@ -562,16 +656,39 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth",
Name: "basic-auth",
Namespace: "default",
},
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("1234"),
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
basicAuth, err := secrets.BasicAuthFromSecret(context.TODO(), secret)
t.Expect(err).ToNot(HaveOccurred())
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
helmgetter.WithBasicAuth(basicAuth.Username, basicAuth.Password),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, nil, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -593,7 +710,8 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth",
Name: "basic-auth",
Namespace: "default",
},
Data: map[string][]byte{
"username": []byte("git"),
@ -601,9 +719,31 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
Type: corev1.SecretTypeDockerConfigJson,
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
basicAuth, err := secrets.BasicAuthFromSecret(context.TODO(), secret)
t.Expect(err).ToNot(HaveOccurred())
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
helmgetter.WithBasicAuth(basicAuth.Username, basicAuth.Password),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, nil, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
@ -625,20 +765,21 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-ca",
Name: "invalid-ca",
Namespace: "default",
},
Data: map[string][]byte{
"ca.crt": []byte("invalid"),
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid CA certificate"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to construct Helm client's TLS config: failed to parse CA certificate"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
@ -652,7 +793,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Invalid URL makes FetchFailed=True and returns stalling error",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "")
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@ -674,7 +815,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Unsupported scheme makes FetchFailed=True and returns stalling error",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "ftp://")
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@ -696,7 +837,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Missing secret returns FetchFailed=True and returns error",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
@ -719,20 +860,21 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
protocol: "http",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "malformed-basic-auth",
Name: "malformed-basic-auth",
Namespace: "default",
},
Data: map[string][]byte{
"username": []byte("git"),
},
},
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "malformed-basic-auth"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "required fields 'username' and 'password"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "secret 'default/malformed-basic-auth': malformed basic auth - has 'username' but missing 'password'"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
},
@ -746,15 +888,29 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Stored index with same revision",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
obj.Status.Artifact = &sourcev1.Artifact{
Revision: rev.String(),
}
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, "foo", "bar")
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "foo", "bar")
},
revFunc: func(t *WithT, server *helmtestserver.HelmServer, secret *corev1.Secret) digest.Digest {
serverURL := server.URL()
repoURL, err := repository.NormalizeURL(serverURL)
t.Expect(err).ToNot(HaveOccurred())
getterOpts := []helmgetter.Option{
helmgetter.WithURL(repoURL),
}
chartRepo, err := repository.NewChartRepository(repoURL, "", testGetters, nil, getterOpts...)
t.Expect(err).ToNot(HaveOccurred())
err = chartRepo.CacheIndex()
t.Expect(err).ToNot(HaveOccurred())
digest := chartRepo.Digest(intdigest.Canonical)
return digest
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
@ -770,7 +926,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Stored index with different revision",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Status.Artifact = &sourcev1.Artifact{
Revision: "80bb3dd67c63095d985850459834ea727603727a370079de90d221191d375a86",
}
@ -795,7 +951,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
{
name: "Existing artifact makes ArtifactOutdated=True",
protocol: "http",
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, rev digest.Digest) {
beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
obj.Status.Artifact = &sourcev1.Artifact{
Path: "some-path",
Revision: "some-rev",
@ -815,6 +971,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
GenerateName: "auth-strategy-",
Generation: 1,
Namespace: "default",
},
Spec: sourcev1.HelmRepositorySpec{
Interval: metav1.Duration{Duration: interval},
@ -873,48 +1030,9 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
clientBuilder.WithObjects(secret.DeepCopy())
}
// Calculate the artifact digest for valid repos configurations.
getterOpts := []helmgetter.Option{
helmgetter.WithURL(server.URL()),
}
var newChartRepo *repository.ChartRepository
var tlsConf *tls.Config
validSecret := true
if secret != nil {
// Extract the client options from secret, ignoring any invalid
// value. validSecret is used to determine if the index digest
// should be calculated below.
var gOpts []helmgetter.Option
var serr error
gOpts, serr = getter.GetterOptionsFromSecret(*secret)
if serr != nil {
validSecret = false
}
getterOpts = append(getterOpts, gOpts...)
repoURL := server.URL()
if tt.url != "" {
repoURL = tt.url
}
tlsConf, _, serr = stls.KubeTLSClientConfigFromSecret(*secret, repoURL)
if serr != nil {
validSecret = false
}
if tlsConf == nil {
tlsConf, _, serr = stls.TLSClientConfigFromSecret(*secret, repoURL)
if serr != nil {
validSecret = false
}
}
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
} else {
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
}
g.Expect(err).ToNot(HaveOccurred())
var rev digest.Digest
if validSecret {
g.Expect(newChartRepo.CacheIndex()).To(Succeed())
rev = newChartRepo.Digest(intdigest.Canonical)
if tt.revFunc != nil {
rev = tt.revFunc(g, server, secret)
}
r := &HelmRepositoryReconciler{
@ -925,7 +1043,14 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
patchOptions: getPatchOptions(helmRepositoryReadyCondition.Owned, "sc"),
}
if tt.beforeFunc != nil {
tt.beforeFunc(g, obj, rev)
tt.beforeFunc(g, obj)
}
// Special handling for tests that need to set revision after calculation
if tt.name == "Stored index with same revision" && rev != "" {
obj.Status.Artifact = &sourcev1.Artifact{
Revision: rev.String(),
}
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())

View File

@ -31,10 +31,11 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/runtime/secrets"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/source-controller/internal/helm/registry"
soci "github.com/fluxcd/source-controller/internal/oci"
stls "github.com/fluxcd/source-controller/internal/tls"
)
const (
@ -69,110 +70,151 @@ func (o ClientOpts) MustLoginToRegistry() bool {
// A temporary directory is created to store the certs files if needed and its path is returned along with the options object. It is the
// caller's responsibility to clean up the directory.
func GetClientOpts(ctx context.Context, c client.Client, obj *sourcev1.HelmRepository, url string) (*ClientOpts, string, error) {
hrOpts := &ClientOpts{
// This function configures authentication for Helm repositories based on the provided secrets:
// - CertSecretRef: TLS client certificates (always takes priority)
// - SecretRef: Can contain Basic Auth or TLS certificates (deprecated)
// For OCI repositories, additional registry-specific authentication is configured (including Docker config)
opts := &ClientOpts{
GetterOpts: []helmgetter.Option{
helmgetter.WithURL(url),
helmgetter.WithTimeout(obj.GetTimeout()),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
},
}
ociRepo := obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI
var (
certSecret *corev1.Secret
tlsBytes *stls.TLSBytes
certFile string
keyFile string
caFile string
dir string
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, tlsBytes, err = stls.KubeTLSClientConfigFromSecret(*certSecret, url)
if err != nil {
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
}
Insecure: obj.Spec.Insecure,
}
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.secretRef`.
if hrOpts.TlsConfig == nil && !ociRepo {
hrOpts.TlsConfig, tlsBytes, err = stls.LegacyTLSClientConfigFromSecret(*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 p := obj.Spec.Provider; p != "" && p != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI && ociRepo {
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil {
return nil, "", fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
}
hrOpts.Authenticator = authenticator
// Process secrets and configure authentication
deprecatedTLS, certSecret, authSecret, err := configureAuthentication(ctx, c, obj, opts, url)
if err != nil {
return nil, "", err
}
if ociRepo {
// Persist the certs files to the path if needed.
if tlsBytes != nil {
dir, err = os.MkdirTemp("", "helm-repo-oci-certs")
if err != nil {
return nil, "", fmt.Errorf("cannot create temporary directory: %w", err)
}
certFile, keyFile, caFile, err = storeTLSCertificateFiles(tlsBytes, dir)
if err != nil {
return nil, "", fmt.Errorf("cannot write certs files to path: %w", err)
}
}
loginOpt, err := registry.NewLoginOption(hrOpts.Authenticator, hrOpts.Keychain, url)
// Setup OCI registry specific configurations if needed
var tempCertDir string
if obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
tempCertDir, err = configureOCIRegistryWithSecrets(ctx, obj, opts, url, certSecret, authSecret)
if err != nil {
return nil, "", err
}
if loginOpt != nil {
hrOpts.RegLoginOpts = []helmreg.LoginOption{loginOpt, helmreg.LoginOptInsecure(obj.Spec.Insecure)}
tlsLoginOpt := registry.TLSLoginOption(certFile, keyFile, caFile)
if tlsLoginOpt != nil {
hrOpts.RegLoginOpts = append(hrOpts.RegLoginOpts, tlsLoginOpt)
}
}
var deprecatedErr error
if deprecatedTLS {
deprecatedErr = ErrDeprecatedTLSConfig
}
return opts, tempCertDir, deprecatedErr
}
// configureAuthentication processes all secret references and sets up authentication.
// Returns (deprecatedTLS, certSecret, authSecret, error) where:
// - deprecatedTLS: true if TLS config comes from SecretRef (deprecated pattern)
// - certSecret: the secret from CertSecretRef (nil if not specified)
// - authSecret: the secret from SecretRef (nil if not specified)
func configureAuthentication(ctx context.Context, c client.Client, obj *sourcev1.HelmRepository, opts *ClientOpts, url string) (bool, *corev1.Secret, *corev1.Secret, error) {
var deprecatedTLS bool
var certSecret, authSecret *corev1.Secret
if obj.Spec.CertSecretRef != nil {
secret, err := fetchSecret(ctx, c, obj.Spec.CertSecretRef.Name, obj.GetNamespace())
if err != nil {
return false, nil, nil, fmt.Errorf("failed to get TLS authentication secret: %w", err)
}
certSecret = secret
tlsConfig, err := secrets.TLSConfigFromSecret(ctx, secret)
if err != nil {
return false, nil, nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
}
opts.TlsConfig = tlsConfig
}
// Extract all authentication methods from SecretRef.
// This secret may contain multiple auth types (Basic Auth, TLS).
if obj.Spec.SecretRef != nil {
secret, err := fetchSecret(ctx, c, obj.Spec.SecretRef.Name, obj.GetNamespace())
if err != nil {
return false, nil, nil, fmt.Errorf("failed to get authentication secret: %w", err)
}
authSecret = secret
methods, err := secrets.AuthMethodsFromSecret(ctx, secret)
if err != nil {
return false, nil, nil, fmt.Errorf("failed to detect authentication methods: %w", err)
}
if methods.HasBasicAuth() {
opts.GetterOpts = append(opts.GetterOpts,
helmgetter.WithBasicAuth(methods.Basic.Username, methods.Basic.Password))
}
// Use TLS from SecretRef only if CertSecretRef is not specified (CertSecretRef takes priority)
if opts.TlsConfig == nil && methods.HasTLS() {
opts.TlsConfig = methods.TLS
deprecatedTLS = true
}
}
if deprecatedTLSConfig {
err = ErrDeprecatedTLSConfig
return deprecatedTLS, certSecret, authSecret, nil
}
// configureOCIRegistryWithSecrets sets up OCI-specific configurations using pre-fetched secrets
func configureOCIRegistryWithSecrets(ctx context.Context, obj *sourcev1.HelmRepository, opts *ClientOpts, url string, certSecret, authSecret *corev1.Secret) (string, error) {
// Configure OCI authentication from authSecret if available
if authSecret != nil {
keychain, err := registry.LoginOptionFromSecret(url, *authSecret)
if err != nil {
return "", fmt.Errorf("failed to configure login options: %w", err)
}
opts.Keychain = keychain
}
hrOpts.Insecure = obj.Spec.Insecure
// Handle OCI provider authentication if no SecretRef
if obj.Spec.SecretRef == nil && obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.GenericOCIProvider {
authenticator, err := soci.OIDCAuth(ctx, url, obj.Spec.Provider)
if err != nil {
return "", fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, err)
}
opts.Authenticator = authenticator
}
return hrOpts, dir, err
// Setup registry login options
loginOpt, err := registry.NewLoginOption(opts.Authenticator, opts.Keychain, url)
if err != nil {
return "", err
}
if loginOpt != nil {
opts.RegLoginOpts = []helmreg.LoginOption{loginOpt, helmreg.LoginOptInsecure(obj.Spec.Insecure)}
}
// Handle TLS certificate files for OCI
var tempCertDir string
if opts.TlsConfig != nil {
tempCertDir, err = os.MkdirTemp("", "helm-repo-oci-certs")
if err != nil {
return "", fmt.Errorf("cannot create temporary directory: %w", err)
}
var tlsSecret *corev1.Secret
if certSecret != nil {
tlsSecret = certSecret
} else if authSecret != nil {
tlsSecret = authSecret
}
certFile, keyFile, caFile, err := storeTLSCertificateFilesForOCI(ctx, tlsSecret, nil, tempCertDir)
if err != nil {
return "", fmt.Errorf("cannot write certs files to path: %w", err)
}
tlsLoginOpt := registry.TLSLoginOption(certFile, keyFile, caFile)
if tlsLoginOpt != nil {
opts.RegLoginOpts = append(opts.RegLoginOpts, tlsLoginOpt)
}
}
return tempCertDir, nil
}
func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
@ -187,30 +229,48 @@ func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (
return &secret, nil
}
// storeTLSCertificateFiles writes the certs files to the given path and returns the files paths.
func storeTLSCertificateFiles(tlsBytes *stls.TLSBytes, path string) (string, string, string, error) {
// storeTLSCertificateFilesForOCI writes TLS certificate data from secrets to files for OCI registry authentication.
// Helm OCI registry client requires certificate file paths rather than in-memory data,
// so we need to temporarily write the certificate data to disk.
// Returns paths to the written cert, key, and CA files (any of which may be empty if not present).
func storeTLSCertificateFilesForOCI(ctx context.Context, certSecret, authSecret *corev1.Secret, path string) (string, string, string, error) {
var (
certFile string
keyFile string
caFile string
err error
)
if len(tlsBytes.CertBytes) > 0 && len(tlsBytes.KeyBytes) > 0 {
certFile, err = writeToFile(tlsBytes.CertBytes, certFileName, path)
if err != nil {
return "", "", "", err
// Try to get TLS data from certSecret first, then authSecret
var tlsSecret *corev1.Secret
if certSecret != nil {
tlsSecret = certSecret
} else if authSecret != nil {
tlsSecret = authSecret
}
if tlsSecret != nil {
if certData, exists := tlsSecret.Data[secrets.KeyTLSCert]; exists {
if keyData, keyExists := tlsSecret.Data[secrets.KeyTLSPrivateKey]; keyExists {
certFile, err = writeToFile(certData, certFileName, path)
if err != nil {
return "", "", "", err
}
keyFile, err = writeToFile(keyData, keyFileName, path)
if err != nil {
return "", "", "", err
}
}
}
keyFile, err = writeToFile(tlsBytes.KeyBytes, keyFileName, path)
if err != nil {
return "", "", "", err
}
}
if len(tlsBytes.CABytes) > 0 {
caFile, err = writeToFile(tlsBytes.CABytes, caFileName, path)
if err != nil {
return "", "", "", err
if caData, exists := tlsSecret.Data[secrets.KeyCACert]; exists {
caFile, err = writeToFile(caData, caFileName, path)
if err != nil {
return "", "", "", err
}
}
}
return certFile, keyFile, caFile, nil
}

View File

@ -19,6 +19,7 @@ package getter
import (
"context"
"os"
"strings"
"testing"
"time"
@ -64,7 +65,6 @@ func TestGetClientOpts(t *testing.T) {
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("pass"),
"caFile": []byte("invalid"),
},
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
@ -186,6 +186,7 @@ func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
certSecret *corev1.Secret
authSecret *corev1.Secret
loginOptsN int
wantErrMsg string
}{
{
name: "with valid caFile",
@ -225,7 +226,7 @@ func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
"password": []byte("pass"),
},
},
loginOptsN: 2,
wantErrMsg: "must contain either 'ca.crt' or both 'tls.crt' and 'tls.key'",
},
{
name: "without cert secret",
@ -271,6 +272,17 @@ func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
c := clientBuilder.Build()
clientOpts, tmpDir, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
if tt.wantErrMsg != "" {
if err == nil {
t.Errorf("GetClientOpts() expected error but got none")
return
}
if !strings.Contains(err.Error(), tt.wantErrMsg) {
t.Errorf("GetClientOpts() expected error containing %q but got %v", tt.wantErrMsg, err)
return
}
return
}
if err != nil {
t.Errorf("GetClientOpts() error = %v", err)
return

View File

@ -1,54 +0,0 @@
/*
Copyright 2020 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 (
"fmt"
"helm.sh/helm/v3/pkg/getter"
corev1 "k8s.io/api/core/v1"
)
// GetterOptionsFromSecret constructs a getter.Option slice for the given secret.
// It returns the slice, or an error.
func GetterOptionsFromSecret(secret corev1.Secret) ([]getter.Option, error) {
var opts []getter.Option
basicAuth, err := basicAuthFromSecret(secret)
if err != nil {
return opts, err
}
if basicAuth != nil {
opts = append(opts, basicAuth)
}
return opts, nil
}
// basicAuthFromSecret attempts to construct a basic auth getter.Option for the
// given v1.Secret and returns the result.
//
// Secrets with no username AND password are ignored, if only one is defined it
// returns an error.
func basicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
username, password := string(secret.Data["username"]), string(secret.Data["password"])
switch {
case username == "" && password == "":
return nil, nil
case username == "" || password == "":
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
return getter.WithBasicAuth(username, password), nil
}

View File

@ -1,93 +0,0 @@
/*
Copyright 2020 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 (
"testing"
corev1 "k8s.io/api/core/v1"
)
var (
basicAuthSecretFixture = corev1.Secret{
Data: map[string][]byte{
"username": []byte("user"),
"password": []byte("password"),
},
}
)
func TestGetterOptionsFromSecret(t *testing.T) {
tests := []struct {
name string
secrets []corev1.Secret
}{
{"basic auth", []corev1.Secret{basicAuthSecretFixture}},
{"empty", []corev1.Secret{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := corev1.Secret{Data: map[string][]byte{}}
for _, s := range tt.secrets {
for k, v := range s.Data {
secret.Data[k] = v
}
}
got, err := GetterOptionsFromSecret(secret)
if err != nil {
t.Errorf("ClientOptionsFromSecret() error = %v", err)
return
}
if len(got) != len(tt.secrets) {
t.Errorf("ClientOptionsFromSecret() options = %v, expected = %v", got, len(tt.secrets))
}
})
}
}
func Test_basicAuthFromSecret(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
wantNil bool
}{
{"username and password", basicAuthSecretFixture, nil, false, false},
{"without username", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "username") }, true, true},
{"without password", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, true, true},
{"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 := basicAuthFromSecret(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("BasicAuthFromSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantNil && got != nil {
t.Error("BasicAuthFromSecret() != nil")
return
}
})
}
}