Merge pull request #1849 from cappyzawa/feat/helm-repository-runtime-secrets-migration
Migrate HelmRepository to runtime/secrets
This commit is contained in:
commit
173a1cccc5
4
go.mod
4
go.mod
|
|
@ -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
8
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue