Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions api/v1alpha1/helmrepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
7 changes: 6 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions config/crd/bases/source.fluxcd.io_helmrepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion controllers/gitrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion controllers/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
33 changes: 28 additions & 5 deletions controllers/helmrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down
73 changes: 73 additions & 0 deletions internal/helm/getter.go
Original file line number Diff line number Diff line change
@@ -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
}