diff --git a/api/v1alpha1/condition_consts.go b/api/v1alpha1/condition_consts.go index 7d2e3131..d7a337bd 100644 --- a/api/v1alpha1/condition_consts.go +++ b/api/v1alpha1/condition_consts.go @@ -81,4 +81,7 @@ const ( // GetCredentialsFailedReason indicates that the HelmReleaseProxy failed to get the credentials for the Helm registry. GetCredentialsFailedReason = "GetCredentialsFailed" + + // GetCACertificateFailedReason indicates that the HelmReleaseProxy failed to get the CA certiicate for the Helm registry. + GetCACertificateFailedReason = "GetCACertificateFailed" ) diff --git a/api/v1alpha1/helmchartproxy_types.go b/api/v1alpha1/helmchartproxy_types.go index 7982c988..0ce52da2 100644 --- a/api/v1alpha1/helmchartproxy_types.go +++ b/api/v1alpha1/helmchartproxy_types.go @@ -72,6 +72,10 @@ type HelmChartProxySpec struct { // Credentials is a reference to an object containing the OCI credentials. If it is not specified, no credentials will be used. // +optional Credentials *Credentials `json:"credentials,omitempty"` + + // TLSConfig contains the TLS configuration for a HelmChartProxy. + // +optional + TLSConfig *TLSConfig `json:"tlsConfig,omitempty"` } type HelmOptions struct { @@ -199,6 +203,17 @@ type Credentials struct { Key string `json:"key"` } +// TLSConfig defines a TLS configuration. +type TLSConfig struct { + // Secret is a reference to a Secret containing the TLS CA certificate at the key ca.crt. + // +optional + CASecretRef *corev1.SecretReference `json:"caSecret,omitempty"` + + // InsecureSkipTLSVerify controls whether the Helm client should verify the server's certificate. + // +optional + InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` +} + // HelmChartProxyStatus defines the observed state of HelmChartProxy. type HelmChartProxyStatus struct { // Conditions defines current state of the HelmChartProxy. diff --git a/api/v1alpha1/helmreleaseproxy_types.go b/api/v1alpha1/helmreleaseproxy_types.go index f5a11d97..788f672a 100644 --- a/api/v1alpha1/helmreleaseproxy_types.go +++ b/api/v1alpha1/helmreleaseproxy_types.go @@ -74,6 +74,9 @@ type HelmReleaseProxySpec struct { // Credentials is a reference to an object containing the OCI credentials. If it is not specified, no credentials will be used. // +optional Credentials *Credentials `json:"credentials,omitempty"` + + // TLSConfig contains the TLS configuration for the HelmReleaseProxy. + TLSConfig *TLSConfig `json:"tlsConfig,omitempty"` } // HelmReleaseProxyStatus defines the observed state of HelmReleaseProxy. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b6f01426..19fb7024 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -112,6 +112,11 @@ func (in *HelmChartProxySpec) DeepCopyInto(out *HelmChartProxySpec) { *out = new(Credentials) **out = **in } + if in.TLSConfig != nil { + in, out := &in.TLSConfig, &out.TLSConfig + *out = new(TLSConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartProxySpec. @@ -262,6 +267,11 @@ func (in *HelmReleaseProxySpec) DeepCopyInto(out *HelmReleaseProxySpec) { *out = new(Credentials) **out = **in } + if in.TLSConfig != nil { + in, out := &in.TLSConfig, &out.TLSConfig + *out = new(TLSConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseProxySpec. @@ -325,3 +335,23 @@ func (in *HelmUpgradeOptions) DeepCopy() *HelmUpgradeOptions { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { + *out = *in + if in.CASecretRef != nil { + in, out := &in.CASecretRef, &out.CASecretRef + *out = new(corev1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. +func (in *TLSConfig) DeepCopy() *TLSConfig { + if in == nil { + return nil + } + out := new(TLSConfig) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml index 1d62b898..046a865d 100644 --- a/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml +++ b/config/crd/bases/addons.cluster.x-k8s.io_helmchartproxies.yaml @@ -261,6 +261,28 @@ spec: RepoURL is the URL of the Helm chart repository. e.g. chart-path oci://repo-url/chart-name as repoURL: oci://repo-url and https://repo-url/chart-name as repoURL: https://repo-url type: string + tlsConfig: + description: TLSConfig contains the TLS configuration for a HelmChartProxy. + properties: + caSecret: + description: Secret is a reference to a Secret containing the + TLS CA certificate at the key ca.crt. + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + insecureSkipTLSVerify: + description: InsecureSkipTLSVerify controls whether the Helm client + should verify the server's certificate. + type: boolean + type: object valuesTemplate: description: |- ValuesTemplate is an inline YAML representing the values for the Helm chart. This YAML supports Go templating to reference diff --git a/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml b/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml index 9aeae0b2..131d0e4f 100644 --- a/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml +++ b/config/crd/bases/addons.cluster.x-k8s.io_helmreleaseproxies.yaml @@ -270,6 +270,28 @@ spec: RepoURL is the URL of the Helm chart repository. e.g. chart-path oci://repo-url/chart-name as repoURL: oci://repo-url and https://repo-url/chart-name as repoURL: https://repo-url type: string + tlsConfig: + description: TLSConfig contains the TLS configuration for the HelmReleaseProxy. + properties: + caSecret: + description: Secret is a reference to a Secret containing the + TLS CA certificate at the key ca.crt. + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + insecureSkipTLSVerify: + description: InsecureSkipTLSVerify controls whether the Helm client + should verify the server's certificate. + type: boolean + type: object values: description: |- Values is an inline YAML representing the values for the Helm chart. This YAML is the result of the rendered diff --git a/controllers/helmchartproxy/helmchartproxy_controller_phases.go b/controllers/helmchartproxy/helmchartproxy_controller_phases.go index dc820e2c..1c5f3180 100644 --- a/controllers/helmchartproxy/helmchartproxy_controller_phases.go +++ b/controllers/helmchartproxy/helmchartproxy_controller_phases.go @@ -235,6 +235,15 @@ func constructHelmReleaseProxy(existing *addonsv1alpha1.HelmReleaseProxy, helmCh } } + helmReleaseProxy.Spec.TLSConfig = helmChartProxy.Spec.TLSConfig + + if helmReleaseProxy.Spec.TLSConfig != nil { + // If the namespace is not set, set it to the namespace of the HelmChartProxy + if helmReleaseProxy.Spec.TLSConfig.CASecretRef.Namespace == "" { + helmReleaseProxy.Spec.TLSConfig.CASecretRef.Namespace = helmChartProxy.Namespace + } + } + return helmReleaseProxy } diff --git a/controllers/helmreleaseproxy/helmreleaseproxy_controller.go b/controllers/helmreleaseproxy/helmreleaseproxy_controller.go index 89006388..0dada508 100644 --- a/controllers/helmreleaseproxy/helmreleaseproxy_controller.go +++ b/controllers/helmreleaseproxy/helmreleaseproxy_controller.go @@ -224,21 +224,39 @@ func (r *HelmReleaseProxyReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, wrappedErr } - defer func() { - if err := os.Remove(credentialsPath); err != nil { - log.Error(err, "failed to remove credentials file in path", "credentialsPath", credentialsPath) - } - }() + if credentialsPath != "" { + defer func() { + if err := os.Remove(credentialsPath); err != nil { + log.Error(err, "failed to remove credentials file in path", "credentialsPath", credentialsPath) + } + }() + } + + caFilePath, err := r.getCAFile(ctx, helmReleaseProxy) + if err != nil { + wrappedErr := errors.Wrapf(err, "failed to get CA certificate file for cluster") + conditions.MarkFalse(helmReleaseProxy, addonsv1alpha1.ClusterAvailableCondition, addonsv1alpha1.GetCACertificateFailedReason, clusterv1.ConditionSeverityError, wrappedErr.Error()) + + return ctrl.Result{}, wrappedErr + } + + if caFilePath != "" { + defer func() { + if err := os.Remove(caFilePath); err != nil { + log.Error(err, "failed to remove CA certificate file in path", "credentialsPath", caFilePath) + } + }() + } log.V(2).Info("Reconciling HelmReleaseProxy", "releaseProxyName", helmReleaseProxy.Name) - err = r.reconcileNormal(ctx, helmReleaseProxy, r.HelmClient, credentialsPath, kubeconfig) + err = r.reconcileNormal(ctx, helmReleaseProxy, r.HelmClient, credentialsPath, caFilePath, kubeconfig) return ctrl.Result{}, err } // reconcileNormal handles HelmReleaseProxy reconciliation when it is not being deleted. This will install or upgrade the HelmReleaseProxy on the Cluster. // It will set the ReleaseName on the HelmReleaseProxy if the name is generated and also set the release status and release revision. -func (r *HelmReleaseProxyReconciler) reconcileNormal(ctx context.Context, helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy, client internal.Client, credentialsPath string, kubeconfig string) error { +func (r *HelmReleaseProxyReconciler) reconcileNormal(ctx context.Context, helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy, client internal.Client, credentialsPath, caFilePath string, kubeconfig string) error { log := ctrl.LoggerFrom(ctx) log.V(2).Info("Reconciling HelmReleaseProxy on cluster", "HelmReleaseProxy", helmReleaseProxy.Name, "cluster", helmReleaseProxy.Spec.ClusterRef.Name) @@ -250,7 +268,7 @@ func (r *HelmReleaseProxyReconciler) reconcileNormal(ctx context.Context, helmRe }) } - release, err := client.InstallOrUpgradeHelmRelease(ctx, kubeconfig, credentialsPath, helmReleaseProxy.Spec) + release, err := client.InstallOrUpgradeHelmRelease(ctx, kubeconfig, credentialsPath, caFilePath, helmReleaseProxy.Spec) if err != nil { log.Error(err, fmt.Sprintf("Failed to install or upgrade release '%s' on cluster %s", helmReleaseProxy.Spec.ReleaseName, helmReleaseProxy.Spec.ClusterRef.Name)) conditions.MarkFalse(helmReleaseProxy, addonsv1alpha1.HelmReleaseReadyCondition, addonsv1alpha1.HelmInstallOrUpgradeFailedReason, clusterv1.ConditionSeverityError, err.Error()) @@ -377,6 +395,31 @@ func (r *HelmReleaseProxyReconciler) getCredentials(ctx context.Context, helmRel return credentialsPath, nil } +// getCAFile fetches the CA certificate from a Secret and writes it to a temporary file, returning the path to the temporary file. +func (r *HelmReleaseProxyReconciler) getCAFile(ctx context.Context, helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy) (string, error) { + caFilePath := "" + if helmReleaseProxy.Spec.TLSConfig != nil && helmReleaseProxy.Spec.TLSConfig.CASecretRef.Name != "" { + // By default, the secret is in the same namespace as the HelmReleaseProxy + if helmReleaseProxy.Spec.TLSConfig.CASecretRef.Namespace == "" { + helmReleaseProxy.Spec.TLSConfig.CASecretRef.Namespace = helmReleaseProxy.Namespace + } + caSecretValues, err := r.getCACertificateFromSecret(ctx, helmReleaseProxy.Spec.TLSConfig.CASecretRef.Name, helmReleaseProxy.Spec.TLSConfig.CASecretRef.Namespace) + if err != nil { + return "", err + } + + // Write to a file + filename, err := writeCACertificateToFile(ctx, caSecretValues) + if err != nil { + return "", err + } + + caFilePath = filename + } + + return caFilePath, nil +} + // getCredentialsFromSecret returns the OCI credentials from a Secret. func (r *HelmReleaseProxyReconciler) getCredentialsFromSecret(ctx context.Context, name, namespace, key string) ([]byte, error) { secret := &corev1.Secret{} @@ -407,3 +450,35 @@ func writeCredentialsToFile(ctx context.Context, credentials []byte) (string, er return credentialsFile.Name(), nil } + +// getCredentialsFromSecret returns the OCI credentials from a Secret. +func (r *HelmReleaseProxyReconciler) getCACertificateFromSecret(ctx context.Context, name, namespace string) ([]byte, error) { + secret := &corev1.Secret{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret); err != nil { + return nil, err + } + + const key = "ca.crt" + credentials, ok := secret.Data[key] + if !ok { + return nil, errors.New(fmt.Sprintf("key %s not found in secret %s/%s", key, namespace, name)) + } + + return credentials, nil +} + +// writeCACertificateToFile writes the CA certificate to a temporary file. +func writeCACertificateToFile(ctx context.Context, caCertificate []byte) (string, error) { + log := ctrl.LoggerFrom(ctx) + log.V(2).Info("Writing CA certficate to file") + caCertFile, err := os.CreateTemp("", "ca-*.crt") + if err != nil { + return "", err + } + + if _, err := caCertFile.Write(caCertificate); err != nil { + return "", err + } + + return caCertFile.Name(), nil +} diff --git a/controllers/helmreleaseproxy/helmreleaseproxy_controller_test.go b/controllers/helmreleaseproxy/helmreleaseproxy_controller_test.go index f67f0623..d2d4288d 100644 --- a/controllers/helmreleaseproxy/helmreleaseproxy_controller_test.go +++ b/controllers/helmreleaseproxy/helmreleaseproxy_controller_test.go @@ -99,6 +99,37 @@ var ( }, } + defaultProxyWithCACertRef = &addonsv1alpha1.HelmReleaseProxy{ + TypeMeta: metav1.TypeMeta{ + Kind: "HelmReleaseProxy", + APIVersion: "addons.cluster.x-k8s.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-proxy", + Namespace: "default", + }, + Spec: addonsv1alpha1.HelmReleaseProxySpec{ + ClusterRef: corev1.ObjectReference{ + APIVersion: "cluster.x-k8s.io/v1beta1", + Kind: "Cluster", + Namespace: "default", + Name: "test-cluster", + }, + RepoURL: "https://test-repo", + ChartName: "test-chart", + Version: "test-version", + ReleaseName: "test-release", + ReleaseNamespace: "default", + Values: "test-values", + TLSConfig: &addonsv1alpha1.TLSConfig{ + CASecretRef: &corev1.SecretReference{ + Name: "test-secret", + Namespace: "default", + }, + }, + }, + } + generateNameProxy = &addonsv1alpha1.HelmReleaseProxy{ TypeMeta: metav1.TypeMeta{ Kind: "HelmReleaseProxy", @@ -140,7 +171,7 @@ func TestReconcileNormal(t *testing.T) { name: "successfully install a Helm release", helmReleaseProxy: defaultProxy.DeepCopy(), clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", defaultProxy.DeepCopy().Spec).Return(&helmRelease.Release{ + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "", defaultProxy.DeepCopy().Spec).Return(&helmRelease.Release{ Name: "test-release", Version: 1, Info: &helmRelease.Info{ @@ -164,7 +195,7 @@ func TestReconcileNormal(t *testing.T) { name: "successfully install a Helm release with a generated name", helmReleaseProxy: generateNameProxy, clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", generateNameProxy.Spec).Return(&helmRelease.Release{ + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "", generateNameProxy.Spec).Return(&helmRelease.Release{ Name: "test-release", Version: 1, Info: &helmRelease.Info{ @@ -188,7 +219,7 @@ func TestReconcileNormal(t *testing.T) { name: "Helm release pending", helmReleaseProxy: defaultProxy.DeepCopy(), clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", defaultProxy.Spec).Return(&helmRelease.Release{ + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "", defaultProxy.Spec).Return(&helmRelease.Release{ Name: "test-release", Version: 1, Info: &helmRelease.Info{ @@ -215,7 +246,7 @@ func TestReconcileNormal(t *testing.T) { name: "Helm client returns error", helmReleaseProxy: defaultProxy.DeepCopy(), clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", defaultProxy.Spec).Return(nil, errInternal).Times(1) + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "", defaultProxy.Spec).Return(nil, errInternal).Times(1) }, expect: func(g *WithT, hrp *addonsv1alpha1.HelmReleaseProxy) { _, ok := hrp.Annotations[addonsv1alpha1.IsReleaseNameGeneratedAnnotation] @@ -233,7 +264,7 @@ func TestReconcileNormal(t *testing.T) { name: "Helm release in a failed state, no client error", helmReleaseProxy: defaultProxy.DeepCopy(), clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", defaultProxy.Spec).Return(&helmRelease.Release{ + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "", defaultProxy.Spec).Return(&helmRelease.Release{ Name: "test-release", Version: 1, Info: &helmRelease.Info{ @@ -273,7 +304,7 @@ func TestReconcileNormal(t *testing.T) { Build(), } - err := r.reconcileNormal(ctx, tc.helmReleaseProxy, clientMock, "", kubeconfig) + err := r.reconcileNormal(ctx, tc.helmReleaseProxy, clientMock, "", "", kubeconfig) if tc.expectedError != "" { g.Expect(err).To(HaveOccurred()) g.Expect(err).To(MatchError(tc.expectedError), err.Error()) @@ -299,7 +330,73 @@ func TestReconcileNormalWithCredentialRef(t *testing.T) { name: "successfully install a Helm release", helmReleaseProxy: defaultProxyWithCredentialRef.DeepCopy(), clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { - c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "/tmp/oci-credentials-xyz.json", defaultProxyWithCredentialRef.Spec).Return(&helmRelease.Release{ + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "/tmp/oci-credentials-xyz.json", "", defaultProxyWithCredentialRef.Spec).Return(&helmRelease.Release{ + Name: "test-release", + Version: 1, + Info: &helmRelease.Info{ + Status: helmRelease.StatusDeployed, + }, + }, nil).Times(1) + }, + expect: func(g *WithT, hrp *addonsv1alpha1.HelmReleaseProxy) { + _, ok := hrp.Annotations[addonsv1alpha1.IsReleaseNameGeneratedAnnotation] + g.Expect(ok).To(BeFalse()) + g.Expect(hrp.Spec.ReleaseName).To(Equal("test-release")) + g.Expect(hrp.Status.Revision).To(Equal(1)) + g.Expect(hrp.Status.Status).To(BeEquivalentTo(helmRelease.StatusDeployed)) + + g.Expect(conditions.Has(hrp, addonsv1alpha1.HelmReleaseReadyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(hrp, addonsv1alpha1.HelmReleaseReadyCondition)).To(BeTrue()) + }, + expectedError: "", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + clientMock := mocks.NewMockClient(mockCtrl) + tc.clientExpect(g, clientMock.EXPECT()) + + r := &HelmReleaseProxyReconciler{ + Client: fake.NewClientBuilder(). + WithScheme(fakeScheme). + WithStatusSubresource(&addonsv1alpha1.HelmReleaseProxy{}). + Build(), + } + + err := r.reconcileNormal(ctx, tc.helmReleaseProxy, clientMock, "/tmp/oci-credentials-xyz.json", "", kubeconfig) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError), err.Error()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + tc.expect(g, tc.helmReleaseProxy) + } + }) + } +} + +func TestReconcileNormalWithACertificateRef(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + helmReleaseProxy *addonsv1alpha1.HelmReleaseProxy + clientExpect func(g *WithT, c *mocks.MockClientMockRecorder) + expect func(g *WithT, hrp *addonsv1alpha1.HelmReleaseProxy) + expectedError string + }{ + { + name: "successfully install a Helm release", + helmReleaseProxy: defaultProxyWithCACertRef.DeepCopy(), + clientExpect: func(g *WithT, c *mocks.MockClientMockRecorder) { + c.InstallOrUpgradeHelmRelease(ctx, kubeconfig, "", "/tmp/ca-xyz.crt", defaultProxyWithCACertRef.Spec).Return(&helmRelease.Release{ Name: "test-release", Version: 1, Info: &helmRelease.Info{ @@ -339,7 +436,7 @@ func TestReconcileNormalWithCredentialRef(t *testing.T) { Build(), } - err := r.reconcileNormal(ctx, tc.helmReleaseProxy, clientMock, "/tmp/oci-credentials-xyz.json", kubeconfig) + err := r.reconcileNormal(ctx, tc.helmReleaseProxy, clientMock, "", "/tmp/ca-xyz.crt", kubeconfig) if tc.expectedError != "" { g.Expect(err).To(HaveOccurred()) g.Expect(err).To(MatchError(tc.expectedError), err.Error()) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 7ae98f8b..5fc636ed 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -131,7 +131,7 @@ var _ = BeforeSuite(func() { kubeconfigGetter = mocks.NewMockGetter(gomock.NewController(&TestReporter{})) kubeconfigGetter.EXPECT().GetClusterKubeconfig(gomock.Any(), client.ObjectKeyFromObject(cluster1)).Return(kubeconfig, nil).AnyTimes() - helmClient.EXPECT().InstallOrUpgradeHelmRelease(gomock.Any(), kubeconfig, gomock.Any(), gomock.Any()).Return(helmReleaseDeployed, nil).AnyTimes() + helmClient.EXPECT().InstallOrUpgradeHelmRelease(gomock.Any(), kubeconfig, gomock.Any(), gomock.Any(), gomock.Any()).Return(helmReleaseDeployed, nil).AnyTimes() helmClient.EXPECT().UninstallHelmRelease(gomock.Any(), kubeconfig, gomock.Any()).Return(&helmRelease.UninstallReleaseResponse{}, nil).AnyTimes() helmClient.EXPECT().GetHelmRelease(gomock.Any(), kubeconfig, gomock.Any()).Return(&helmRelease.Release{}, nil).AnyTimes() diff --git a/internal/helm_client.go b/internal/helm_client.go index c5e3f47c..734773ee 100644 --- a/internal/helm_client.go +++ b/internal/helm_client.go @@ -39,12 +39,13 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" + "k8s.io/utils/ptr" addonsv1alpha1 "sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" ) type Client interface { - InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig string, credentialsPath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) + InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig string, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) GetHelmRelease(ctx context.Context, kubeconfig string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) UninstallHelmRelease(ctx context.Context, kubeconfig string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.UninstallReleaseResponse, error) } @@ -114,7 +115,7 @@ func HelmInit(ctx context.Context, namespace string, kubeconfig string) (*helmCl // InstallOrUpgradeHelmRelease installs a Helm release if it does not exist, or upgrades it if it does and differs from the spec. // It returns a boolean indicating whether an install or upgrade was performed. -func (c *HelmClient) InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig string, credentialsPath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) { +func (c *HelmClient) InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig string, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) { log := ctrl.LoggerFrom(ctx) log.V(2).Info("Installing or upgrading Helm release") @@ -125,13 +126,13 @@ func (c *HelmClient) InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig existingRelease, err := c.GetHelmRelease(ctx, kubeconfig, spec) if err != nil { if errors.Is(err, helmDriver.ErrReleaseNotFound) { - return c.InstallHelmRelease(ctx, kubeconfig, credentialsPath, spec) + return c.InstallHelmRelease(ctx, kubeconfig, credentialsPath, caFilePath, spec) } return nil, err } - return c.UpgradeHelmReleaseIfChanged(ctx, kubeconfig, credentialsPath, spec, existingRelease) + return c.UpgradeHelmReleaseIfChanged(ctx, kubeconfig, credentialsPath, caFilePath, spec, existingRelease) } // generateHelmInstallConfig generates default helm install config using helmOptions specified in HCP CR spec. @@ -191,7 +192,7 @@ func generateHelmUpgradeConfig(actionConfig *helmAction.Configuration, helmOptio } // InstallHelmRelease installs a Helm release. -func (c *HelmClient) InstallHelmRelease(ctx context.Context, kubeconfig string, credentialsPath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) { +func (c *HelmClient) InstallHelmRelease(ctx context.Context, kubeconfig string, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec) (*helmRelease.Release, error) { log := ctrl.LoggerFrom(ctx) settings, actionConfig, err := HelmInit(ctx, spec.ReleaseNamespace, kubeconfig) @@ -201,8 +202,9 @@ func (c *HelmClient) InstallHelmRelease(ctx context.Context, kubeconfig string, settings.RegistryConfig = credentialsPath enableClientCache := spec.Options.EnableClientCache - log.V(2).Info("Creating install registry client", "enableClientCache", enableClientCache, "credentialsPath", credentialsPath) - registryClient, err := newDefaultRegistryClient(credentialsPath, enableClientCache) + log.V(2).Info("Creating install registry client", "enableClientCache", enableClientCache, "credentialsPath", credentialsPath, "cFilePath", caFilePath) + + registryClient, err := newDefaultRegistryClient(credentialsPath, enableClientCache, caFilePath, ptr.Deref(spec.TLSConfig, addonsv1alpha1.TLSConfig{}).InsecureSkipTLSVerify) if err != nil { return nil, err } @@ -274,33 +276,22 @@ func (c *HelmClient) InstallHelmRelease(ctx context.Context, kubeconfig string, } // newDefaultRegistryClient creates registry client object with default config which can be used to install/upgrade helm charts. -func newDefaultRegistryClient(credentialsPath string, enableCache bool) (*registry.Client, error) { - var err error - var registryClient *registry.Client - if credentialsPath == "" { - // Create a new registry client - registryClient, err = registry.NewClient( +func newDefaultRegistryClient(credentialsPath string, enableCache bool, caFilePath string, insecureSkipTLSVerify bool) (*registry.Client, error) { + if caFilePath == "" || !insecureSkipTLSVerify { + opts := []registry.ClientOption{ registry.ClientOptDebug(true), registry.ClientOptEnableCache(enableCache), registry.ClientOptWriter(os.Stderr), - ) - if err != nil { - return nil, err } - } else { - // Create a new registry client with credentials - registryClient, err = registry.NewClient( - registry.ClientOptDebug(true), - registry.ClientOptEnableCache(enableCache), - registry.ClientOptWriter(os.Stderr), - registry.ClientOptCredentialsFile(credentialsPath), - ) - if err != nil { - return nil, err + if credentialsPath != "" { + // Create a new registry client with credentials + opts = append(opts, registry.ClientOptCredentialsFile(credentialsPath)) } + + return registry.NewClient(opts...) } - return registryClient, nil + return registry.NewRegistryClientWithTLS(os.Stderr, "", "", caFilePath, insecureSkipTLSVerify, credentialsPath, true) } // getHelmChartAndRepoName returns chartName, repoURL as per the format requirred in helm install/upgrade config. @@ -321,7 +312,7 @@ func getHelmChartAndRepoName(chartName, repoURL string) (string, string, error) } // UpgradeHelmReleaseIfChanged upgrades a Helm release. The boolean refers to if an upgrade was attempted. -func (c *HelmClient) UpgradeHelmReleaseIfChanged(ctx context.Context, kubeconfig string, credentialsPath string, spec addonsv1alpha1.HelmReleaseProxySpec, existing *helmRelease.Release) (*helmRelease.Release, error) { +func (c *HelmClient) UpgradeHelmReleaseIfChanged(ctx context.Context, kubeconfig string, credentialsPath, caFilePath string, spec addonsv1alpha1.HelmReleaseProxySpec, existing *helmRelease.Release) (*helmRelease.Release, error) { log := ctrl.LoggerFrom(ctx) settings, actionConfig, err := HelmInit(ctx, spec.ReleaseNamespace, kubeconfig) @@ -332,7 +323,8 @@ func (c *HelmClient) UpgradeHelmReleaseIfChanged(ctx context.Context, kubeconfig enableClientCache := spec.Options.EnableClientCache log.V(2).Info("Creating upgrade registry client", "enableClientCache", enableClientCache, "credentialsPath", credentialsPath) - registryClient, err := newDefaultRegistryClient(credentialsPath, enableClientCache) + + registryClient, err := newDefaultRegistryClient(credentialsPath, enableClientCache, caFilePath, ptr.Deref(spec.TLSConfig, addonsv1alpha1.TLSConfig{}).InsecureSkipTLSVerify) if err != nil { return nil, err } diff --git a/internal/mocks/helm_client_mock.go b/internal/mocks/helm_client_mock.go index 987243eb..9740dfeb 100644 --- a/internal/mocks/helm_client_mock.go +++ b/internal/mocks/helm_client_mock.go @@ -72,18 +72,18 @@ func (mr *MockClientMockRecorder) GetHelmRelease(ctx, kubeconfig, spec any) *gom } // InstallOrUpgradeHelmRelease mocks base method. -func (m *MockClient) InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig, credentialsPath string, spec v1alpha1.HelmReleaseProxySpec) (*release.Release, error) { +func (m *MockClient) InstallOrUpgradeHelmRelease(ctx context.Context, kubeconfig, credentialsPath, caFilePath string, spec v1alpha1.HelmReleaseProxySpec) (*release.Release, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstallOrUpgradeHelmRelease", ctx, kubeconfig, credentialsPath, spec) + ret := m.ctrl.Call(m, "InstallOrUpgradeHelmRelease", ctx, kubeconfig, credentialsPath, caFilePath, spec) ret0, _ := ret[0].(*release.Release) ret1, _ := ret[1].(error) return ret0, ret1 } // InstallOrUpgradeHelmRelease indicates an expected call of InstallOrUpgradeHelmRelease. -func (mr *MockClientMockRecorder) InstallOrUpgradeHelmRelease(ctx, kubeconfig, credentialsPath, spec any) *gomock.Call { +func (mr *MockClientMockRecorder) InstallOrUpgradeHelmRelease(ctx, kubeconfig, credentialsPath, caFilePath, spec any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallOrUpgradeHelmRelease", reflect.TypeOf((*MockClient)(nil).InstallOrUpgradeHelmRelease), ctx, kubeconfig, credentialsPath, spec) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallOrUpgradeHelmRelease", reflect.TypeOf((*MockClient)(nil).InstallOrUpgradeHelmRelease), ctx, kubeconfig, credentialsPath, caFilePath, spec) } // UninstallHelmRelease mocks base method.