Skip to content

Commit ea31499

Browse files
rdksapreshnm
andauthored
feat: add deploy-eso (#91)
* feat: add deploy-eso command, read eso resources from ocm root component, deploy OCIRepositories for image and chart, deploy HelmRelease, add tests. * feat: add ExternalSecrets structure to bootstrapper config to allow specification of repository and image pull secrets * feat: use pointer ref * feat: add deploy-eso part to readme * fix: revert go version change * Update cmd/deploy_eso.go Co-authored-by: René Schünemann <[email protected]> * fix: re-add indentation --------- Signed-off-by: Radek Schekalla (SAP) <[email protected]> Co-authored-by: René Schünemann <[email protected]>
1 parent 85714fb commit ea31499

File tree

17 files changed

+437
-44
lines changed

17 files changed

+437
-44
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ Example:
7575
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
7676
```
7777

78+
## `deploy-eso`
79+
The `deploy-eso` command is used to deploy the `external-secrets-operator` to a Kubernetes cluster using the previously deployed `FluxCD` components.
80+
81+
The `deploy-eso` command requires the following parameters:
82+
* `bootstrapper-config`: Path to the bootstrapper configuration file, optionally containing the `ExternalSecrets` section.
83+
* `ExternalSecrets` (optional): Configuration for the external-secrets-operator deployment containing `RepositorySecretRef` and `ImagePullSecrets`
84+
85+
```yaml
86+
externalSecrets:
87+
repositorySecretRef:
88+
name: repo-secret
89+
imagePullSecrets:
90+
- name: image-pull-secret
91+
```
92+
93+
Optional parameters:
94+
* `--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.
95+
* `--ocm-config`: Path to the OCM configuration file.
96+
97+
Example:
98+
```shell
99+
openmcp-bootstrapper deploy-eso ./examples/bootstrapper-config.yaml --kubeconfig ~/.kube/config --ocm-config ./examples/ocm-config.yaml ./examples/bootstrapper-config.yaml
100+
```
101+
78102
## `manage-deployment-repo`
79103

80104
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.

cmd/deploy_eso.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
cfg "github.com/openmcp-project/bootstrapper/internal/config"
9+
10+
esodeployer "github.com/openmcp-project/bootstrapper/internal/eso-deployer"
11+
logging "github.com/openmcp-project/bootstrapper/internal/log"
12+
"github.com/openmcp-project/bootstrapper/internal/scheme"
13+
"github.com/openmcp-project/bootstrapper/internal/util"
14+
)
15+
16+
// deployEsoCmd represents the deploy-eso command
17+
var deployEsoCmd = &cobra.Command{
18+
Use: "deploy-eso",
19+
Short: "Deploys External Secrets Operator controllers on the target cluster",
20+
Long: "Deploys External Secrets Operator controllers on the target cluster",
21+
Args: cobra.ExactArgs(1),
22+
ArgAliases: []string{
23+
"configFile",
24+
},
25+
Example: ` openmcp-bootstrapper deploy-eso "./config.yaml"`,
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
configFilePath := args[0]
28+
config := &cfg.BootstrapperConfig{}
29+
err := config.ReadFromFile(configFilePath)
30+
if err != nil {
31+
return fmt.Errorf("failed to read config file: %w", err)
32+
}
33+
log := logging.GetLogger()
34+
log.Info("Starting deployment of external secrets operator controllers.")
35+
36+
targetCluster, err := util.GetCluster(cmd.Flag(FlagKubeConfig).Value.String(), "target-cluster", scheme.NewFluxScheme())
37+
if err != nil {
38+
return fmt.Errorf("failed to get platform cluster: %w", err)
39+
}
40+
41+
if err = esodeployer.NewEsoDeployer(config, cmd.Flag(FlagOcmConfig).Value.String(), targetCluster, log).Deploy(cmd.Context()); err != nil {
42+
return fmt.Errorf("failed deploying eso: %w", err)
43+
}
44+
45+
return nil
46+
},
47+
}
48+
49+
func init() {
50+
RootCmd.AddCommand(deployEsoCmd)
51+
deployEsoCmd.Flags().SortFlags = false
52+
deployEsoCmd.Flags().String(FlagOcmConfig, "", "OCM configuration file")
53+
deployEsoCmd.Flags().String(FlagKubeConfig, "", "Kubernetes configuration file")
54+
}

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ go 1.25.3
44

55
require (
66
github.com/Masterminds/sprig/v3 v3.3.0
7+
github.com/fluxcd/helm-controller/api v1.4.2
78
github.com/fluxcd/kustomize-controller/api v1.7.1
9+
github.com/fluxcd/pkg/apis/meta v1.22.0
10+
github.com/fluxcd/source-controller/api v1.7.2
811
github.com/go-git/go-billy/v5 v5.6.2
912
github.com/go-git/go-git/v5 v5.16.3
1013
github.com/go-logr/logr v1.4.3
@@ -13,6 +16,7 @@ require (
1316
github.com/spf13/cobra v1.10.1
1417
github.com/stretchr/testify v1.11.1
1518
k8s.io/api v0.34.1
19+
k8s.io/apiextensions-apiserver v0.34.1
1620
k8s.io/apimachinery v0.34.1
1721
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
1822
sigs.k8s.io/controller-runtime v0.22.3
@@ -36,8 +40,8 @@ require (
3640
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
3741
github.com/emirpasic/gods v1.18.1 // indirect
3842
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
43+
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
3944
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
40-
github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect
4145
github.com/fsnotify/fsnotify v1.9.0 // indirect
4246
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
4347
github.com/go-errors/errors v1.4.2 // indirect
@@ -94,7 +98,6 @@ require (
9498
gopkg.in/inf.v0 v0.9.1 // indirect
9599
gopkg.in/warnings.v0 v0.1.2 // indirect
96100
gopkg.in/yaml.v3 v3.0.1 // indirect
97-
k8s.io/apiextensions-apiserver v0.34.1 // indirect
98101
k8s.io/client-go v0.34.1 // indirect
99102
k8s.io/klog/v2 v2.130.1 // indirect
100103
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,18 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH
4040
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
4141
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
4242
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
43+
github.com/fluxcd/helm-controller/api v1.4.2 h1:2+D3kX3UJhYlr+1rzOkQ/YbIQ96R/olmdfjaYS+okNg=
44+
github.com/fluxcd/helm-controller/api v1.4.2/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4=
4345
github.com/fluxcd/kustomize-controller/api v1.7.1 h1:wFevRoziJcQEcJtNL2/NrQfAA1lrrOnFSmFIZrBBfBc=
4446
github.com/fluxcd/kustomize-controller/api v1.7.1/go.mod h1:77OSly9kxQli7Nmcln0OqZDjVpRMc6eLKED0CiJHYz8=
47+
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
48+
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
4549
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
4650
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
4751
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
4852
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
53+
github.com/fluxcd/source-controller/api v1.7.2 h1:/lg/xoyRjxwdhHKqjTxQS2o1cp+DMKJ8W4rpm+ZLemQ=
54+
github.com/fluxcd/source-controller/api v1.7.2/go.mod h1:2JtCeUVpl0aqKImS19jUz9EEnMdzgqNWHkllrIhV004=
4955
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
5056
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
5157
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=

internal/flux_deployer/component_manager.go renamed to internal/component/component_manager.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package flux_deployer
1+
package component
22

33
import (
44
"context"
@@ -9,7 +9,7 @@ import (
99

1010
// ComponentManager bundles the OCM logic required by the FluxDeployer.
1111
type ComponentManager interface {
12-
GetComponentWithImageResources(ctx context.Context) (*ocm_cli.ComponentVersion, error)
12+
GetComponentWithImageResources(ctx context.Context, resourceName string) (*ocm_cli.ComponentVersion, error)
1313
DownloadTemplatesResource(ctx context.Context, downloadDir string) error
1414
}
1515

@@ -35,8 +35,8 @@ func NewComponentManager(ctx context.Context, config *cfg.BootstrapperConfig, oc
3535
return m, nil
3636
}
3737

38-
func (m *ComponentManagerImpl) GetComponentWithImageResources(ctx context.Context) (*ocm_cli.ComponentVersion, error) {
39-
return m.ComponentGetter.GetComponentVersionForResourceRecursive(ctx, m.ComponentGetter.RootComponentVersion(), FluxCDSourceControllerResourceName)
38+
func (m *ComponentManagerImpl) GetComponentWithImageResources(ctx context.Context, resourceName string) (*ocm_cli.ComponentVersion, error) {
39+
return m.ComponentGetter.GetComponentVersionForResourceRecursive(ctx, m.ComponentGetter.RootComponentVersion(), resourceName)
4040
}
4141

4242
func (m *ComponentManagerImpl) DownloadTemplatesResource(ctx context.Context, downloadDir string) error {

internal/flux_deployer/component_manager_test.go renamed to internal/component/component_manager_mock.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package flux_deployer_test
1+
package component
22

33
import (
44
"context"
@@ -7,7 +7,6 @@ import (
77

88
"sigs.k8s.io/yaml"
99

10-
"github.com/openmcp-project/bootstrapper/internal/flux_deployer"
1110
ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli"
1211
"github.com/openmcp-project/bootstrapper/internal/util"
1312
)
@@ -18,13 +17,13 @@ type MockComponentManager struct {
1817
TemplatesPath string
1918
}
2019

21-
var _ flux_deployer.ComponentManager = (*MockComponentManager)(nil)
20+
var _ ComponentManager = (*MockComponentManager)(nil)
2221

23-
func (m MockComponentManager) GetComponentWithImageResources(_ context.Context) (*ocmcli.ComponentVersion, error) {
22+
func (m MockComponentManager) GetComponentWithImageResources(_ context.Context, _ string) (*ocmcli.ComponentVersion, error) {
2423
return loadComponentVersion(m.ComponentPath)
2524
}
2625

27-
func (m MockComponentManager) DownloadTemplatesResource(ctx context.Context, downloadDir string) error {
26+
func (m MockComponentManager) DownloadTemplatesResource(_ context.Context, downloadDir string) error {
2827
return util.CopyDir(m.TemplatesPath, downloadDir)
2928
}
3029

internal/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"os"
66

7+
"github.com/fluxcd/pkg/apis/meta"
78
"k8s.io/apimachinery/pkg/util/validation/field"
89
"sigs.k8s.io/yaml"
910
)
@@ -16,6 +17,7 @@ type BootstrapperConfig struct {
1617
OpenMCPOperator OpenMCPOperator `json:"openmcpOperator"`
1718
Environment string `json:"environment"`
1819
TemplateInput map[string]interface{} `json:"templateInput"`
20+
ExternalSecrets ExternalSecrets `json:"externalSecrets"`
1921
}
2022

2123
type Component struct {
@@ -56,6 +58,11 @@ type Manifest struct {
5658
ManifestParsed map[string]interface{}
5759
}
5860

61+
type ExternalSecrets struct {
62+
RepositorySecretRef *meta.LocalObjectReference `json:"repositorySecretRef"`
63+
ImagePullSecrets []meta.LocalObjectReference `json:"imagePullSecrets"`
64+
}
65+
5966
func (c *BootstrapperConfig) ReadFromFile(path string) error {
6067
data, err := os.ReadFile(path)
6168
if err != nil {

internal/deployment-repo/deploymentRepoManager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ func (m *DeploymentRepoManager) RunKustomizeAndApply(ctx context.Context) error
599599
for _, manifest := range manifests {
600600
if manifest.GetKind() == "Kustomization" && strings.Contains(manifest.GetAPIVersion(), "kustomize.toolkit.fluxcd.io") {
601601
logger.Infof("Applying Kustomization manifest: %s/%s", manifest.GetNamespace(), manifest.GetName())
602-
err = util.ApplyUnstructuredObject(ctx, m.TargetCluster, manifest)
602+
err = util.CreateOrUpdate(ctx, m.TargetCluster, manifest)
603603
if err != nil {
604604
return fmt.Errorf("failed to apply Kustomization manifest %s/%s: %w", manifest.GetNamespace(), manifest.GetName(), err)
605605
}

internal/eso-deployer/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package eso_deployer
2+
3+
const (
4+
esoNamespace = "external-secrets"
5+
esoImageRepoName = "external-secrets-image"
6+
esoChartRepoName = "external-secrets-chart"
7+
esoHelmReleaseName = "external-secrets-operator"
8+
)

internal/eso-deployer/deployer.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package eso_deployer
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
helmv2 "github.com/fluxcd/helm-controller/api/v2"
10+
sourcev1 "github.com/fluxcd/source-controller/api/v1"
11+
"github.com/openmcp-project/controller-utils/pkg/clusters"
12+
"github.com/sirupsen/logrus"
13+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
16+
"github.com/openmcp-project/bootstrapper/internal/component"
17+
cfg "github.com/openmcp-project/bootstrapper/internal/config"
18+
"github.com/openmcp-project/bootstrapper/internal/flux_deployer"
19+
ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli"
20+
"github.com/openmcp-project/bootstrapper/internal/util"
21+
)
22+
23+
type EsoDeployer struct {
24+
Config *cfg.BootstrapperConfig
25+
26+
// OcmConfigPath is the path to the OCM configuration file
27+
OcmConfigPath string
28+
29+
platformCluster *clusters.Cluster
30+
log *logrus.Logger
31+
}
32+
33+
func NewEsoDeployer(config *cfg.BootstrapperConfig, ocmConfigPath string, platformCluster *clusters.Cluster, log *logrus.Logger) *EsoDeployer {
34+
return &EsoDeployer{
35+
Config: config,
36+
OcmConfigPath: ocmConfigPath,
37+
platformCluster: platformCluster,
38+
log: log,
39+
}
40+
}
41+
42+
func (d *EsoDeployer) Deploy(ctx context.Context) error {
43+
componentManager, err := component.NewComponentManager(ctx, d.Config, d.OcmConfigPath)
44+
if err != nil {
45+
return fmt.Errorf("error creating component manager: %w", err)
46+
}
47+
48+
return d.DeployWithComponentManager(ctx, componentManager)
49+
}
50+
51+
func (d *EsoDeployer) DeployWithComponentManager(ctx context.Context, componentManager component.ComponentManager) error {
52+
d.log.Info("Getting OCM component containing ESO resources.")
53+
esoComponent, err := componentManager.GetComponentWithImageResources(ctx, "external-secrets-operator-image")
54+
if err != nil {
55+
return fmt.Errorf("failed to get external-secrets-operator-image component: %w", err)
56+
}
57+
58+
esoChartRes, err := esoComponent.GetResource("external-secrets-operator-chart")
59+
if err != nil {
60+
return fmt.Errorf("failed to get external-secrets-operator-chart resource: %w", err)
61+
}
62+
d.log.Info("Deploying OCIRepo for ESO chart.")
63+
if err = d.deployRepo(ctx, esoChartRes, esoChartRepoName); err != nil {
64+
return fmt.Errorf("failed to create helm chart repo: %w", err)
65+
}
66+
67+
esoImageRes, err := esoComponent.GetResource("external-secrets-operator-image")
68+
if err != nil {
69+
return fmt.Errorf("failed to get external-secrets-operator-image resource: %w", err)
70+
}
71+
d.log.Info("Deploying OCIRepo for ESO image.")
72+
if err = d.deployRepo(ctx, esoImageRes, esoImageRepoName); err != nil {
73+
return fmt.Errorf("failed to create helm image repo: %w", err)
74+
}
75+
76+
d.log.Info("Deploying HelmRelease for ESO.")
77+
if err = d.deployHelmRelease(ctx, esoImageRes); err != nil {
78+
return fmt.Errorf("failed to deploy helm release: %w", err)
79+
}
80+
81+
d.log.Info("Done.")
82+
return nil
83+
}
84+
85+
func (d *EsoDeployer) deployHelmRelease(ctx context.Context, res *ocmcli.Resource) error {
86+
name, tag, _, err := util.ParseImageVersionAndTag(*res.Access.ImageReference)
87+
if err != nil {
88+
return fmt.Errorf("failed to parse image resource: %w", err)
89+
}
90+
91+
values := map[string]any{
92+
"image": map[string]any{
93+
"repository": name,
94+
"tag": tag,
95+
},
96+
}
97+
values["imagePullSecrets"] = d.Config.ExternalSecrets.ImagePullSecrets
98+
99+
encoded, err := json.Marshal(values)
100+
if err != nil {
101+
return fmt.Errorf("failed to marshal ESO Helm values: %w", err)
102+
}
103+
jsonVals := &apiextensionsv1.JSON{Raw: encoded}
104+
105+
helmRelease := &helmv2.HelmRelease{
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: esoHelmReleaseName,
108+
Namespace: flux_deployer.FluxSystemNamespace,
109+
},
110+
Spec: helmv2.HelmReleaseSpec{
111+
ChartRef: &helmv2.CrossNamespaceSourceReference{
112+
Kind: "OCIRepository",
113+
Name: esoChartRepoName,
114+
Namespace: flux_deployer.FluxSystemNamespace,
115+
},
116+
ReleaseName: "eso",
117+
TargetNamespace: esoNamespace,
118+
Install: &helmv2.Install{
119+
CreateNamespace: true,
120+
},
121+
Values: jsonVals,
122+
},
123+
}
124+
return util.CreateOrUpdate(ctx, d.platformCluster, helmRelease)
125+
}
126+
127+
func (d *EsoDeployer) deployRepo(ctx context.Context, res *ocmcli.Resource, repoName string) error {
128+
name, tag, digest, err := util.ParseImageVersionAndTag(*res.Access.ImageReference)
129+
if err != nil {
130+
return err
131+
}
132+
133+
ociRepo := &sourcev1.OCIRepository{
134+
ObjectMeta: metav1.ObjectMeta{
135+
Name: repoName,
136+
Namespace: flux_deployer.FluxSystemNamespace,
137+
},
138+
Spec: sourcev1.OCIRepositorySpec{
139+
URL: fmt.Sprintf("oci://%s", name),
140+
Reference: &sourcev1.OCIRepositoryRef{
141+
Tag: tag,
142+
Digest: digest,
143+
},
144+
Timeout: &metav1.Duration{Duration: 1 * time.Minute},
145+
SecretRef: d.Config.ExternalSecrets.RepositorySecretRef,
146+
},
147+
}
148+
return util.CreateOrUpdate(ctx, d.platformCluster, ociRepo)
149+
}

0 commit comments

Comments
 (0)