Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions cmd/deploy_eso.go
Original file line number Diff line number Diff line change
@@ -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")
}
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package flux_deployer
package component

import (
"context"
Expand All @@ -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
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package flux_deployer_test
package component

import (
"context"
Expand All @@ -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"
)
Expand All @@ -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)
}

Expand Down
7 changes: 7 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/deployment-repo/deploymentRepoManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/eso-deployer/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eso_deployer

const (
esoNamespace = "external-secrets"
esoImageRepoName = "external-secrets-image"
esoChartRepoName = "external-secrets-chart"
esoHelmReleaseName = "external-secrets-operator"
)
149 changes: 149 additions & 0 deletions internal/eso-deployer/deployer.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading