diff --git a/apis/compositiondefinitions/v1alpha1/types.go b/apis/compositiondefinitions/v1alpha1/types.go index 6455ed9..2cdedfd 100644 --- a/apis/compositiondefinitions/v1alpha1/types.go +++ b/apis/compositiondefinitions/v1alpha1/types.go @@ -14,6 +14,11 @@ type CompositionDefinitionList struct { Items []CompositionDefinition `json:"items"` } +type Credentials struct { + Username string `json:"username"` + PasswordRef rtv1.SecretKeySelector `json:"passwordRef"` +} + // +kubebuilder:validation:XValidation:rule="!has(oldSelf.version) || has(self.version)", message="Version is required once set" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.repo) || has(self.repo)", message="Repo is required once set" type ChartInfo struct { @@ -30,6 +35,10 @@ type ChartInfo struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Repo is immutable" // +kubebuilder:validation:MaxLength=256 Repo string `json:"repo,omitempty"` + + // Credentials: credentials for private repos + // +optional + Credentials *Credentials `json:"credentials,omitempty"` } type CompositionDefinitionSpec struct { diff --git a/apis/compositiondefinitions/v1alpha1/zz_generated.deepcopy.go b/apis/compositiondefinitions/v1alpha1/zz_generated.deepcopy.go index cae5929..f55aff4 100644 --- a/apis/compositiondefinitions/v1alpha1/zz_generated.deepcopy.go +++ b/apis/compositiondefinitions/v1alpha1/zz_generated.deepcopy.go @@ -27,6 +27,11 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChartInfo) DeepCopyInto(out *ChartInfo) { *out = *in + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = new(Credentials) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChartInfo. @@ -105,7 +110,7 @@ func (in *CompositionDefinitionSpec) DeepCopyInto(out *CompositionDefinitionSpec if in.Chart != nil { in, out := &in.Chart, &out.Chart *out = new(ChartInfo) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -139,3 +144,19 @@ func (in *CompositionDefinitionStatus) DeepCopy() *CompositionDefinitionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Credentials) DeepCopyInto(out *Credentials) { + *out = *in + out.PasswordRef = in.PasswordRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Credentials. +func (in *Credentials) DeepCopy() *Credentials { + if in == nil { + return nil + } + out := new(Credentials) + in.DeepCopyInto(out) + return out +} diff --git a/crds/core.krateo.io_compositiondefinitions.yaml b/crds/core.krateo.io_compositiondefinitions.yaml index ff24406..c5ec4f1 100644 --- a/crds/core.krateo.io_compositiondefinitions.yaml +++ b/crds/core.krateo.io_compositiondefinitions.yaml @@ -64,6 +64,33 @@ spec: properties: chart: properties: + credentials: + description: 'Credentials: credentials for private repos' + properties: + passwordRef: + description: A SecretKeySelector is a reference to a secret + key in an arbitrary namespace. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object. + type: string + required: + - key + - name + - namespace + type: object + username: + type: string + required: + - passwordRef + - username + type: object repo: description: 'Repo: helm repo name (for helm repo urls only)' maxLength: 256 diff --git a/internal/controllers/compositiondefinitions/compositiondefinitions.go b/internal/controllers/compositiondefinitions/compositiondefinitions.go index 7a59041..f5ccf1e 100644 --- a/internal/controllers/compositiondefinitions/compositiondefinitions.go +++ b/internal/controllers/compositiondefinitions/compositiondefinitions.go @@ -117,7 +117,7 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (reconciler return reconciler.ExternalObservation{}, errors.New(errNotCR) } - pkg, err := chartfs.ForSpec(cr.Spec.Chart) + pkg, err := chartfs.ForSpec(ctx, e.kube, cr.Spec.Chart) if err != nil { return reconciler.ExternalObservation{}, err } @@ -221,7 +221,7 @@ func (e *external) Create(ctx context.Context, mg resource.Managed) error { return nil } - pkg, dir, err := generator.ChartInfoFromSpec(ctx, cr.Spec.Chart) + pkg, dir, err := generator.ChartInfoFromSpec(ctx, e.kube, cr.Spec.Chart) if err != nil { return err } @@ -299,7 +299,7 @@ func (e *external) Create(ctx context.Context, mg resource.Managed) error { opts.Log = e.log.Debug } - err, rbacErr := tools.Deploy(ctx, opts) + err, rbacErr := tools.Deploy(ctx, e.kube, opts) if rbacErr != nil { strErr := rbacErr.Error() cr.Status.Error = &strErr @@ -340,7 +340,7 @@ func (e *external) Delete(ctx context.Context, mg resource.Managed) error { return nil } - pkg, dir, err := generator.ChartInfoFromSpec(ctx, cr.Spec.Chart) + pkg, dir, err := generator.ChartInfoFromSpec(ctx, e.kube, cr.Spec.Chart) if err != nil { return err } diff --git a/internal/controllers/compositiondefinitions/generator/chart.go b/internal/controllers/compositiondefinitions/generator/chart.go index 3b56116..ee36986 100644 --- a/internal/controllers/compositiondefinitions/generator/chart.go +++ b/internal/controllers/compositiondefinitions/generator/chart.go @@ -13,8 +13,10 @@ import ( "github.com/krateoplatformops/core-provider/internal/helm/getter" "github.com/krateoplatformops/core-provider/internal/strutil" "github.com/krateoplatformops/core-provider/internal/tgzfs" + "github.com/krateoplatformops/core-provider/internal/tools/resolvers" "github.com/krateoplatformops/crdgen" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -22,16 +24,26 @@ const ( defaultGroup = "composition.krateo.io" ) -func ChartInfoFromSpec(ctx context.Context, nfo *v1alpha1.ChartInfo) (pkg fs.FS, rootDir string, err error) { +func ChartInfoFromSpec(ctx context.Context, kube client.Client, nfo *v1alpha1.ChartInfo) (pkg fs.FS, rootDir string, err error) { if nfo == nil { return nil, "", fmt.Errorf("chart infos cannot be nil") } - dat, _, err := getter.Get(getter.GetOptions{ + opts := getter.GetOptions{ URI: nfo.Url, Version: nfo.Version, - Repo: nfo.Repo, - }) + Repo: nfo.Repo} + if nfo.Credentials != nil { + secret, err := resolvers.GetSecret(ctx, kube, nfo.Credentials.PasswordRef) + if err != nil { + return nil, "", fmt.Errorf("failed to get secret: %w", err) + } + opts.Username = nfo.Credentials.Username + opts.Password = secret + opts.PassCredentialsAll = true + } + + dat, _, err := getter.Get(opts) if err != nil { return nil, "", err } diff --git a/internal/controllers/compositiondefinitions/generator/chart_test.go b/internal/controllers/compositiondefinitions/generator/chart_test.go index 63da936..0533eb9 100644 --- a/internal/controllers/compositiondefinitions/generator/chart_test.go +++ b/internal/controllers/compositiondefinitions/generator/chart_test.go @@ -6,11 +6,15 @@ package generator_test import ( "context" "fmt" + "os" + "path" "testing" "github.com/krateoplatformops/core-provider/apis/compositiondefinitions/v1alpha1" "github.com/krateoplatformops/core-provider/internal/controllers/compositiondefinitions/generator" "github.com/krateoplatformops/crdgen" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" ) func TestJsonSchemaFromOCI(t *testing.T) { @@ -18,8 +22,22 @@ func TestJsonSchemaFromOCI(t *testing.T) { Url: "oci://registry-1.docker.io/bitnamicharts/redis", Version: "18.0.1", } + home, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + + cfg, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) + if err != nil { + t.Fatal(err) + } + + cli, err := client.New(cfg, client.Options{}) + if err != nil { + t.Fatal(err) + } - pkg, rootdir, err := generator.ChartInfoFromSpec(context.TODO(), &nfo) + pkg, rootdir, err := generator.ChartInfoFromSpec(context.TODO(), cli, &nfo) if err != nil { t.Fatal(err) } @@ -40,7 +58,7 @@ func TestCRDGenFromOCI(t *testing.T) { Version: "18.0.1", } - pkg, dir, err := generator.ChartInfoFromSpec(context.TODO(), &nfo) + pkg, dir, err := generator.ChartInfoFromSpec(context.TODO(), nil, &nfo) if err != nil { t.Fatal(err) } diff --git a/internal/helm/getter/oci.go b/internal/helm/getter/oci.go index 6d2b165..bad81af 100644 --- a/internal/helm/getter/oci.go +++ b/internal/helm/getter/oci.go @@ -67,6 +67,17 @@ func (g *ociGetter) Get(opts GetOptions) ([]byte, string, error) { if err != nil { return nil, "", err } + if opts.PassCredentialsAll { + host := strings.Split(ref, "/")[0] + loginopts := []registry.LoginOption{ + registry.LoginOptBasicAuth(opts.Username, opts.Password), + } + err := g.client.Login(host, loginopts...) + if err != nil { + return nil, "", fmt.Errorf("failed to login: %w", err) + } + defer g.client.Logout(host) + } pullOpts := []registry.PullOption{ registry.PullOptWithChart(true), diff --git a/internal/helm/getter/tgz_test.go b/internal/helm/getter/tgz_test.go index 626d7b5..bcf225a 100644 --- a/internal/helm/getter/tgz_test.go +++ b/internal/helm/getter/tgz_test.go @@ -6,7 +6,7 @@ import ( func TestTGZ(t *testing.T) { const ( - uri = "https://github.com/krateoplatformops/krateo-v2-template-fireworksapp/releases/download/0.0.1/fireworks-app-0.1.0.tgz" + uri = "https://github.com/krateoplatformops/krateo-v2-template-fireworksapp/releases/download/0.1.0/fireworks-app-0.1.0.tgz" ) if !isTGZ(uri) { diff --git a/internal/tools/chartfs/chartfs.go b/internal/tools/chartfs/chartfs.go index 1c0d440..7408ed1 100644 --- a/internal/tools/chartfs/chartfs.go +++ b/internal/tools/chartfs/chartfs.go @@ -2,6 +2,7 @@ package chartfs import ( "bytes" + "context" "fmt" "io" "io/fs" @@ -9,6 +10,8 @@ import ( "github.com/krateoplatformops/core-provider/apis/compositiondefinitions/v1alpha1" "github.com/krateoplatformops/core-provider/internal/helm/getter" "github.com/krateoplatformops/core-provider/internal/tgzfs" + "github.com/krateoplatformops/core-provider/internal/tools/resolvers" + "sigs.k8s.io/controller-runtime/pkg/client" ) func FromReader(in io.Reader, pkgurl string) (*ChartFS, error) { @@ -40,16 +43,26 @@ func FromReader(in io.Reader, pkgurl string) (*ChartFS, error) { }, nil } -func ForSpec(nfo *v1alpha1.ChartInfo) (*ChartFS, error) { +func ForSpec(ctx context.Context, kube client.Client, nfo *v1alpha1.ChartInfo) (*ChartFS, error) { if nfo == nil { return nil, fmt.Errorf("chart infos cannot be nil") } - dat, url, err := getter.Get(getter.GetOptions{ + opts := getter.GetOptions{ URI: nfo.Url, Version: nfo.Version, - Repo: nfo.Repo, - }) + Repo: nfo.Repo} + if nfo.Credentials != nil { + secret, err := resolvers.GetSecret(ctx, kube, nfo.Credentials.PasswordRef) + if err != nil { + return nil, fmt.Errorf("failed to get secret: %w", err) + } + + opts.Username = nfo.Credentials.Username + opts.Password = secret + opts.PassCredentialsAll = true + } + dat, url, err := getter.Get(opts) if err != nil { return nil, err } diff --git a/internal/tools/deploy.go b/internal/tools/deploy.go index 3c21f83..bffeead 100644 --- a/internal/tools/deploy.go +++ b/internal/tools/deploy.go @@ -96,8 +96,8 @@ type DeployOptions struct { Log func(msg string, keysAndValues ...any) } -func Deploy(ctx context.Context, opts DeployOptions) (err error, rbacErr error) { - pkg, err := chartfs.ForSpec(opts.Spec) +func Deploy(ctx context.Context, kube client.Client, opts DeployOptions) (err error, rbacErr error) { + pkg, err := chartfs.ForSpec(ctx, kube, opts.Spec) if err != nil { return err, nil } diff --git a/internal/tools/deploy_test.go b/internal/tools/deploy_test.go index d1acbcf..ae3fb25 100644 --- a/internal/tools/deploy_test.go +++ b/internal/tools/deploy_test.go @@ -6,12 +6,15 @@ package tools_test import ( "context" "os" + "path" "testing" "github.com/krateoplatformops/core-provider/apis/compositiondefinitions/v1alpha1" "github.com/krateoplatformops/core-provider/internal/tools" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" ) func TestDeploy(t *testing.T) { @@ -24,8 +27,17 @@ func TestDeploy(t *testing.T) { if err != nil { t.Fatal(err) } + home, err := os.UserHomeDir() + cfg, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) + if err != nil { + t.Fatal(err) + } + cli, err := client.New(cfg, client.Options{}) + if err != nil { + t.Fatal(err) + } - err, _ = tools.Deploy(context.TODO(), tools.DeployOptions{ + err, _ = tools.Deploy(context.TODO(), cli, tools.DeployOptions{ KubeClient: kube, Spec: nfo, NamespacedName: types.NamespacedName{ diff --git a/internal/tools/resolvers/secrets.go b/internal/tools/resolvers/secrets.go new file mode 100644 index 0000000..2fac758 --- /dev/null +++ b/internal/tools/resolvers/secrets.go @@ -0,0 +1,22 @@ +package resolvers + +import ( + "context" + + rtv1 "github.com/krateoplatformops/provider-runtime/apis/common/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetSecret(ctx context.Context, kube client.Client, secretKeySelector rtv1.SecretKeySelector) (string, error) { + secret := &corev1.Secret{} + if err := kube.Get(ctx, types.NamespacedName{ + Name: secretKeySelector.Name, + Namespace: secretKeySelector.Namespace, + }, secret); err != nil { + return "", err + } + + return string(secret.Data[secretKeySelector.Key]), nil +} diff --git a/internal/tools/role_test.go b/internal/tools/role_test.go index a9268b1..29c8c94 100644 --- a/internal/tools/role_test.go +++ b/internal/tools/role_test.go @@ -6,6 +6,8 @@ package tools_test import ( "context" "fmt" + "os" + "path" "testing" "github.com/krateoplatformops/core-provider/apis/compositiondefinitions/v1alpha1" @@ -13,6 +15,8 @@ import ( "github.com/krateoplatformops/core-provider/internal/tools/chartfs" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -69,7 +73,16 @@ func TestCreateRoleFromTGZ(t *testing.T) { } func createRoleFromURL() (rbacv1.Role, error) { - pkg, err := chartfs.ForSpec(&v1alpha1.ChartInfo{ + home, err := os.UserHomeDir() + cfg, err := clientcmd.BuildConfigFromFlags("", path.Join(home, ".kube/config")) + if err != nil { + return rbacv1.Role{}, err + } + cli, err := client.New(cfg, client.Options{}) + if err != nil { + return rbacv1.Role{}, err + } + pkg, err := chartfs.ForSpec(context.TODO(), cli, &v1alpha1.ChartInfo{ Url: testChartUrl, }) if err != nil { diff --git a/testdata/compositiondefinition-private-oci.yaml b/testdata/compositiondefinition-private-oci.yaml new file mode 100644 index 0000000..87a16c3 --- /dev/null +++ b/testdata/compositiondefinition-private-oci.yaml @@ -0,0 +1,18 @@ +apiVersion: core.krateo.io/v1alpha1 +kind: CompositionDefinition +metadata: + annotations: + "krateo.io/connector-verbose": "true" + name: fireworks-private + namespace: krateo-system +spec: + chart: + url: oci://registry-1.docker.io/matteogastaldello/fireworks-app + version: "0.1.0" + credentials: + username: matteogastaldello + passwordRef: # reference to a secret + key: token + name: docker-hub + namespace: default +