Merge pull request #9 from fluxcd/helm-auth

Helm repository and chart HTTP and TLS auth
This commit is contained in:
Hidde Beydals 2020-04-13 11:58:04 +02:00 committed by GitHub
commit b7897b7144
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 12 deletions

View File

@ -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"`

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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) {

73
internal/helm/getter.go Normal file
View File

@ -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
}