Skip to content

Commit ea61082

Browse files
committed
Helm repository and chart HTTP and TLS auth
1 parent 580574d commit ea61082

File tree

7 files changed

+151
-12
lines changed

7 files changed

+151
-12
lines changed

api/v1alpha1/helmrepository_types.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ import (
2323

2424
// HelmRepositorySpec defines the desired state of HelmRepository
2525
type HelmRepositorySpec struct {
26-
// The repository address
27-
// +kubebuilder:validation:MinLength=4
26+
// The Helm repository URL, a valid URL contains at least a
27+
// protocol and host.
2828
// +required
2929
URL string `json:"url"`
3030

31+
// The name of the secret containing authentication credentials
32+
// for the Helm repository.
33+
// +optional
34+
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
35+
3136
// The interval at which to check for repository updates
3237
// +required
3338
Interval metav1.Duration `json:"interval"`

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/source.fluxcd.io_helmrepositories.yaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,18 @@ spec:
5252
interval:
5353
description: The interval at which to check for repository updates
5454
type: string
55+
secretRef:
56+
description: The name of the secret containing authentication credentials
57+
for the Helm repository.
58+
properties:
59+
name:
60+
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
61+
TODO: Add other useful fields. apiVersion, kind, uid?'
62+
type: string
63+
type: object
5564
url:
56-
description: The repository address
57-
minLength: 4
65+
description: The Helm repository URL, a valid URL contains at least
66+
a protocol and host.
5867
type: string
5968
required:
6069
- interval

controllers/gitrepository_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func (r *GitRepositoryReconciler) sync(repository sourcev1.GitRepository) (sourc
140140
auth, err := r.auth(repository, tmpSSH)
141141
if err != nil {
142142
err = fmt.Errorf("auth error: %w", err)
143-
return sourcev1.GitRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err
143+
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
144144
}
145145

146146
// create tmp dir for the Git clone

controllers/helmchart_controller.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"sigs.k8s.io/yaml"
3737

3838
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
39+
"github.com/fluxcd/source-controller/internal/helm"
3940
)
4041

4142
// HelmChartReconciler reconciles a HelmChart object
@@ -155,7 +156,30 @@ func (r *HelmChartReconciler) sync(repository sourcev1.HelmRepository, chart sou
155156
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
156157
}
157158

158-
res, err := c.Get(u.String(), getter.WithURL(repository.Spec.URL))
159+
var clientOpts []getter.Option
160+
if repository.Spec.SecretRef != nil {
161+
name := types.NamespacedName{
162+
Namespace: repository.GetNamespace(),
163+
Name: repository.Spec.SecretRef.Name,
164+
}
165+
166+
var secret corev1.Secret
167+
err := r.Client.Get(context.TODO(), name, &secret)
168+
if err != nil {
169+
err = fmt.Errorf("auth secret error: %w", err)
170+
return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err
171+
}
172+
173+
opts, cleanup, err := helm.ClientOptionsFromSecret(secret)
174+
if err != nil {
175+
err = fmt.Errorf("auth options error: %w", err)
176+
return sourcev1.HelmChartNotReady(chart, sourcev1.AuthenticationFailedReason, err.Error()), err
177+
}
178+
defer cleanup()
179+
clientOpts = opts
180+
}
181+
182+
res, err := c.Get(u.String(), clientOpts...)
159183
if err != nil {
160184
return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err
161185
}

controllers/helmrepository_controller.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ import (
3030
corev1 "k8s.io/api/core/v1"
3131
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/types"
3334
ctrl "sigs.k8s.io/controller-runtime"
3435
"sigs.k8s.io/controller-runtime/pkg/client"
3536
"sigs.k8s.io/yaml"
3637

3738
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
39+
"github.com/fluxcd/source-controller/internal/helm"
3840
)
3941

4042
// HelmRepositoryReconciler reconciles a HelmRepository object
@@ -113,9 +115,30 @@ func (r *HelmRepositoryReconciler) sync(repository sourcev1.HelmRepository) (sou
113115
u.RawPath = path.Join(u.RawPath, "index.yaml")
114116
u.Path = path.Join(u.Path, "index.yaml")
115117

116-
indexURL := u.String()
117-
// TODO(hidde): add authentication config
118-
res, err := c.Get(indexURL, getter.WithURL(repository.Spec.URL))
118+
var clientOpts []getter.Option
119+
if repository.Spec.SecretRef != nil {
120+
name := types.NamespacedName{
121+
Namespace: repository.GetNamespace(),
122+
Name: repository.Spec.SecretRef.Name,
123+
}
124+
125+
var secret corev1.Secret
126+
err := r.Client.Get(context.TODO(), name, &secret)
127+
if err != nil {
128+
err = fmt.Errorf("auth secret error: %w", err)
129+
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
130+
}
131+
132+
opts, cleanup, err := helm.ClientOptionsFromSecret(secret)
133+
if err != nil {
134+
err = fmt.Errorf("auth options error: %w", err)
135+
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
136+
}
137+
defer cleanup()
138+
clientOpts = opts
139+
}
140+
141+
res, err := c.Get(u.String(), clientOpts...)
119142
if err != nil {
120143
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.IndexationFailedReason, err.Error()), err
121144
}
@@ -162,14 +185,14 @@ func (r *HelmRepositoryReconciler) sync(repository sourcev1.HelmRepository) (sou
162185
}
163186

164187
// update index symlink
165-
indexUrl, err := r.Storage.Symlink(artifact, "index.yaml")
188+
indexURL, err := r.Storage.Symlink(artifact, "index.yaml")
166189
if err != nil {
167190
err = fmt.Errorf("storage error: %w", err)
168191
return sourcev1.HelmRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err
169192
}
170193

171194
message := fmt.Sprintf("Helm repository index is available at: %s", artifact.Path)
172-
return sourcev1.HelmRepositoryReady(repository, artifact, indexUrl, sourcev1.IndexationSucceededReason, message), nil
195+
return sourcev1.HelmRepositoryReady(repository, artifact, indexURL, sourcev1.IndexationSucceededReason, message), nil
173196
}
174197

175198
func (r *HelmRepositoryReconciler) shouldResetStatus(repository sourcev1.HelmRepository) (bool, sourcev1.HelmRepositoryStatus) {

internal/helm/getter.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package helm
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
9+
"helm.sh/helm/v3/pkg/getter"
10+
corev1 "k8s.io/api/core/v1"
11+
)
12+
13+
func ClientOptionsFromSecret(secret corev1.Secret) ([]getter.Option, func(), error) {
14+
var opts []getter.Option
15+
basicAuth, err := BasicAuthFromSecret(secret)
16+
if err != nil {
17+
return opts, nil, err
18+
}
19+
opts = append(opts, basicAuth)
20+
tlsClientConfig, cleanup, err := TLSClientConfigFromSecret(secret)
21+
if err != nil {
22+
return opts, nil, err
23+
}
24+
opts = append(opts, tlsClientConfig)
25+
return opts, cleanup, nil
26+
}
27+
28+
func BasicAuthFromSecret(secret corev1.Secret) (getter.Option, error) {
29+
username, password := string(secret.Data["username"]), string(secret.Data["password"])
30+
switch {
31+
case username == "" && password == "":
32+
return nil, nil
33+
case username == "" || password == "":
34+
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
35+
}
36+
return getter.WithBasicAuth(username, password), nil
37+
}
38+
39+
func TLSClientConfigFromSecret(secret corev1.Secret) (getter.Option, func(), error) {
40+
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
41+
switch {
42+
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
43+
return nil, nil, nil
44+
case len(certBytes) == 0 || len(keyBytes) == 0 || len(caBytes) == 0:
45+
return nil, nil, fmt.Errorf("invalid '%s' secret data: required fields 'certFile', 'keyFile' and 'caFile'",
46+
secret.Name)
47+
}
48+
49+
// create tmp dir for TLS files
50+
tmp, err := ioutil.TempDir("", "helm-tls-"+secret.Name)
51+
if err != nil {
52+
return nil, nil, err
53+
}
54+
cleanup := func() { os.RemoveAll(tmp) }
55+
56+
certFile := filepath.Join(tmp, "cert.crt")
57+
if err := ioutil.WriteFile(certFile, certBytes, 0644); err != nil {
58+
cleanup()
59+
return nil, nil, err
60+
}
61+
keyFile := filepath.Join(tmp, "key.crt")
62+
if err := ioutil.WriteFile(keyFile, keyBytes, 0644); err != nil {
63+
cleanup()
64+
return nil, nil, err
65+
}
66+
caFile := filepath.Join(tmp, "ca.pem")
67+
if err := ioutil.WriteFile(caFile, caBytes, 0644); err != nil {
68+
cleanup()
69+
return nil, nil, err
70+
}
71+
72+
return getter.WithTLSClientConfig(certFile, keyFile, caFile), cleanup, nil
73+
}

0 commit comments

Comments
 (0)