Add support for custom certificate and skip-tls-verify in helm OCI
If implemented user will be able to provide their own custom start and bypass tls verification when interacting with OCI registries over https to pull helmCharts. Signed-off-by: Soule BA <soule@weave.works>
This commit is contained in:
parent
6377c6fa4a
commit
d45c08cba6
|
@ -459,8 +459,6 @@ a deprecation warning will be logged.
|
||||||
|
|
||||||
### Cert secret reference
|
### Cert secret reference
|
||||||
|
|
||||||
**Note:** TLS authentication is not yet supported by OCI Helm repositories.
|
|
||||||
|
|
||||||
`.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
|
`.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
|
||||||
certificate data. The secret can contain the following keys:
|
certificate data. The secret can contain the following keys:
|
||||||
|
|
||||||
|
|
|
@ -512,7 +512,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return chartRepoConfigErrorReturn(err, obj)
|
return chartRepoConfigErrorReturn(err, obj)
|
||||||
}
|
}
|
||||||
clientOpts, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
|
|
||||||
|
clientOpts, certsTmpDir, err := getter.GetClientOpts(ctxTimeout, r.Client, repo, normalizedURL)
|
||||||
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: err,
|
Err: err,
|
||||||
|
@ -521,6 +522,15 @@ 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
|
||||||
}
|
}
|
||||||
|
if certsTmpDir != "" {
|
||||||
|
defer func() {
|
||||||
|
if err := os.RemoveAll(certsTmpDir); err != nil {
|
||||||
|
r.eventLogf(ctx, obj, corev1.EventTypeWarning, meta.FailedReason,
|
||||||
|
"failed to delete temporary certificates directory: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
getterOpts := clientOpts.GetterOpts
|
getterOpts := clientOpts.GetterOpts
|
||||||
|
|
||||||
// Initialize the chart repository
|
// Initialize the chart repository
|
||||||
|
@ -536,7 +546,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(clientOpts.RegLoginOpt != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
|
||||||
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),
|
||||||
|
@ -585,8 +595,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
|
|
||||||
// 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 clientOpts.RegLoginOpt != nil {
|
if clientOpts.MustLoginToRegistry() {
|
||||||
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
|
err = ociChartRepo.Login(clientOpts.RegLoginOpts...)
|
||||||
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),
|
||||||
|
@ -983,7 +993,7 @@ 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, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
|
clientOpts, certsTmpDir, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
|
||||||
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
if err != nil && !errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -991,7 +1001,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
|
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
if helmreg.IsOCI(normalizedURL) {
|
if helmreg.IsOCI(normalizedURL) {
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.RegLoginOpt != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -1002,6 +1012,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
|
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters),
|
||||||
repository.WithOCIGetterOptions(getterOpts),
|
repository.WithOCIGetterOptions(getterOpts),
|
||||||
repository.WithOCIRegistryClient(registryClient),
|
repository.WithOCIRegistryClient(registryClient),
|
||||||
|
repository.WithCertificatesStore(certsTmpDir),
|
||||||
repository.WithCredentialsFile(credentialsFile))
|
repository.WithCredentialsFile(credentialsFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
|
errs = append(errs, fmt.Errorf("failed to create OCI chart repository: %w", err))
|
||||||
|
@ -1016,8 +1027,8 @@ 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 clientOpts.RegLoginOpt != nil {
|
if clientOpts.MustLoginToRegistry() {
|
||||||
err = ociChartRepo.Login(clientOpts.RegLoginOpt)
|
err = ociChartRepo.Login(clientOpts.RegLoginOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
|
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository: %w", err))
|
||||||
// clean up the credentialsFile
|
// clean up the credentialsFile
|
||||||
|
|
|
@ -1109,7 +1109,7 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
// Upload the test chart
|
// Upload the test chart
|
||||||
metadata, err := loadTestChartToOCI(chartData, chartPath, testRegistryServer)
|
metadata, err := loadTestChartToOCI(chartData, testRegistryServer, "", "", "")
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
||||||
|
@ -2244,6 +2244,9 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
url string
|
url string
|
||||||
registryOpts registryOptions
|
registryOpts registryOptions
|
||||||
secretOpts secretOptions
|
secretOpts secretOptions
|
||||||
|
secret *corev1.Secret
|
||||||
|
certsecret *corev1.Secret
|
||||||
|
insecure bool
|
||||||
provider string
|
provider string
|
||||||
providerImg string
|
providerImg string
|
||||||
want sreconcile.Result
|
want sreconcile.Result
|
||||||
|
@ -2253,6 +2256,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "HTTP without basic auth",
|
name: "HTTP without basic auth",
|
||||||
want: sreconcile.ResultSuccess,
|
want: sreconcile.ResultSuccess,
|
||||||
|
insecure: true,
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
|
@ -2261,6 +2265,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "HTTP with basic auth secret",
|
name: "HTTP with basic auth secret",
|
||||||
want: sreconcile.ResultSuccess,
|
want: sreconcile.ResultSuccess,
|
||||||
|
insecure: true,
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
@ -2268,6 +2273,13 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
username: testRegistryUsername,
|
username: testRegistryUsername,
|
||||||
password: testRegistryPassword,
|
password: testRegistryPassword,
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
|
@ -2277,6 +2289,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
name: "HTTP registry - basic auth with invalid secret",
|
name: "HTTP registry - basic auth with invalid secret",
|
||||||
want: sreconcile.ResultEmpty,
|
want: sreconcile.ResultEmpty,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
insecure: true,
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
@ -2284,6 +2297,13 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
username: "wrong-pass",
|
username: "wrong-pass",
|
||||||
password: "wrong-pass",
|
password: "wrong-pass",
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to login to OCI registry"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to login to OCI registry"),
|
||||||
},
|
},
|
||||||
|
@ -2291,6 +2311,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "with contextual login provider",
|
name: "with contextual login provider",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
insecure: true,
|
||||||
provider: "aws",
|
provider: "aws",
|
||||||
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
|
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
|
@ -2303,16 +2324,87 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
insecure: true,
|
||||||
secretOpts: secretOptions{
|
secretOpts: secretOptions{
|
||||||
username: testRegistryUsername,
|
username: testRegistryUsername,
|
||||||
password: testRegistryPassword,
|
password: testRegistryPassword,
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
provider: "azure",
|
provider: "azure",
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS With invalid CA cert",
|
||||||
|
wantErr: true,
|
||||||
|
registryOpts: registryOptions{
|
||||||
|
withTLS: true,
|
||||||
|
withClientCertAuth: true,
|
||||||
|
},
|
||||||
|
secretOpts: secretOptions{
|
||||||
|
username: testRegistryUsername,
|
||||||
|
password: testRegistryPassword,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
|
certsecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "certs-secretref",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": []byte("invalid caFile"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 caFile"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS With CA cert",
|
||||||
|
want: sreconcile.ResultSuccess,
|
||||||
|
registryOpts: registryOptions{
|
||||||
|
withTLS: true,
|
||||||
|
withClientCertAuth: true,
|
||||||
|
},
|
||||||
|
secretOpts: secretOptions{
|
||||||
|
username: testRegistryUsername,
|
||||||
|
password: testRegistryPassword,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
|
certsecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "certs-secretref",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": tlsCA,
|
||||||
|
"certFile": clientPublicKey,
|
||||||
|
"keyFile": clientPrivateKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: pulled 'helmchart' chart with version '0.1.0'"),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -2325,7 +2417,9 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
|
|
||||||
workspaceDir := t.TempDir()
|
workspaceDir := t.TempDir()
|
||||||
|
|
||||||
|
if tt.insecure {
|
||||||
tt.registryOpts.disableDNSMocking = true
|
tt.registryOpts.disableDNSMocking = true
|
||||||
|
}
|
||||||
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
|
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
@ -2337,7 +2431,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
// Upload the test chart
|
// Upload the test chart
|
||||||
metadata, err := loadTestChartToOCI(chartData, chartPath, server)
|
metadata, err := loadTestChartToOCI(chartData, server, "testdata/certs/client.pem", "testdata/certs/client-key.pem", "testdata/certs/ca.pem")
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
repo := &helmv1.HelmRepository{
|
repo := &helmv1.HelmRepository{
|
||||||
|
@ -2364,24 +2458,25 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
|
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
|
||||||
secret := &corev1.Secret{
|
tt.secret.Data[".dockerconfigjson"] = []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
server.registryHost, tt.secretOpts.username, tt.secretOpts.password))
|
||||||
Name: "auth-secretref",
|
|
||||||
},
|
|
||||||
Type: corev1.SecretTypeDockerConfigJson,
|
|
||||||
Data: map[string][]byte{
|
|
||||||
".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
|
|
||||||
server.registryHost, tt.secretOpts.username, tt.secretOpts.password)),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tt.secret != nil {
|
||||||
repo.Spec.SecretRef = &meta.LocalObjectReference{
|
repo.Spec.SecretRef = &meta.LocalObjectReference{
|
||||||
Name: secret.Name,
|
Name: tt.secret.Name,
|
||||||
}
|
}
|
||||||
clientBuilder.WithObjects(secret, repo)
|
clientBuilder.WithObjects(tt.secret)
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if tt.certsecret != nil {
|
||||||
|
repo.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||||
|
Name: tt.certsecret.Name,
|
||||||
|
}
|
||||||
|
clientBuilder.WithObjects(tt.certsecret)
|
||||||
|
}
|
||||||
|
|
||||||
clientBuilder.WithObjects(repo)
|
clientBuilder.WithObjects(repo)
|
||||||
}
|
|
||||||
|
|
||||||
obj := &helmv1.HelmChart{
|
obj := &helmv1.HelmChart{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
@ -2456,7 +2551,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
// Upload the test chart
|
// Upload the test chart
|
||||||
metadata, err := loadTestChartToOCI(chartData, chartPath, server)
|
metadata, err := loadTestChartToOCI(chartData, server, "", "", "")
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
||||||
|
@ -2687,30 +2782,24 @@ func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
||||||
return ch.Metadata, nil
|
return ch.Metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTestChartToOCI(chartData []byte, chartPath string, server *registryClientTestServer) (*hchart.Metadata, error) {
|
func loadTestChartToOCI(chartData []byte, server *registryClientTestServer, certFile, keyFile, cafile string) (*hchart.Metadata, error) {
|
||||||
// Login to the registry
|
// Login to the registry
|
||||||
err := server.registryClient.Login(server.registryHost,
|
err := server.registryClient.Login(server.registryHost,
|
||||||
helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword),
|
helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword),
|
||||||
helmreg.LoginOptInsecure(true))
|
helmreg.LoginOptTLSClientConfig(certFile, keyFile, cafile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to login to OCI registry: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
// Load a test chart
|
|
||||||
chartData, err = os.ReadFile(chartPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
metadata, err := extractChartMeta(chartData)
|
metadata, err := extractChartMeta(chartData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to extract chart metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload the test chart
|
// Upload the test chart
|
||||||
ref := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
|
ref := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
|
||||||
_, err = server.registryClient.Push(chartData, ref)
|
_, err = server.registryClient.Push(chartData, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to push chart: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
|
|
|
@ -399,7 +399,7 @@ func (r *HelmRepositoryReconciler) reconcileSource(ctx context.Context, sp *patc
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
clientOpts, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
|
clientOpts, _, err := getter.GetClientOpts(ctx, r.Client, obj, normalizedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
if errors.Is(err, getter.ErrDeprecatedTLSConfig) {
|
||||||
ctrl.LoggerFrom(ctx).
|
ctrl.LoggerFrom(ctx).
|
||||||
|
|
|
@ -18,20 +18,18 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
|
||||||
helmreg "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"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
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"
|
||||||
|
@ -42,7 +40,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/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"
|
||||||
|
@ -51,10 +48,9 @@ import (
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
"github.com/fluxcd/source-controller/internal/helm/getter"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +75,7 @@ type HelmRepositoryOCIReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
kuberecorder.EventRecorder
|
kuberecorder.EventRecorder
|
||||||
helper.Metrics
|
helper.Metrics
|
||||||
Getters helmgetter.Providers
|
|
||||||
ControllerName string
|
ControllerName string
|
||||||
RegistryClientGenerator RegistryClientGeneratorFunc
|
RegistryClientGenerator RegistryClientGeneratorFunc
|
||||||
|
|
||||||
|
@ -95,7 +91,7 @@ type HelmRepositoryOCIReconciler struct {
|
||||||
// and an optional file name.
|
// and an optional file name.
|
||||||
// The file is used to store the registry client credentials.
|
// The file is used to store the registry client credentials.
|
||||||
// The caller is responsible for deleting the file.
|
// The caller is responsible for deleting the file.
|
||||||
type RegistryClientGeneratorFunc func(isLogin bool) (*helmreg.Client, string, error)
|
type RegistryClientGeneratorFunc func(tlsConfig *tls.Config, isLogin bool) (*helmreg.Client, string, error)
|
||||||
|
|
||||||
func (r *HelmRepositoryOCIReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *HelmRepositoryOCIReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
return r.SetupWithManagerAndOptions(mgr, HelmRepositoryReconcilerOptions{})
|
return r.SetupWithManagerAndOptions(mgr, HelmRepositoryReconcilerOptions{})
|
||||||
|
@ -226,7 +222,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a successful reconciliation.
|
// Check if it's a successful reconciliation.
|
||||||
if result.RequeueAfter == obj.GetRequeueAfter() && result.Requeue == false &&
|
if result.RequeueAfter == obj.GetRequeueAfter() && !result.Requeue &&
|
||||||
retErr == nil {
|
retErr == nil {
|
||||||
// Remove reconciling condition if the reconciliation was successful.
|
// Remove reconciling condition if the reconciliation was successful.
|
||||||
conditions.Delete(obj, meta.ReconcilingCondition)
|
conditions.Delete(obj, meta.ReconcilingCondition)
|
||||||
|
@ -305,43 +301,34 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
|
||||||
result, retErr = ctrl.Result{}, nil
|
result, retErr = ctrl.Result{}, nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizedURL, err := repository.NormalizeURL(obj.Spec.URL)
|
||||||
|
if err != nil {
|
||||||
|
conditions.MarkStalled(obj, sourcev1.URLInvalidReason, err.Error())
|
||||||
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.URLInvalidReason, err.Error())
|
||||||
|
result, retErr = ctrl.Result{}, nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
conditions.Delete(obj, meta.StalledCondition)
|
conditions.Delete(obj, meta.StalledCondition)
|
||||||
|
|
||||||
var (
|
clientOpts, certsTmpDir, err := getter.GetClientOpts(ctxTimeout, r.Client, obj, normalizedURL)
|
||||||
authenticator authn.Authenticator
|
|
||||||
keychain authn.Keychain
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
// Configure any authentication related options.
|
|
||||||
if obj.Spec.SecretRef != nil {
|
|
||||||
keychain, err = authFromSecret(ctx, r.Client, obj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
|
||||||
result, retErr = ctrl.Result{}, err
|
result, retErr = ctrl.Result{}, err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI {
|
if certsTmpDir != "" {
|
||||||
auth, authErr := soci.OIDCAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
defer func() {
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if err := os.RemoveAll(certsTmpDir); err != nil {
|
||||||
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
r.eventLogf(ctx, obj, corev1.EventTypeWarning, meta.FailedReason,
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
"failed to delete temporary certs directory: %s", err)
|
||||||
result, retErr = ctrl.Result{}, e
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if auth != nil {
|
}()
|
||||||
authenticator = auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loginOpt, err := makeLoginOption(authenticator, keychain, obj.Spec.URL)
|
|
||||||
if err != nil {
|
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
|
|
||||||
result, retErr = ctrl.Result{}, err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create registry client and login if needed.
|
// Create registry client and login if needed.
|
||||||
registryClient, file, err := r.RegistryClientGenerator(loginOpt != nil)
|
registryClient, file, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to create registry client: %w", err)
|
e := fmt.Errorf("failed to create registry client: %w", err)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
|
||||||
|
@ -368,8 +355,8 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
|
||||||
conditions.Delete(obj, meta.StalledCondition)
|
conditions.Delete(obj, meta.StalledCondition)
|
||||||
|
|
||||||
// Attempt to login to the registry if credentials are provided.
|
// Attempt to login to the registry if credentials are provided.
|
||||||
if loginOpt != nil {
|
if clientOpts.MustLoginToRegistry() {
|
||||||
err = chartRepo.Login(loginOpt)
|
err = chartRepo.Login(clientOpts.RegLoginOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
|
e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
@ -411,41 +398,6 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
|
||||||
r.Eventf(obj, eventType, reason, msg)
|
r.Eventf(obj, eventType, reason, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// authFromSecret returns an authn.Keychain for the given HelmRepository.
|
|
||||||
// If the HelmRepository does not specify a secretRef, an anonymous keychain is returned.
|
|
||||||
func authFromSecret(ctx context.Context, client client.Client, obj *helmv1.HelmRepository) (authn.Keychain, error) {
|
|
||||||
// Attempt to retrieve secret.
|
|
||||||
name := types.NamespacedName{
|
|
||||||
Namespace: obj.GetNamespace(),
|
|
||||||
Name: obj.Spec.SecretRef.Name,
|
|
||||||
}
|
|
||||||
var secret corev1.Secret
|
|
||||||
if err := client.Get(ctx, name, &secret); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get secret '%s': %w", name.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct login options.
|
|
||||||
keychain, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
|
|
||||||
}
|
|
||||||
return keychain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeLoginOption returns a registry login option for the given HelmRepository.
|
|
||||||
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
|
|
||||||
func makeLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (helmreg.LoginOption, error) {
|
|
||||||
if auth != nil {
|
|
||||||
return registry.AuthAdaptHelper(auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if keychain != nil {
|
|
||||||
return registry.KeychainAdaptHelper(keychain)(registryURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func conditionsDiff(a, b []string) []string {
|
func conditionsDiff(a, b []string) []string {
|
||||||
bMap := make(map[string]struct{}, len(b))
|
bMap := make(map[string]struct{}, len(b))
|
||||||
for _, j := range b {
|
for _, j := range b {
|
||||||
|
|
|
@ -205,7 +205,10 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
registryOpts registryOptions
|
registryOpts registryOptions
|
||||||
|
insecure bool
|
||||||
secretOpts secretOptions
|
secretOpts secretOptions
|
||||||
|
secret *corev1.Secret
|
||||||
|
certsSecret *corev1.Secret
|
||||||
provider string
|
provider string
|
||||||
providerImg string
|
providerImg string
|
||||||
want ctrl.Result
|
want ctrl.Result
|
||||||
|
@ -222,6 +225,7 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "HTTP with basic auth secret",
|
name: "HTTP with basic auth secret",
|
||||||
want: ctrl.Result{RequeueAfter: interval},
|
want: ctrl.Result{RequeueAfter: interval},
|
||||||
|
insecure: true,
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
@ -229,6 +233,13 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
username: testRegistryUsername,
|
username: testRegistryUsername,
|
||||||
password: testRegistryPassword,
|
password: testRegistryPassword,
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
|
||||||
},
|
},
|
||||||
|
@ -237,6 +248,7 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
name: "HTTP registry - basic auth with invalid secret",
|
name: "HTTP registry - basic auth with invalid secret",
|
||||||
want: ctrl.Result{},
|
want: ctrl.Result{},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
insecure: true,
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
@ -244,6 +256,13 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
username: "wrong-pass",
|
username: "wrong-pass",
|
||||||
password: "wrong-pass",
|
password: "wrong-pass",
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing object: new generation"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing object: new generation"),
|
||||||
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to login to registry"),
|
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to login to registry"),
|
||||||
|
@ -252,6 +271,7 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "with contextual login provider",
|
name: "with contextual login provider",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
insecure: true,
|
||||||
provider: "aws",
|
provider: "aws",
|
||||||
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
|
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
|
@ -265,15 +285,86 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
registryOpts: registryOptions{
|
registryOpts: registryOptions{
|
||||||
withBasicAuth: true,
|
withBasicAuth: true,
|
||||||
},
|
},
|
||||||
|
insecure: true,
|
||||||
secretOpts: secretOptions{
|
secretOpts: secretOptions{
|
||||||
username: testRegistryUsername,
|
username: testRegistryUsername,
|
||||||
password: testRegistryPassword,
|
password: testRegistryPassword,
|
||||||
},
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
provider: "azure",
|
provider: "azure",
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS With invalid CA cert",
|
||||||
|
wantErr: true,
|
||||||
|
registryOpts: registryOptions{
|
||||||
|
withTLS: true,
|
||||||
|
withClientCertAuth: true,
|
||||||
|
},
|
||||||
|
secretOpts: secretOptions{
|
||||||
|
username: testRegistryUsername,
|
||||||
|
password: testRegistryPassword,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
|
certsSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "certs-secretref",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": []byte("invalid caFile"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing object: new generation 0 -> 1"),
|
||||||
|
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid caFile"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS With CA cert",
|
||||||
|
want: ctrl.Result{RequeueAfter: interval},
|
||||||
|
registryOpts: registryOptions{
|
||||||
|
withTLS: true,
|
||||||
|
withClientCertAuth: true,
|
||||||
|
},
|
||||||
|
secretOpts: secretOptions{
|
||||||
|
username: testRegistryUsername,
|
||||||
|
password: testRegistryPassword,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-secretref",
|
||||||
|
},
|
||||||
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
|
certsSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "certs-secretref",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": tlsCA,
|
||||||
|
"certFile": clientPublicKey,
|
||||||
|
"keyFile": clientPrivateKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -285,7 +376,9 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
WithStatusSubresource(&helmv1.HelmRepository{})
|
WithStatusSubresource(&helmv1.HelmRepository{})
|
||||||
|
|
||||||
workspaceDir := t.TempDir()
|
workspaceDir := t.TempDir()
|
||||||
|
if tt.insecure {
|
||||||
tt.registryOpts.disableDNSMocking = true
|
tt.registryOpts.disableDNSMocking = true
|
||||||
|
}
|
||||||
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
|
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
@ -317,28 +410,27 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
|
if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
|
||||||
secret := &corev1.Secret{
|
tt.secret.Data[".dockerconfigjson"] = []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
server.registryHost, tt.secretOpts.username, tt.secretOpts.password))
|
||||||
Name: "auth-secretref",
|
|
||||||
},
|
|
||||||
Type: corev1.SecretTypeDockerConfigJson,
|
|
||||||
Data: map[string][]byte{
|
|
||||||
".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
|
|
||||||
server.registryHost, tt.secretOpts.username, tt.secretOpts.password)),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clientBuilder.WithObjects(secret)
|
if tt.secret != nil {
|
||||||
|
clientBuilder.WithObjects(tt.secret)
|
||||||
obj.Spec.SecretRef = &meta.LocalObjectReference{
|
obj.Spec.SecretRef = &meta.LocalObjectReference{
|
||||||
Name: secret.Name,
|
Name: tt.secret.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.certsSecret != nil {
|
||||||
|
clientBuilder.WithObjects(tt.certsSecret)
|
||||||
|
obj.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||||
|
Name: tt.certsSecret.Name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &HelmRepositoryOCIReconciler{
|
r := &HelmRepositoryOCIReconciler{
|
||||||
Client: clientBuilder.Build(),
|
Client: clientBuilder.Build(),
|
||||||
EventRecorder: record.NewFakeRecorder(32),
|
EventRecorder: record.NewFakeRecorder(32),
|
||||||
Getters: testGetters,
|
|
||||||
RegistryClientGenerator: registry.ClientGenerator,
|
RegistryClientGenerator: registry.ClientGenerator,
|
||||||
patchOptions: getPatchOptions(helmRepositoryOCIOwnedConditions, "sc"),
|
patchOptions: getPatchOptions(helmRepositoryOCIOwnedConditions, "sc"),
|
||||||
}
|
}
|
||||||
|
@ -349,7 +441,6 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sp := patch.NewSerialPatcher(obj, r.Client)
|
sp := patch.NewSerialPatcher(obj, r.Client)
|
||||||
|
|
||||||
got, err := r.reconcile(ctx, sp, obj)
|
got, err := r.reconcile(ctx, sp, obj)
|
||||||
g.Expect(err != nil).To(Equal(tt.wantErr))
|
g.Expect(err != nil).To(Equal(tt.wantErr))
|
||||||
g.Expect(got).To(Equal(tt.want))
|
g.Expect(got).To(Equal(tt.want))
|
||||||
|
|
|
@ -796,7 +796,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
if tt.url != "" {
|
if tt.url != "" {
|
||||||
repoURL = tt.url
|
repoURL = tt.url
|
||||||
}
|
}
|
||||||
tlsConf, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
|
tlsConf, _, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
validSecret = false
|
validSecret = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,15 @@ package controller
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -148,15 +151,11 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
server.out = &out
|
server.out = &out
|
||||||
|
|
||||||
// init test client
|
// init test client options
|
||||||
client, err := helmreg.NewClient(
|
clientOpts := []helmreg.ClientOption{
|
||||||
helmreg.ClientOptDebug(true),
|
helmreg.ClientOptDebug(true),
|
||||||
helmreg.ClientOptWriter(server.out),
|
helmreg.ClientOptWriter(server.out),
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create registry client: %s", err)
|
|
||||||
}
|
}
|
||||||
server.registryClient = client
|
|
||||||
|
|
||||||
config := &configuration.Configuration{}
|
config := &configuration.Configuration{}
|
||||||
port, err := freeport.GetFreePort()
|
port, err := freeport.GetFreePort()
|
||||||
|
@ -218,6 +217,13 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
|
||||||
if opts.withClientCertAuth {
|
if opts.withClientCertAuth {
|
||||||
config.HTTP.TLS.ClientCAs = []string{"testdata/certs/ca.pem"}
|
config.HTTP.TLS.ClientCAs = []string{"testdata/certs/ca.pem"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add TLS configured HTTP client option to clientOpts
|
||||||
|
httpClient, err := tlsConfiguredHTTPCLient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create TLS configured HTTP client: %s", err)
|
||||||
|
}
|
||||||
|
clientOpts = append(clientOpts, helmreg.ClientOptHTTPClient(httpClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup logger options
|
// setup logger options
|
||||||
|
@ -232,12 +238,41 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
|
||||||
return nil, fmt.Errorf("failed to create docker registry: %w", err)
|
return nil, fmt.Errorf("failed to create docker registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init test client
|
||||||
|
client, err := helmreg.NewClient(clientOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create registry client: %s", err)
|
||||||
|
}
|
||||||
|
server.registryClient = client
|
||||||
|
|
||||||
// Start Docker registry
|
// Start Docker registry
|
||||||
go dockerRegistry.ListenAndServe()
|
go dockerRegistry.ListenAndServe()
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tlsConfiguredHTTPCLient() (*http.Client, error) {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if !pool.AppendCertsFromPEM(tlsCA) {
|
||||||
|
return nil, fmt.Errorf("failed to append CA certificate to pool")
|
||||||
|
}
|
||||||
|
cert, err := tls.LoadX509KeyPair("testdata/certs/server.pem", "testdata/certs/server-key.pem")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load server certificate: %s", err)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: pool,
|
||||||
|
Certificates: []tls.Certificate{
|
||||||
|
cert,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return httpClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *registryClientTestServer) Close() {
|
func (r *registryClientTestServer) Close() {
|
||||||
if r.dnsServer != nil {
|
if r.dnsServer != nil {
|
||||||
mockdns.UnpatchNet(net.DefaultResolver)
|
mockdns.UnpatchNet(net.DefaultResolver)
|
||||||
|
@ -345,7 +380,6 @@ func TestMain(m *testing.M) {
|
||||||
Client: testEnv,
|
Client: testEnv,
|
||||||
EventRecorder: record.NewFakeRecorder(32),
|
EventRecorder: record.NewFakeRecorder(32),
|
||||||
Metrics: testMetricsH,
|
Metrics: testMetricsH,
|
||||||
Getters: testGetters,
|
|
||||||
RegistryClientGenerator: registry.ClientGenerator,
|
RegistryClientGenerator: registry.ClientGenerator,
|
||||||
}).SetupWithManagerAndOptions(testEnv, HelmRepositoryReconcilerOptions{
|
}).SetupWithManagerAndOptions(testEnv, HelmRepositoryReconcilerOptions{
|
||||||
RateLimiter: controller.GetDefaultRateLimiter(),
|
RateLimiter: controller.GetDefaultRateLimiter(),
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/oci"
|
"github.com/fluxcd/pkg/oci"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
@ -37,23 +39,47 @@ import (
|
||||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certFileName = "cert.pem"
|
||||||
|
keyFileName = "key.pem"
|
||||||
|
caFileName = "ca.pem"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
|
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
|
||||||
|
|
||||||
|
// TLSBytes contains the bytes of the TLS files.
|
||||||
|
type TLSBytes struct {
|
||||||
|
// CertBytes is the bytes of the certificate file.
|
||||||
|
CertBytes []byte
|
||||||
|
// KeyBytes is the bytes of the key file.
|
||||||
|
KeyBytes []byte
|
||||||
|
// CABytes is the bytes of the CA file.
|
||||||
|
CABytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
// ClientOpts contains the various options to use while constructing
|
// ClientOpts contains the various options to use while constructing
|
||||||
// a Helm repository client.
|
// a Helm repository client.
|
||||||
type ClientOpts struct {
|
type ClientOpts struct {
|
||||||
Authenticator authn.Authenticator
|
Authenticator authn.Authenticator
|
||||||
Keychain authn.Keychain
|
Keychain authn.Keychain
|
||||||
RegLoginOpt helmreg.LoginOption
|
RegLoginOpts []helmreg.LoginOption
|
||||||
TlsConfig *tls.Config
|
TlsConfig *tls.Config
|
||||||
GetterOpts []helmgetter.Option
|
GetterOpts []helmgetter.Option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustLoginToRegistry returns true if the client options contain at least
|
||||||
|
// one registry login option.
|
||||||
|
func (o ClientOpts) MustLoginToRegistry() bool {
|
||||||
|
return len(o.RegLoginOpts) > 0 && o.RegLoginOpts[0] != nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetClientOpts uses the provided HelmRepository object and a normalized
|
// GetClientOpts uses the provided HelmRepository object and a normalized
|
||||||
// URL to construct a HelmClientOpts object. If obj is an OCI HelmRepository,
|
// URL to construct a HelmClientOpts object. If obj is an OCI HelmRepository,
|
||||||
// then the returned options object will also contain the required registry
|
// then the returned options object will also contain the required registry
|
||||||
// auth mechanisms.
|
// auth mechanisms.
|
||||||
func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmRepository, url string) (*ClientOpts, error) {
|
// 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 *helmv1.HelmRepository, url string) (*ClientOpts, string, error) {
|
||||||
hrOpts := &ClientOpts{
|
hrOpts := &ClientOpts{
|
||||||
GetterOpts: []helmgetter.Option{
|
GetterOpts: []helmgetter.Option{
|
||||||
helmgetter.WithURL(url),
|
helmgetter.WithURL(url),
|
||||||
|
@ -63,18 +89,25 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
}
|
}
|
||||||
ociRepo := obj.Spec.Type == helmv1.HelmRepositoryTypeOCI
|
ociRepo := obj.Spec.Type == helmv1.HelmRepositoryTypeOCI
|
||||||
|
|
||||||
var certSecret *corev1.Secret
|
var (
|
||||||
var err error
|
certSecret *corev1.Secret
|
||||||
|
tlsBytes *TLSBytes
|
||||||
|
certFile string
|
||||||
|
keyFile string
|
||||||
|
caFile string
|
||||||
|
dir string
|
||||||
|
err error
|
||||||
|
)
|
||||||
// Check `.spec.certSecretRef` first for any TLS auth data.
|
// Check `.spec.certSecretRef` first for any TLS auth data.
|
||||||
if obj.Spec.CertSecretRef != nil {
|
if obj.Spec.CertSecretRef != nil {
|
||||||
certSecret, err = fetchSecret(ctx, c, obj.Spec.CertSecretRef.Name, obj.GetNamespace())
|
certSecret, err = fetchSecret(ctx, c, obj.Spec.CertSecretRef.Name, obj.GetNamespace())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get TLS authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.CertSecretRef.Name, err)
|
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)
|
hrOpts.TlsConfig, tlsBytes, err = TLSClientConfigFromSecret(*certSecret, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,22 +116,22 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
if obj.Spec.SecretRef != nil {
|
if obj.Spec.SecretRef != nil {
|
||||||
authSecret, err = fetchSecret(ctx, c, obj.Spec.SecretRef.Name, obj.GetNamespace())
|
authSecret, err = fetchSecret(ctx, c, obj.Spec.SecretRef.Name, obj.GetNamespace())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.SecretRef.Name, err)
|
return nil, "", fmt.Errorf("failed to get authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.SecretRef.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct actual Helm client options.
|
// Construct actual Helm client options.
|
||||||
opts, err := GetterOptionsFromSecret(*authSecret)
|
opts, err := GetterOptionsFromSecret(*authSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to configure Helm client: %w", err)
|
return nil, "", fmt.Errorf("failed to configure Helm client: %w", err)
|
||||||
}
|
}
|
||||||
hrOpts.GetterOpts = append(hrOpts.GetterOpts, opts...)
|
hrOpts.GetterOpts = append(hrOpts.GetterOpts, opts...)
|
||||||
|
|
||||||
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
|
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
|
||||||
// then try to use `.spec.certSecretRef`.
|
// then try to use `.spec.secretRef`.
|
||||||
if hrOpts.TlsConfig == nil {
|
if hrOpts.TlsConfig == nil {
|
||||||
hrOpts.TlsConfig, err = TLSClientConfigFromSecret(*authSecret, url)
|
hrOpts.TlsConfig, tlsBytes, err = TLSClientConfigFromSecret(*authSecret, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
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.
|
// Constructing a TLS config using the auth secret is deprecated behavior.
|
||||||
if hrOpts.TlsConfig != nil {
|
if hrOpts.TlsConfig != nil {
|
||||||
|
@ -109,13 +142,13 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
if ociRepo {
|
if ociRepo {
|
||||||
hrOpts.Keychain, err = registry.LoginOptionFromSecret(url, *authSecret)
|
hrOpts.Keychain, err = registry.LoginOptionFromSecret(url, *authSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to configure login options: %w", err)
|
return nil, "", fmt.Errorf("failed to configure login options: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI && ociRepo {
|
} else if obj.Spec.Provider != helmv1.GenericOCIProvider && obj.Spec.Type == helmv1.HelmRepositoryTypeOCI && ociRepo {
|
||||||
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
|
authenticator, authErr := soci.OIDCAuth(ctx, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
return nil, fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
|
return nil, "", fmt.Errorf("failed to get credential from '%s': %w", obj.Spec.Provider, authErr)
|
||||||
}
|
}
|
||||||
if authenticator != nil {
|
if authenticator != nil {
|
||||||
hrOpts.Authenticator = authenticator
|
hrOpts.Authenticator = authenticator
|
||||||
|
@ -123,16 +156,34 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
}
|
}
|
||||||
|
|
||||||
if ociRepo {
|
if ociRepo {
|
||||||
hrOpts.RegLoginOpt, err = registry.NewLoginOption(hrOpts.Authenticator, hrOpts.Keychain, url)
|
// Persist the certs files to the path if needed.
|
||||||
|
if tlsBytes != nil {
|
||||||
|
dir, err = os.MkdirTemp("", "helm-repo-oci-certs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if loginOpt != nil {
|
||||||
|
hrOpts.RegLoginOpts = []helmreg.LoginOption{loginOpt}
|
||||||
|
}
|
||||||
|
tlsLoginOpt := registry.TLSLoginOption(certFile, keyFile, caFile)
|
||||||
|
if tlsLoginOpt != nil {
|
||||||
|
hrOpts.RegLoginOpts = append(hrOpts.RegLoginOpts, tlsLoginOpt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if deprecatedTLSConfig {
|
if deprecatedTLSConfig {
|
||||||
err = ErrDeprecatedTLSConfig
|
err = ErrDeprecatedTLSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
return hrOpts, err
|
return hrOpts, dir, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
|
func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (*corev1.Secret, error) {
|
||||||
|
@ -152,13 +203,13 @@ func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (
|
||||||
//
|
//
|
||||||
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
||||||
// certBytes OR keyBytes is defined it returns an error.
|
// certBytes OR keyBytes is defined it returns an error.
|
||||||
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, error) {
|
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, *TLSBytes, error) {
|
||||||
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
||||||
switch {
|
switch {
|
||||||
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
||||||
return nil, nil
|
return nil, nil, nil
|
||||||
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
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",
|
return nil, nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
||||||
secret.Name)
|
secret.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +217,7 @@ func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls
|
||||||
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
||||||
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
||||||
}
|
}
|
||||||
|
@ -174,10 +225,10 @@ func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls
|
||||||
if len(caBytes) > 0 {
|
if len(caBytes) > 0 {
|
||||||
cp, err := x509.SystemCertPool()
|
cp, err := x509.SystemCertPool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
return nil, nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
||||||
}
|
}
|
||||||
if !cp.AppendCertsFromPEM(caBytes) {
|
if !cp.AppendCertsFromPEM(caBytes) {
|
||||||
return nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
|
return nil, nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf.RootCAs = cp
|
tlsConf.RootCAs = cp
|
||||||
|
@ -187,10 +238,50 @@ func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls
|
||||||
|
|
||||||
u, err := url.Parse(repositoryUrl)
|
u, err := url.Parse(repositoryUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
return nil, nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf.ServerName = u.Hostname()
|
tlsConf.ServerName = u.Hostname()
|
||||||
|
|
||||||
return tlsConf, nil
|
return tlsConf, &TLSBytes{
|
||||||
|
CertBytes: certBytes,
|
||||||
|
KeyBytes: keyBytes,
|
||||||
|
CABytes: caBytes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreTLSCertificateFiles writes the certs files to the given path and returns the files paths.
|
||||||
|
func StoreTLSCertificateFiles(tlsBytes *TLSBytes, 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return certFile, keyFile, caFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeToFile(data []byte, filename, tmpDir string) (string, error) {
|
||||||
|
file := path.Join(tmpDir, filename)
|
||||||
|
err := os.WriteFile(file, data, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,7 @@ func TestGetClientOpts(t *testing.T) {
|
||||||
}
|
}
|
||||||
c := clientBuilder.Build()
|
c := clientBuilder.Build()
|
||||||
|
|
||||||
clientOpts, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
|
clientOpts, _, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
|
||||||
if tt.err != nil {
|
if tt.err != nil {
|
||||||
g.Expect(err).To(Equal(tt.err))
|
g.Expect(err).To(Equal(tt.err))
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,7 +183,7 @@ func Test_tlsClientConfigFromSecret(t *testing.T) {
|
||||||
tt.modify(secret)
|
tt.modify(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := TLSClientConfigFromSecret(*secret, "")
|
got, _, err := TLSClientConfigFromSecret(*secret, "")
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -196,6 +196,118 @@ func Test_tlsClientConfigFromSecret(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetClientOpts_registryTLSLoginOption(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
|
||||||
|
loginOptsN int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with valid caFile",
|
||||||
|
certSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ca-file",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": tlsCA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-oci",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loginOptsN: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without caFile",
|
||||||
|
certSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ca-file",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
},
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-oci",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loginOptsN: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without cert secret",
|
||||||
|
certSecret: nil,
|
||||||
|
authSecret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "auth-oci",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"username": []byte("user"),
|
||||||
|
"password": []byte("pass"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loginOptsN: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
helmRepo := &helmv1.HelmRepository{
|
||||||
|
Spec: helmv1.HelmRepositorySpec{
|
||||||
|
Timeout: &metav1.Duration{
|
||||||
|
Duration: time.Second,
|
||||||
|
},
|
||||||
|
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, tmpDir, err := GetClientOpts(context.TODO(), c, helmRepo, "https://ghcr.io/dummy")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetClientOpts() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tmpDir != "" {
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
}
|
||||||
|
if tt.loginOptsN != len(clientOpts.RegLoginOpts) {
|
||||||
|
// we should have a login option but no TLS option
|
||||||
|
t.Error("registryTLSLoginOption() != nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
||||||
// valid from a syntax (minimum requirements) perspective.
|
// valid from a syntax (minimum requirements) perspective.
|
||||||
func validTlsSecret(t *testing.T) corev1.Secret {
|
func validTlsSecret(t *testing.T) corev1.Secret {
|
||||||
|
|
|
@ -154,3 +154,13 @@ func NewLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryU
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSLoginOption returns a LoginOption that can be used to configure the TLS client.
|
||||||
|
// It requires either the caFile or both certFile and keyFile to be not blank.
|
||||||
|
func TLSLoginOption(certFile, keyFile, caFile string) registry.LoginOption {
|
||||||
|
if (certFile != "" && keyFile != "") || caFile != "" {
|
||||||
|
return registry.LoginOptTLSClientConfig(certFile, keyFile, caFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@ limitations under the License.
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
|
@ -27,7 +29,7 @@ import (
|
||||||
// ClientGenerator generates a registry client and a temporary credential file.
|
// ClientGenerator generates a registry client and a temporary credential file.
|
||||||
// The client is meant to be used for a single reconciliation.
|
// The client is meant to be used for a single reconciliation.
|
||||||
// The file is meant to be used for a single reconciliation and deleted after.
|
// The file is meant to be used for a single reconciliation and deleted after.
|
||||||
func ClientGenerator(isLogin bool) (*registry.Client, string, error) {
|
func ClientGenerator(tlsConfig *tls.Config, isLogin bool) (*registry.Client, string, error) {
|
||||||
if isLogin {
|
if isLogin {
|
||||||
// create a temporary file to store the credentials
|
// create a temporary file to store the credentials
|
||||||
// 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.
|
||||||
|
@ -37,7 +39,7 @@ func ClientGenerator(isLogin bool) (*registry.Client, string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
rClient, err := registry.NewClient(registry.ClientOptWriter(io.Discard), registry.ClientOptCredentialsFile(credentialsFile.Name()))
|
rClient, err := newClient(credentialsFile.Name(), tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
// attempt to delete the temporary file
|
// attempt to delete the temporary file
|
||||||
|
@ -52,9 +54,27 @@ func ClientGenerator(isLogin bool) (*registry.Client, string, error) {
|
||||||
return rClient, credentialsFile.Name(), nil
|
return rClient, credentialsFile.Name(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rClient, err := registry.NewClient(registry.ClientOptWriter(io.Discard))
|
rClient, err := newClient("", tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
return rClient, "", nil
|
return rClient, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newClient(credentialsFile string, tlsConfig *tls.Config) (*registry.Client, error) {
|
||||||
|
opts := []registry.ClientOption{
|
||||||
|
registry.ClientOptWriter(io.Discard),
|
||||||
|
}
|
||||||
|
if tlsConfig != nil {
|
||||||
|
opts = append(opts, registry.ClientOptHTTPClient(&http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if credentialsFile != "" {
|
||||||
|
opts = append(opts, registry.ClientOptCredentialsFile(credentialsFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.NewClient(opts...)
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -65,9 +66,13 @@ type OCIChartRepository struct {
|
||||||
|
|
||||||
// RegistryClient is a client to use while downloading tags or charts from a registry.
|
// RegistryClient is a client to use while downloading tags or charts from a registry.
|
||||||
RegistryClient RegistryClient
|
RegistryClient RegistryClient
|
||||||
|
|
||||||
// credentialsFile is a temporary credentials file to use while downloading tags or charts from a registry.
|
// credentialsFile is a temporary credentials file to use while downloading tags or charts from a registry.
|
||||||
credentialsFile string
|
credentialsFile string
|
||||||
|
|
||||||
|
// certificatesStore is a temporary store to use while downloading tags or charts from a registry.
|
||||||
|
certificatesStore string
|
||||||
|
|
||||||
// verifiers is a list of verifiers to use when verifying a chart.
|
// verifiers is a list of verifiers to use when verifying a chart.
|
||||||
verifiers []oci.Verifier
|
verifiers []oci.Verifier
|
||||||
}
|
}
|
||||||
|
@ -120,6 +125,14 @@ func WithCredentialsFile(credentialsFile string) OCIChartRepositoryOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCertificatesStore returns a ChartRepositoryOption that will set the certificates store
|
||||||
|
func WithCertificatesStore(store string) OCIChartRepositoryOption {
|
||||||
|
return func(r *OCIChartRepository) error {
|
||||||
|
r.certificatesStore = store
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewOCIChartRepository constructs and returns a new ChartRepository with
|
// NewOCIChartRepository constructs and returns a new ChartRepository with
|
||||||
// the ChartRepository.Client configured to the getter.Getter for the
|
// the ChartRepository.Client configured to the getter.Getter for the
|
||||||
// repository URL scheme. It returns an error on URL parsing failures.
|
// repository URL scheme. It returns an error on URL parsing failures.
|
||||||
|
@ -265,14 +278,24 @@ func (r *OCIChartRepository) HasCredentials() bool {
|
||||||
|
|
||||||
// Clear deletes the OCI registry credentials file.
|
// Clear deletes the OCI registry credentials file.
|
||||||
func (r *OCIChartRepository) Clear() error {
|
func (r *OCIChartRepository) Clear() error {
|
||||||
|
var errs error
|
||||||
// clean the credentials file if it exists
|
// clean the credentials file if it exists
|
||||||
if r.credentialsFile != "" {
|
if r.credentialsFile != "" {
|
||||||
if err := os.Remove(r.credentialsFile); err != nil {
|
if err := os.Remove(r.credentialsFile); err != nil {
|
||||||
return err
|
errs = errors.Join(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.credentialsFile = ""
|
r.credentialsFile = ""
|
||||||
return nil
|
|
||||||
|
// clean the certificates store if it exists
|
||||||
|
if r.certificatesStore != "" {
|
||||||
|
if err := os.RemoveAll(r.certificatesStore); err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.certificatesStore = ""
|
||||||
|
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLastMatchingVersionOrConstraint returns the last version that matches the given version string.
|
// getLastMatchingVersionOrConstraint returns the last version that matches the given version string.
|
||||||
|
|
1
main.go
1
main.go
|
@ -198,7 +198,6 @@ func main() {
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
EventRecorder: eventRecorder,
|
EventRecorder: eventRecorder,
|
||||||
Metrics: metrics,
|
Metrics: metrics,
|
||||||
Getters: getters,
|
|
||||||
ControllerName: controllerName,
|
ControllerName: controllerName,
|
||||||
RegistryClientGenerator: registry.ClientGenerator,
|
RegistryClientGenerator: registry.ClientGenerator,
|
||||||
}).SetupWithManagerAndOptions(mgr, controller.HelmRepositoryReconcilerOptions{
|
}).SetupWithManagerAndOptions(mgr, controller.HelmRepositoryReconcilerOptions{
|
||||||
|
|
Loading…
Reference in New Issue