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:
parent
1070d1287a
commit
bb4d886ba2
|
@ -492,7 +492,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build registryClient options from secret
|
// Build registryClient options from secret
|
||||||
logOpt, err := loginOptionFromSecret(*secret)
|
logOpt, err := loginOptionFromSecret(repo.Spec.URL, *secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
||||||
|
|
|
@ -19,6 +19,7 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -825,6 +826,35 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
|
||||||
assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build)
|
assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build)
|
||||||
cleanFunc func(g *WithT, 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",
|
name: "Reconciles chart build with repository credentials",
|
||||||
secret: &corev1.Secret{
|
secret: &corev1.Secret{
|
||||||
|
|
|
@ -17,12 +17,15 @@ limitations under the License.
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||||
|
@ -273,7 +276,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct actual options
|
// Construct actual options
|
||||||
logOpt, err := loginOptionFromSecret(secret)
|
logOpt, err := loginOptionFromSecret(obj.Spec.URL, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
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
|
return sreconcile.ResultSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginOptionFromSecret(secret corev1.Secret) (registry.LoginOption, error) {
|
// loginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
|
||||||
username, password := string(secret.Data["username"]), string(secret.Data["password"])
|
// 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 {
|
switch {
|
||||||
case username == "" && password == "":
|
case username == "" && password == "":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ import (
|
||||||
func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
secretType corev1.SecretType
|
||||||
secretData map[string][]byte
|
secretData map[string][]byte
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -49,6 +51,15 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
||||||
name: "no auth data",
|
name: "no auth data",
|
||||||
secretData: nil,
|
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 {
|
for _, tt := range tests {
|
||||||
|
@ -66,6 +77,9 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
|
||||||
},
|
},
|
||||||
Data: tt.secretData,
|
Data: tt.secretData,
|
||||||
}
|
}
|
||||||
|
if tt.secretType != "" {
|
||||||
|
secret.Type = tt.secretType
|
||||||
|
}
|
||||||
|
|
||||||
g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed())
|
g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed())
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue