From ea610829c3d24eca8e5b9fbef7d527ab2b221282 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Sun, 12 Apr 2020 20:23:51 +0200 Subject: [PATCH] Helm repository and chart HTTP and TLS auth --- api/v1alpha1/helmrepository_types.go | 9 ++- api/v1alpha1/zz_generated.deepcopy.go | 7 +- .../source.fluxcd.io_helmrepositories.yaml | 13 +++- controllers/gitrepository_controller.go | 2 +- controllers/helmchart_controller.go | 26 ++++++- controllers/helmrepository_controller.go | 33 +++++++-- internal/helm/getter.go | 73 +++++++++++++++++++ 7 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 internal/helm/getter.go diff --git a/api/v1alpha1/helmrepository_types.go b/api/v1alpha1/helmrepository_types.go index 665082ed..cd1eac32 100644 --- a/api/v1alpha1/helmrepository_types.go +++ b/api/v1alpha1/helmrepository_types.go @@ -23,11 +23,16 @@ import ( // HelmRepositorySpec defines the desired state of HelmRepository type HelmRepositorySpec struct { - // The repository address - // +kubebuilder:validation:MinLength=4 + // The Helm repository URL, a valid URL contains at least a + // protocol and host. // +required URL string `json:"url"` + // The name of the secret containing authentication credentials + // for the Helm repository. + // +optional + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + // The interval at which to check for repository updates // +required Interval metav1.Duration `json:"interval"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 26bdf49e..337c634b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -281,7 +281,7 @@ func (in *HelmRepository) DeepCopyInto(out *HelmRepository) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -338,6 +338,11 @@ func (in *HelmRepositoryList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmRepositorySpec) DeepCopyInto(out *HelmRepositorySpec) { *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } out.Interval = in.Interval } diff --git a/config/crd/bases/source.fluxcd.io_helmrepositories.yaml b/config/crd/bases/source.fluxcd.io_helmrepositories.yaml index 458b14c7..c43c968a 100644 --- a/config/crd/bases/source.fluxcd.io_helmrepositories.yaml +++ b/config/crd/bases/source.fluxcd.io_helmrepositories.yaml @@ -52,9 +52,18 @@ spec: interval: description: The interval at which to check for repository updates type: string + secretRef: + description: The name of the secret containing authentication credentials + for the Helm repository. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object url: - description: The repository address - minLength: 4 + description: The Helm repository URL, a valid URL contains at least + a protocol and host. type: string required: - interval diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index 3fadfaaf..776c1fc0 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -140,7 +140,7 @@ func (r *GitRepositoryReconciler) sync(repository sourcev1.GitRepository) (sourc auth, err := r.auth(repository, tmpSSH) if err != nil { err = fmt.Errorf("auth error: %w", err) - return sourcev1.GitRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err + return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err } // create tmp dir for the Git clone diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 83933ca1..258a170a 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/yaml" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/fluxcd/source-controller/internal/helm" ) // HelmChartReconciler reconciles a HelmChart object @@ -155,7 +156,30 @@ func (r *HelmChartReconciler) sync(repository sourcev1.HelmRepository, chart sou return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err } - res, err := c.Get(u.String(), getter.WithURL(repository.Spec.URL)) + var clientOpts []getter.Option + if repository.Spec.SecretRef != nil { + name := types.NamespacedName{ + Namespace: repository.GetNamespace(), + Name: repository.Spec.SecretRef.Name, + } + + var secret corev1.Secret + err := r.Client.Get(context.TODO(), name, &secret) + if err != nil { + err = fmt.Errorf("auth secret error: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err + } + + opts, cleanup, err := helm.ClientOptionsFromSecret(secret) + if err != nil { + err = fmt.Errorf("auth options error: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err + } + defer cleanup() + clientOpts = opts + } + + res, err := c.Get(u.String(), clientOpts...) if err != nil { return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err } diff --git a/controllers/helmrepository_controller.go b/controllers/helmrepository_controller.go index 8c0b3403..e298c0f9 100644 --- a/controllers/helmrepository_controller.go +++ b/controllers/helmrepository_controller.go @@ -30,11 +30,13 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/fluxcd/source-controller/internal/helm" ) // HelmRepositoryReconciler reconciles a HelmRepository object @@ -113,9 +115,30 @@ func (r *HelmRepositoryReconciler) sync(repository sourcev1.HelmRepository) (sou u.RawPath = path.Join(u.RawPath, "index.yaml") u.Path = path.Join(u.Path, "index.yaml") - indexURL := u.String() - // TODO(hidde): add authentication config - res, err := c.Get(indexURL, getter.WithURL(repository.Spec.URL)) + var clientOpts []getter.Option + if repository.Spec.SecretRef != nil { + name := types.NamespacedName{ + Namespace: repository.GetNamespace(), + Name: repository.Spec.SecretRef.Name, + } + + var secret corev1.Secret + err := r.Client.Get(context.TODO(), name, &secret) + if err != nil { + err = fmt.Errorf("auth secret error: %w", err) + return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err + } + + opts, cleanup, err := helm.ClientOptionsFromSecret(secret) + if err != nil { + err = fmt.Errorf("auth options error: %w", err) + return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err + } + defer cleanup() + clientOpts = opts + } + + res, err := c.Get(u.String(), clientOpts...) if err != nil { return sourcev1.HelmRepositoryNotReady(repository, sourcev1.IndexationFailedReason, err.Error()), err } @@ -162,14 +185,14 @@ func (r *HelmRepositoryReconciler) sync(repository sourcev1.HelmRepository) (sou } // update index symlink - indexUrl, err := r.Storage.Symlink(artifact, "index.yaml") + indexURL, err := r.Storage.Symlink(artifact, "index.yaml") if err != nil { err = fmt.Errorf("storage error: %w", err) return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err } message := fmt.Sprintf("Helm repository index is available at: %s", artifact.Path) - return sourcev1.HelmRepositoryReady(repository, artifact, indexUrl, sourcev1.IndexationSucceededReason, message), nil + return sourcev1.HelmRepositoryReady(repository, artifact, indexURL, sourcev1.IndexationSucceededReason, message), nil } func (r *HelmRepositoryReconciler) shouldResetStatus(repository sourcev1.HelmRepository) (bool, sourcev1.HelmRepositoryStatus) { diff --git a/internal/helm/getter.go b/internal/helm/getter.go new file mode 100644 index 00000000..5457acaf --- /dev/null +++ b/internal/helm/getter.go @@ -0,0 +1,73 @@ +package helm + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "helm.sh/helm/v3/pkg/getter" + corev1 "k8s.io/api/core/v1" +) + +func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, func(), error) { + var opts []getter.Option + basicAuth, err := BasicAuthFromSecret(secret) + if err != nil { + return opts, nil, err + } + opts = append(opts, basicAuth) + tlsClientConfig, cleanup, err := TLSClientConfigFromSecret(secret) + if err != nil { + return opts, nil, err + } + opts = append(opts, tlsClientConfig) + return opts, cleanup, nil +} + +func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) { + username, password := string(secret.Data["username"]), string(secret.Data["password"]) + switch { + case username == "" && password == "": + return nil, nil + case username == "" || password == "": + return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name) + } + return getter.WithBasicAuth(username, password), nil +} + +func TLSClientConfigFromSecret(secret corev1.Secret) (getter.Option, func(), error) { + certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"] + switch { + case len(certBytes)+len(keyBytes)+len(caBytes) == 0: + return nil, nil, nil + case len(certBytes) == 0 || len(keyBytes) == 0 || len(caBytes) == 0: + return nil, nil, fmt.Errorf("invalid '%s' secret data: required fields 'certFile', 'keyFile' and 'caFile'", + secret.Name) + } + + // create tmp dir for TLS files + tmp, err := ioutil.TempDir("", "helm-tls-"+secret.Name) + if err != nil { + return nil, nil, err + } + cleanup := func() { os.RemoveAll(tmp) } + + certFile := filepath.Join(tmp, "cert.crt") + if err := ioutil.WriteFile(certFile, certBytes, 0644); err != nil { + cleanup() + return nil, nil, err + } + keyFile := filepath.Join(tmp, "key.crt") + if err := ioutil.WriteFile(keyFile, keyBytes, 0644); err != nil { + cleanup() + return nil, nil, err + } + caFile := filepath.Join(tmp, "ca.pem") + if err := ioutil.WriteFile(caFile, caBytes, 0644); err != nil { + cleanup() + return nil, nil, err + } + + return getter.WithTLSClientConfig(certFile, keyFile, caFile), cleanup, nil +}