dockerconfigjson for OCI registry authentication

`loginOptionFromSecret` now derives username/password from a docker
config stored in Secrets of type "kubernetes.io/dockerconfigjson".

Signed-off-by: Max Jonas Werner <mail@makk.es>
This commit is contained in:
Max Jonas Werner 2022-05-20 20:59:08 +02:00
parent 1070d1287a
commit bb4d886ba2
No known key found for this signature in database
GPG Key ID: EB525E0F02B52140
4 changed files with 73 additions and 4 deletions

View File

@ -492,7 +492,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
}
// Build registryClient options from secret
logOpt, err := loginOptionFromSecret(*secret)
logOpt, err := loginOptionFromSecret(repo.Spec.URL, *secret)
if err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),

View File

@ -19,6 +19,7 @@ package controllers
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
@ -825,6 +826,35 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build)
cleanFunc func(g *WithT, build *chart.Build)
}{
{
name: "Reconciles chart build with docker repository credentials",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "auth",
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
".dockerconfigjson": []byte(`{"auths":{"` +
testRegistryserver.DockerRegistryHost + `":{"` +
`auth":"` + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword)) + `"}}}`),
},
},
beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) {
obj.Spec.Chart = metadata.Name
obj.Spec.Version = metadata.Version
repository.Spec.SecretRef = &meta.LocalObjectReference{Name: "auth"}
},
want: sreconcile.ResultSuccess,
assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) {
g.Expect(build.Name).To(Equal(metadata.Name))
g.Expect(build.Version).To(Equal(metadata.Version))
g.Expect(build.Path).ToNot(BeEmpty())
g.Expect(build.Path).To(BeARegularFile())
},
cleanFunc: func(g *WithT, build *chart.Build) {
g.Expect(os.Remove(build.Path)).To(Succeed())
},
},
{
name: "Reconciles chart build with repository credentials",
secret: &corev1.Secret{

View File

@ -17,12 +17,15 @@ limitations under the License.
package controllers
import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/docker/cli/cli/config"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
@ -273,7 +276,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
}
// Construct actual options
logOpt, err := loginOptionFromSecret(secret)
logOpt, err := loginOptionFromSecret(obj.Spec.URL, secret)
if err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@ -352,8 +355,30 @@ func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *s
return sreconcile.ResultSuccess, nil
}
func loginOptionFromSecret(secret corev1.Secret) (registry.LoginOption, error) {
username, password := string(secret.Data["username"]), string(secret.Data["password"])
// loginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
// empty, a nil LoginOption and a nil error will be returned.
func loginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.LoginOption, error) {
var username, password string
if secret.Type == corev1.SecretTypeDockerConfigJson {
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
if err != nil {
return nil, fmt.Errorf("unable to load Docker config: %w", err)
}
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL: %w", err)
}
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
if err != nil {
return nil, fmt.Errorf("unable to get authentication data from Secret: %w", err)
}
username = authConfig.Username
password = authConfig.Password
} else {
username, password = string(secret.Data["username"]), string(secret.Data["password"])
}
switch {
case username == "" && password == "":
return nil, nil

View File

@ -17,6 +17,7 @@ limitations under the License.
package controllers
import (
"encoding/base64"
"fmt"
"testing"
@ -36,6 +37,7 @@ import (
func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
tests := []struct {
name string
secretType corev1.SecretType
secretData map[string][]byte
}{
{
@ -49,6 +51,15 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
name: "no auth data",
secretData: nil,
},
{
name: "dockerconfigjson Secret",
secretType: corev1.SecretTypeDockerConfigJson,
secretData: map[string][]byte{
".dockerconfigjson": []byte(`{"auths":{"` +
testRegistryserver.DockerRegistryHost + `":{"` +
`auth":"` + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword)) + `"}}}`),
},
},
}
for _, tt := range tests {
@ -66,6 +77,9 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
},
Data: tt.secretData,
}
if tt.secretType != "" {
secret.Type = tt.secretType
}
g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed())