diff --git a/README.md b/README.md index 070ac00..f8e099b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,30 @@ Example: openmcp-bootstrapper deploy-flux ./examples/bootstrapper-config.yaml --kubeconfig ~/.kube/config --ocm-config ./examples/ocm-config.yaml --git-config ./examples/git-config.yaml ./examples/bootstrapper-config.yaml ``` +## `deploy-eso` +The `deploy-eso` command is used to deploy the `external-secrets-operator` to a Kubernetes cluster using the previously deployed `FluxCD` components. + +The `deploy-eso` command requires the following parameters: +* `bootstrapper-config`: Path to the bootstrapper configuration file, optionally containing the `ExternalSecrets` section. + * `ExternalSecrets` (optional): Configuration for the external-secrets-operator deployment containing `RepositorySecretRef` and `ImagePullSecrets` + +```yaml +externalSecrets: + repositorySecretRef: + name: repo-secret + imagePullSecrets: + - name: image-pull-secret +``` + +Optional parameters: +* `--kubeconfig`: Path to the kubeconfig file of the target Kubernetes cluster. If not set, the value of the `KUBECONFIG` environment variable will be used. If the `KUBECONFIG` environment variable is not set, the default kubeconfig file located at `$HOME/.kube/config` will be used. +* `--ocm-config`: Path to the OCM configuration file. + +Example: +```shell +openmcp-bootstrapper deploy-eso ./examples/bootstrapper-config.yaml --kubeconfig ~/.kube/config --ocm-config ./examples/ocm-config.yaml ./examples/bootstrapper-config.yaml +``` + ## `manage-deployment-repo` The `manageDeploymentRepo` command is used to template the openMCP git ops templates and apply them to the specified git repository and all kustomized resources to the specified Kubernetes cluster. diff --git a/cmd/deploy_eso.go b/cmd/deploy_eso.go new file mode 100644 index 0000000..131621c --- /dev/null +++ b/cmd/deploy_eso.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + cfg "github.com/openmcp-project/bootstrapper/internal/config" + + esodeployer "github.com/openmcp-project/bootstrapper/internal/eso-deployer" + logging "github.com/openmcp-project/bootstrapper/internal/log" + "github.com/openmcp-project/bootstrapper/internal/scheme" + "github.com/openmcp-project/bootstrapper/internal/util" +) + +// deployEsoCmd represents the deploy-eso command +var deployEsoCmd = &cobra.Command{ + Use: "deploy-eso", + Short: "Deploys External Secrets Operator controllers on the target cluster", + Long: "Deploys External Secrets Operator controllers on the target cluster", + Args: cobra.ExactArgs(1), + ArgAliases: []string{ + "configFile", + }, + Example: ` openmcp-bootstrapper deploy-eso "./config.yaml"`, + RunE: func(cmd *cobra.Command, args []string) error { + configFilePath := args[0] + config := &cfg.BootstrapperConfig{} + err := config.ReadFromFile(configFilePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + log := logging.GetLogger() + log.Info("Starting deployment of external secrets operator controllers.") + + targetCluster, err := util.GetCluster(cmd.Flag(FlagKubeConfig).Value.String(), "target-cluster", scheme.NewFluxScheme()) + if err != nil { + return fmt.Errorf("failed to get platform cluster: %w", err) + } + + if err = esodeployer.NewEsoDeployer(config, cmd.Flag(FlagOcmConfig).Value.String(), targetCluster, log).Deploy(cmd.Context()); err != nil { + return fmt.Errorf("failed deploying eso: %w", err) + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(deployEsoCmd) + deployEsoCmd.Flags().SortFlags = false + deployEsoCmd.Flags().String(FlagOcmConfig, "", "OCM configuration file") + deployEsoCmd.Flags().String(FlagKubeConfig, "", "Kubernetes configuration file") +} diff --git a/go.mod b/go.mod index f458c29..33c4056 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,10 @@ go 1.25.3 require ( github.com/Masterminds/sprig/v3 v3.3.0 + github.com/fluxcd/helm-controller/api v1.4.2 github.com/fluxcd/kustomize-controller/api v1.7.1 + github.com/fluxcd/pkg/apis/meta v1.22.0 + github.com/fluxcd/source-controller/api v1.7.2 github.com/go-git/go-billy/v5 v5.6.2 github.com/go-git/go-git/v5 v5.16.3 github.com/go-logr/logr v1.4.3 @@ -13,6 +16,7 @@ require ( github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 k8s.io/api v0.34.1 + k8s.io/apiextensions-apiserver v0.34.1 k8s.io/apimachinery v0.34.1 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.22.3 @@ -36,8 +40,8 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect - github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect @@ -94,7 +98,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/client-go v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect diff --git a/go.sum b/go.sum index 7ab824d..c8360d5 100644 --- a/go.sum +++ b/go.sum @@ -40,12 +40,18 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fluxcd/helm-controller/api v1.4.2 h1:2+D3kX3UJhYlr+1rzOkQ/YbIQ96R/olmdfjaYS+okNg= +github.com/fluxcd/helm-controller/api v1.4.2/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4= github.com/fluxcd/kustomize-controller/api v1.7.1 h1:wFevRoziJcQEcJtNL2/NrQfAA1lrrOnFSmFIZrBBfBc= github.com/fluxcd/kustomize-controller/api v1.7.1/go.mod h1:77OSly9kxQli7Nmcln0OqZDjVpRMc6eLKED0CiJHYz8= +github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA= +github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4= github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE= github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc= github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM= github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg= +github.com/fluxcd/source-controller/api v1.7.2 h1:/lg/xoyRjxwdhHKqjTxQS2o1cp+DMKJ8W4rpm+ZLemQ= +github.com/fluxcd/source-controller/api v1.7.2/go.mod h1:2JtCeUVpl0aqKImS19jUz9EEnMdzgqNWHkllrIhV004= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/internal/flux_deployer/component_manager.go b/internal/component/component_manager.go similarity index 83% rename from internal/flux_deployer/component_manager.go rename to internal/component/component_manager.go index 932b707..cb8d0dc 100644 --- a/internal/flux_deployer/component_manager.go +++ b/internal/component/component_manager.go @@ -1,4 +1,4 @@ -package flux_deployer +package component import ( "context" @@ -9,7 +9,7 @@ import ( // ComponentManager bundles the OCM logic required by the FluxDeployer. type ComponentManager interface { - GetComponentWithImageResources(ctx context.Context) (*ocm_cli.ComponentVersion, error) + GetComponentWithImageResources(ctx context.Context, resourceName string) (*ocm_cli.ComponentVersion, error) DownloadTemplatesResource(ctx context.Context, downloadDir string) error } @@ -35,8 +35,8 @@ func NewComponentManager(ctx context.Context, config *cfg.BootstrapperConfig, oc return m, nil } -func (m *ComponentManagerImpl) GetComponentWithImageResources(ctx context.Context) (*ocm_cli.ComponentVersion, error) { - return m.ComponentGetter.GetComponentVersionForResourceRecursive(ctx, m.ComponentGetter.RootComponentVersion(), FluxCDSourceControllerResourceName) +func (m *ComponentManagerImpl) GetComponentWithImageResources(ctx context.Context, resourceName string) (*ocm_cli.ComponentVersion, error) { + return m.ComponentGetter.GetComponentVersionForResourceRecursive(ctx, m.ComponentGetter.RootComponentVersion(), resourceName) } func (m *ComponentManagerImpl) DownloadTemplatesResource(ctx context.Context, downloadDir string) error { diff --git a/internal/flux_deployer/component_manager_test.go b/internal/component/component_manager_mock.go similarity index 75% rename from internal/flux_deployer/component_manager_test.go rename to internal/component/component_manager_mock.go index 222a4fc..6c226f1 100644 --- a/internal/flux_deployer/component_manager_test.go +++ b/internal/component/component_manager_mock.go @@ -1,4 +1,4 @@ -package flux_deployer_test +package component import ( "context" @@ -7,7 +7,6 @@ import ( "sigs.k8s.io/yaml" - "github.com/openmcp-project/bootstrapper/internal/flux_deployer" ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" "github.com/openmcp-project/bootstrapper/internal/util" ) @@ -18,13 +17,13 @@ type MockComponentManager struct { TemplatesPath string } -var _ flux_deployer.ComponentManager = (*MockComponentManager)(nil) +var _ ComponentManager = (*MockComponentManager)(nil) -func (m MockComponentManager) GetComponentWithImageResources(_ context.Context) (*ocmcli.ComponentVersion, error) { +func (m MockComponentManager) GetComponentWithImageResources(_ context.Context, _ string) (*ocmcli.ComponentVersion, error) { return loadComponentVersion(m.ComponentPath) } -func (m MockComponentManager) DownloadTemplatesResource(ctx context.Context, downloadDir string) error { +func (m MockComponentManager) DownloadTemplatesResource(_ context.Context, downloadDir string) error { return util.CopyDir(m.TemplatesPath, downloadDir) } diff --git a/internal/config/config.go b/internal/config/config.go index abf7560..8b9a65a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" + "github.com/fluxcd/pkg/apis/meta" "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/yaml" ) @@ -16,6 +17,7 @@ type BootstrapperConfig struct { OpenMCPOperator OpenMCPOperator `json:"openmcpOperator"` Environment string `json:"environment"` TemplateInput map[string]interface{} `json:"templateInput"` + ExternalSecrets ExternalSecrets `json:"externalSecrets"` } type Component struct { @@ -56,6 +58,11 @@ type Manifest struct { ManifestParsed map[string]interface{} } +type ExternalSecrets struct { + RepositorySecretRef *meta.LocalObjectReference `json:"repositorySecretRef"` + ImagePullSecrets []meta.LocalObjectReference `json:"imagePullSecrets"` +} + func (c *BootstrapperConfig) ReadFromFile(path string) error { data, err := os.ReadFile(path) if err != nil { diff --git a/internal/deployment-repo/deploymentRepoManager.go b/internal/deployment-repo/deploymentRepoManager.go index ad63bc7..83bc973 100644 --- a/internal/deployment-repo/deploymentRepoManager.go +++ b/internal/deployment-repo/deploymentRepoManager.go @@ -599,7 +599,7 @@ func (m *DeploymentRepoManager) RunKustomizeAndApply(ctx context.Context) error for _, manifest := range manifests { if manifest.GetKind() == "Kustomization" && strings.Contains(manifest.GetAPIVersion(), "kustomize.toolkit.fluxcd.io") { logger.Infof("Applying Kustomization manifest: %s/%s", manifest.GetNamespace(), manifest.GetName()) - err = util.ApplyUnstructuredObject(ctx, m.TargetCluster, manifest) + err = util.CreateOrUpdate(ctx, m.TargetCluster, manifest) if err != nil { return fmt.Errorf("failed to apply Kustomization manifest %s/%s: %w", manifest.GetNamespace(), manifest.GetName(), err) } diff --git a/internal/eso-deployer/constants.go b/internal/eso-deployer/constants.go new file mode 100644 index 0000000..0886805 --- /dev/null +++ b/internal/eso-deployer/constants.go @@ -0,0 +1,8 @@ +package eso_deployer + +const ( + esoNamespace = "external-secrets" + esoImageRepoName = "external-secrets-image" + esoChartRepoName = "external-secrets-chart" + esoHelmReleaseName = "external-secrets-operator" +) diff --git a/internal/eso-deployer/deployer.go b/internal/eso-deployer/deployer.go new file mode 100644 index 0000000..8e8eb21 --- /dev/null +++ b/internal/eso-deployer/deployer.go @@ -0,0 +1,149 @@ +package eso_deployer + +import ( + "context" + "encoding/json" + "fmt" + "time" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + "github.com/openmcp-project/controller-utils/pkg/clusters" + "github.com/sirupsen/logrus" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openmcp-project/bootstrapper/internal/component" + cfg "github.com/openmcp-project/bootstrapper/internal/config" + "github.com/openmcp-project/bootstrapper/internal/flux_deployer" + ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" + "github.com/openmcp-project/bootstrapper/internal/util" +) + +type EsoDeployer struct { + Config *cfg.BootstrapperConfig + + // OcmConfigPath is the path to the OCM configuration file + OcmConfigPath string + + platformCluster *clusters.Cluster + log *logrus.Logger +} + +func NewEsoDeployer(config *cfg.BootstrapperConfig, ocmConfigPath string, platformCluster *clusters.Cluster, log *logrus.Logger) *EsoDeployer { + return &EsoDeployer{ + Config: config, + OcmConfigPath: ocmConfigPath, + platformCluster: platformCluster, + log: log, + } +} + +func (d *EsoDeployer) Deploy(ctx context.Context) error { + componentManager, err := component.NewComponentManager(ctx, d.Config, d.OcmConfigPath) + if err != nil { + return fmt.Errorf("error creating component manager: %w", err) + } + + return d.DeployWithComponentManager(ctx, componentManager) +} + +func (d *EsoDeployer) DeployWithComponentManager(ctx context.Context, componentManager component.ComponentManager) error { + d.log.Info("Getting OCM component containing ESO resources.") + esoComponent, err := componentManager.GetComponentWithImageResources(ctx, "external-secrets-operator-image") + if err != nil { + return fmt.Errorf("failed to get external-secrets-operator-image component: %w", err) + } + + esoChartRes, err := esoComponent.GetResource("external-secrets-operator-chart") + if err != nil { + return fmt.Errorf("failed to get external-secrets-operator-chart resource: %w", err) + } + d.log.Info("Deploying OCIRepo for ESO chart.") + if err = d.deployRepo(ctx, esoChartRes, esoChartRepoName); err != nil { + return fmt.Errorf("failed to create helm chart repo: %w", err) + } + + esoImageRes, err := esoComponent.GetResource("external-secrets-operator-image") + if err != nil { + return fmt.Errorf("failed to get external-secrets-operator-image resource: %w", err) + } + d.log.Info("Deploying OCIRepo for ESO image.") + if err = d.deployRepo(ctx, esoImageRes, esoImageRepoName); err != nil { + return fmt.Errorf("failed to create helm image repo: %w", err) + } + + d.log.Info("Deploying HelmRelease for ESO.") + if err = d.deployHelmRelease(ctx, esoImageRes); err != nil { + return fmt.Errorf("failed to deploy helm release: %w", err) + } + + d.log.Info("Done.") + return nil +} + +func (d *EsoDeployer) deployHelmRelease(ctx context.Context, res *ocmcli.Resource) error { + name, tag, _, err := util.ParseImageVersionAndTag(*res.Access.ImageReference) + if err != nil { + return fmt.Errorf("failed to parse image resource: %w", err) + } + + values := map[string]any{ + "image": map[string]any{ + "repository": name, + "tag": tag, + }, + } + values["imagePullSecrets"] = d.Config.ExternalSecrets.ImagePullSecrets + + encoded, err := json.Marshal(values) + if err != nil { + return fmt.Errorf("failed to marshal ESO Helm values: %w", err) + } + jsonVals := &apiextensionsv1.JSON{Raw: encoded} + + helmRelease := &helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: esoHelmReleaseName, + Namespace: flux_deployer.FluxSystemNamespace, + }, + Spec: helmv2.HelmReleaseSpec{ + ChartRef: &helmv2.CrossNamespaceSourceReference{ + Kind: "OCIRepository", + Name: esoChartRepoName, + Namespace: flux_deployer.FluxSystemNamespace, + }, + ReleaseName: "eso", + TargetNamespace: esoNamespace, + Install: &helmv2.Install{ + CreateNamespace: true, + }, + Values: jsonVals, + }, + } + return util.CreateOrUpdate(ctx, d.platformCluster, helmRelease) +} + +func (d *EsoDeployer) deployRepo(ctx context.Context, res *ocmcli.Resource, repoName string) error { + name, tag, digest, err := util.ParseImageVersionAndTag(*res.Access.ImageReference) + if err != nil { + return err + } + + ociRepo := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: flux_deployer.FluxSystemNamespace, + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: fmt.Sprintf("oci://%s", name), + Reference: &sourcev1.OCIRepositoryRef{ + Tag: tag, + Digest: digest, + }, + Timeout: &metav1.Duration{Duration: 1 * time.Minute}, + SecretRef: d.Config.ExternalSecrets.RepositorySecretRef, + }, + } + return util.CreateOrUpdate(ctx, d.platformCluster, ociRepo) +} diff --git a/internal/eso-deployer/deployer_test.go b/internal/eso-deployer/deployer_test.go new file mode 100644 index 0000000..da287eb --- /dev/null +++ b/internal/eso-deployer/deployer_test.go @@ -0,0 +1,94 @@ +package eso_deployer + +import ( + "context" + "testing" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + "github.com/openmcp-project/controller-utils/pkg/clusters" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openmcp-project/bootstrapper/internal/component" + cfg "github.com/openmcp-project/bootstrapper/internal/config" + "github.com/openmcp-project/bootstrapper/internal/flux_deployer" + logging "github.com/openmcp-project/bootstrapper/internal/log" + "github.com/openmcp-project/bootstrapper/internal/scheme" +) + +func TestEsoDeployer_DeployWithComponentManager(t *testing.T) { + platformClient := fake.NewClientBuilder(). + WithScheme(scheme.NewFluxScheme()). + Build() + platformCluster := clusters.NewTestClusterFromClient("platform", platformClient) + namespace := flux_deployer.FluxSystemNamespace + + config := &cfg.BootstrapperConfig{ + Component: cfg.Component{}, + DeploymentRepository: cfg.DeploymentRepository{}, + Providers: cfg.Providers{}, + ImagePullSecrets: nil, + Environment: "test", + } + + d := NewEsoDeployer(config, "", platformCluster, logging.GetLogger()) + + // Initial + initial := &component.MockComponentManager{ + ComponentPath: "./testdata/component_1.yaml", + TemplatesPath: "", + } + + err := d.DeployWithComponentManager(context.Background(), initial) + assert.NoError(t, err, "Error deploying eso controllers") + + chartRepo := &sourcev1.OCIRepository{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoChartRepoName, Namespace: namespace}, chartRepo) + assert.NoError(t, err, "Error getting chart repo") + assert.Equal(t, namespace, chartRepo.Namespace, "Repo namespace does not match expected namespace") + assert.Equal(t, "external-secrets-v1.0.0", chartRepo.Spec.Reference.Tag, "Tag does not match expected tag") + + imgRepo := &sourcev1.OCIRepository{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoImageRepoName, Namespace: namespace}, imgRepo) + assert.NoError(t, err, "Error getting image repo") + assert.Equal(t, namespace, imgRepo.Namespace, "Repo namespace does not match expected namespace") + assert.Equal(t, "external-secrets-v1.0.0", imgRepo.Spec.Reference.Tag, "Tag does not match expected tag") + + helmRelease := &helmv2.HelmRelease{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoHelmReleaseName, Namespace: namespace}, helmRelease) + assert.NoError(t, err, "Error getting eso helm release") + assert.Equal(t, namespace, helmRelease.Namespace, "HelmRelease namespace does not match expected namespace") + assert.Equal(t, esoChartRepoName, helmRelease.Spec.ChartRef.Name, "ChartRef name does not match expected name") + assert.Equal(t, "eso", helmRelease.Spec.ReleaseName, "ReleaseName does not match expected name") + + // Updated + updated := &component.MockComponentManager{ + ComponentPath: "./testdata/component_2.yaml", + TemplatesPath: "", + } + + err = d.DeployWithComponentManager(context.Background(), updated) + assert.NoError(t, err, "Error deploying eso controllers") + + chartRepo = &sourcev1.OCIRepository{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoChartRepoName, Namespace: namespace}, chartRepo) + assert.NoError(t, err, "Error getting chart repo") + assert.Equal(t, namespace, chartRepo.Namespace, "Repo namespace does not match expected namespace") + assert.Equal(t, "external-secrets-v2.0.0", chartRepo.Spec.Reference.Tag, "Tag does not match expected tag") + + imgRepo = &sourcev1.OCIRepository{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoImageRepoName, Namespace: namespace}, imgRepo) + assert.NoError(t, err, "Error getting image repo") + assert.Equal(t, namespace, imgRepo.Namespace, "Repo namespace does not match expected namespace") + assert.Equal(t, "external-secrets-v2.0.0", imgRepo.Spec.Reference.Tag, "Tag does not match expected tag") + + helmRelease = &helmv2.HelmRelease{} + err = platformClient.Get(t.Context(), client.ObjectKey{Name: esoHelmReleaseName, Namespace: namespace}, helmRelease) + assert.NoError(t, err, "Error getting eso helm release") + assert.Equal(t, namespace, helmRelease.Namespace, "HelmRelease namespace does not match expected namespace") + assert.Equal(t, esoChartRepoName, helmRelease.Spec.ChartRef.Name, "ChartRef name does not match expected name") + assert.Equal(t, "eso", helmRelease.Spec.ReleaseName, "ReleaseName does not match expected name") + +} diff --git a/internal/eso-deployer/testdata/component_1.yaml b/internal/eso-deployer/testdata/component_1.yaml new file mode 100644 index 0000000..32a878e --- /dev/null +++ b/internal/eso-deployer/testdata/component_1.yaml @@ -0,0 +1,19 @@ +component: + name: github.com/openmcp-project/openmcp + version: v1.0.0 + provider: + name: openmcp-project + + resources: + - name: external-secrets-operator-chart + type: helmChart + access: + type: ociArtifact + imageReference: ghcr.io/charts/external-secrets-operator:external-secrets-v1.0.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + + - name: external-secrets-operator-image + version: v1.0.0 + type: ociImage + access: + type: ociArtifact + imageReference: ghcr.io/images/external-secrets-operator:external-secrets-v1.0.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 diff --git a/internal/eso-deployer/testdata/component_2.yaml b/internal/eso-deployer/testdata/component_2.yaml new file mode 100644 index 0000000..5ab7a7e --- /dev/null +++ b/internal/eso-deployer/testdata/component_2.yaml @@ -0,0 +1,19 @@ +component: + name: github.com/openmcp-project/openmcp + version: v2.0.0 + provider: + name: openmcp-project + + resources: + - name: external-secrets-operator-chart + type: helmChart + access: + type: ociArtifact + imageReference: ghcr.io/charts/external-secrets-operator:external-secrets-v2.0.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + + - name: external-secrets-operator-image + version: v2.0.0 + type: ociImage + access: + type: ociArtifact + imageReference: ghcr.io/images/external-secrets-operator:external-secrets-v2.0.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 diff --git a/internal/flux_deployer/deployer.go b/internal/flux_deployer/deployer.go index 0a9456a..cfaebff 100644 --- a/internal/flux_deployer/deployer.go +++ b/internal/flux_deployer/deployer.go @@ -12,6 +12,8 @@ import ( "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/kyaml/filesys" + "github.com/openmcp-project/bootstrapper/internal/component" + cfg "github.com/openmcp-project/bootstrapper/internal/config" ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" "github.com/openmcp-project/bootstrapper/internal/util" @@ -49,7 +51,7 @@ func NewFluxDeployer(config *cfg.BootstrapperConfig, gitConfigPath, ocmConfigPat } func (d *FluxDeployer) Deploy(ctx context.Context) (err error) { - componentManager, err := NewComponentManager(ctx, d.Config, d.OcmConfigPath) + componentManager, err := component.NewComponentManager(ctx, d.Config, d.OcmConfigPath) if err != nil { return fmt.Errorf("error creating component manager: %w", err) } @@ -57,7 +59,7 @@ func (d *FluxDeployer) Deploy(ctx context.Context) (err error) { return d.DeployWithComponentManager(ctx, componentManager) } -func (d *FluxDeployer) DeployWithComponentManager(ctx context.Context, componentManager ComponentManager) (err error) { +func (d *FluxDeployer) DeployWithComponentManager(ctx context.Context, componentManager component.ComponentManager) (err error) { d.log.Infof("Ensure namespace %s exists", d.fluxNamespace) namespaceMutator := resources.NewNamespaceMutator(d.fluxNamespace) if err := resources.CreateOrUpdateResource(ctx, d.platformCluster.Client(), namespaceMutator); err != nil { @@ -107,7 +109,7 @@ func (d *FluxDeployer) DeployWithComponentManager(ctx context.Context, component d.log.Tracef("Created repo directory: %s", d.repoDir) // Get component which contains the fluxcd images as resources - d.fluxcdCV, err = componentManager.GetComponentWithImageResources(ctx) + d.fluxcdCV, err = componentManager.GetComponentWithImageResources(ctx, FluxCDSourceControllerResourceName) if err != nil { return fmt.Errorf("failed to get fluxcd source controller component version: %w", err) } diff --git a/internal/flux_deployer/deployer_test.go b/internal/flux_deployer/deployer_test.go index 0b34384..4806a2a 100644 --- a/internal/flux_deployer/deployer_test.go +++ b/internal/flux_deployer/deployer_test.go @@ -9,6 +9,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/openmcp-project/bootstrapper/internal/component" + cfg "github.com/openmcp-project/bootstrapper/internal/config" "github.com/openmcp-project/bootstrapper/internal/flux_deployer" logging "github.com/openmcp-project/bootstrapper/internal/log" @@ -34,7 +36,7 @@ func TestDeployFluxController(t *testing.T) { d := flux_deployer.NewFluxDeployer(config, "", ocmcli.NoOcmConfig, platformCluster, logging.GetLogger()) // Initial deployment - componentManager1 := &MockComponentManager{ + componentManager1 := &component.MockComponentManager{ ComponentPath: "./testdata/01/component_1.yaml", TemplatesPath: "./testdata/01/fluxcd_resource", } @@ -48,7 +50,7 @@ func TestDeployFluxController(t *testing.T) { assert.Equal(t, "ghcr.io/fluxcd/source-controller:v1.0.0", deployment.Spec.Template.Spec.Containers[0].Image, "Deployment image does not match expected image") // Update deployment - componentManager2 := &MockComponentManager{ + componentManager2 := &component.MockComponentManager{ ComponentPath: "./testdata/01/component_2.yaml", TemplatesPath: "./testdata/01/fluxcd_resource", } diff --git a/internal/scheme/scheme.go b/internal/scheme/scheme.go new file mode 100644 index 0000000..f144738 --- /dev/null +++ b/internal/scheme/scheme.go @@ -0,0 +1,18 @@ +package scheme + +import ( + helmv2 "github.com/fluxcd/helm-controller/api/v2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +func NewFluxScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + + // Register Flux types + utilruntime.Must(helmv2.AddToScheme(scheme)) + utilruntime.Must(sourcev1.AddToScheme(scheme)) + + return scheme +} diff --git a/internal/util/kubernetes.go b/internal/util/kubernetes.go index ba4af7c..cdacbbe 100644 --- a/internal/util/kubernetes.go +++ b/internal/util/kubernetes.go @@ -9,7 +9,6 @@ import ( "path/filepath" "github.com/openmcp-project/controller-utils/pkg/clusters" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/yaml" @@ -67,7 +66,7 @@ func ApplyManifests(ctx context.Context, cluster *clusters.Cluster, manifests [] // Apply objects to the platform cluster for _, u := range unstructuredObjects { - if err = ApplyUnstructuredObject(ctx, cluster, u); err != nil { + if err = CreateOrUpdate(ctx, cluster, u); err != nil { return err } } @@ -95,33 +94,23 @@ func ParseManifests(reader io.Reader) ([]*unstructured.Unstructured, error) { return result, nil } -func ApplyUnstructuredObject(ctx context.Context, cluster *clusters.Cluster, u *unstructured.Unstructured) error { +func CreateOrUpdate(ctx context.Context, cluster *clusters.Cluster, obj client.Object) error { logger := log.GetLogger() - objectKey := client.ObjectKeyFromObject(u) - objectLogString := fmt.Sprintf("%s %s", u.GetObjectKind().GroupVersionKind().String(), objectKey.String()) - - existingObj := &unstructured.Unstructured{} - existingObj.SetGroupVersionKind(u.GroupVersionKind()) - getErr := cluster.Client().Get(ctx, objectKey, existingObj) - if getErr != nil { - if apierrors.IsNotFound(getErr) { - // create object + objectKey := client.ObjectKeyFromObject(obj) + objectLogString := fmt.Sprintf("%s %s", obj.GetObjectKind().GroupVersionKind().String(), objectKey.String()) + + existing := obj.DeepCopyObject().(client.Object) + err := cluster.Client().Get(ctx, client.ObjectKeyFromObject(obj), existing) + + if err != nil { + if client.IgnoreNotFound(err) == nil { logger.Tracef("Creating object %s", objectLogString) - createErr := cluster.Client().Create(ctx, u) - if createErr != nil { - return fmt.Errorf("error creating object %s: %w", objectLogString, createErr) - } - } else { - return fmt.Errorf("error reading object %s: %w", objectLogString, getErr) - } - } else { - // update object - logger.Tracef("Updating object %s", objectLogString) - u.SetResourceVersion(existingObj.GetResourceVersion()) - updateErr := cluster.Client().Update(ctx, u) - if updateErr != nil { - return fmt.Errorf("error updating object %s: %w", objectLogString, updateErr) + return cluster.Client().Create(ctx, obj) } + return err } - return nil + + logger.Tracef("Updating object %s", objectLogString) + obj.SetResourceVersion(existing.GetResourceVersion()) + return cluster.Client().Update(ctx, obj) }