Skip to content

Commit c38393e

Browse files
authored
feat(install): support merged config overlays and pc-apps values (#522)
Allow repeated --config flags for install codesphere commands and merge the rendered YAML files in order before invoking the installer. Also expose pc-apps config on the normal config file and values files on the dependencies flow and pass them into the BOM-based pc-apps installer. config: ```yaml pcApps: chartRegistry: example.com/charts ``` --------- Signed-off-by: schrodit <7979201+schrodit@users.noreply.github.com> Co-authored-by: schrodit <7979201+schrodit@users.noreply.github.com>
1 parent 621cbe6 commit c38393e

16 files changed

Lines changed: 539 additions & 124 deletions

cli/cmd/install_codesphere.go

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44
package cmd
55

66
import (
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
711
"github.com/codesphere-cloud/cs-go/pkg/io"
12+
"github.com/codesphere-cloud/oms/internal/configtemplating"
813
"github.com/codesphere-cloud/oms/internal/env"
914
"github.com/codesphere-cloud/oms/internal/installer"
1015
"github.com/codesphere-cloud/oms/internal/installer/argocd"
16+
"github.com/codesphere-cloud/oms/internal/installer/files"
1117
"github.com/codesphere-cloud/oms/internal/util"
1218
"github.com/spf13/cobra"
19+
"go.yaml.in/yaml/v3"
20+
)
21+
22+
const (
23+
mergedInstallConfigDirPattern = "oms-install-config-*"
24+
mergedInstallConfigFileName = "config.yaml"
1325
)
1426

1527
// InstallCodesphereCmd represents the codesphere command
@@ -23,7 +35,8 @@ type InstallCodesphereOpts struct {
2335
*GlobalOptions
2436
Package string
2537
Force bool
26-
Config string
38+
Configs []string
39+
ConfigPath string
2740
Vault string
2841
PrivKey string
2942
SkipSteps []string
@@ -36,10 +49,11 @@ type InstallCodesphereOpts struct {
3649
ArgoCDForceConflicts bool
3750
ArgoCDRepoURL string
3851
ArgoCDValues []string
52+
PCAppsValues []string
3953
}
4054

4155
func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
42-
cfg, cleanup, err := parseInstallConfig(c.Opts, installer.NewConfig())
56+
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
4357
if err != nil {
4458
return err
4559
}
@@ -59,17 +73,17 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
5973
}
6074

6175
if c.Opts.CodesphereOnly {
62-
return installCodespherePlatform(c.Opts, c.Env)
76+
return installCodespherePlatform(effectiveOpts, c.Env)
6377
}
6478

6579
if infraInstaller.HasExecutableSteps(cfg) {
66-
if err := installCodesphereInfra(c.Opts, c.Env); err != nil {
80+
if err := installCodesphereInfra(effectiveOpts, c.Env); err != nil {
6781
return err
6882
}
6983
}
7084

7185
if dependenciesInstaller.HasExecutableSteps(cfg) || !installer.IsStepSkipped(cfg, c.Opts.SkipSteps, installer.ArgoCDStep) {
72-
if err := installCodesphereDepencies(c.Opts, c.Env); err != nil {
86+
if err := installCodesphereDepencies(effectiveOpts, cfg, c.Env); err != nil {
7387
return err
7488
}
7589
}
@@ -78,7 +92,7 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
7892
return nil
7993
}
8094

81-
return installCodespherePlatform(c.Opts, c.Env)
95+
return installCodespherePlatform(effectiveOpts, c.Env)
8296
}
8397

8498
func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) {
@@ -104,7 +118,7 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) {
104118
}
105119
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load binaries, installer etc. from")
106120
codesphere.cmd.PersistentFlags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction")
107-
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.Config, "config", "c", "", "Path to the Codesphere Private Cloud configuration file (yaml)")
121+
codesphere.cmd.PersistentFlags().StringArrayVarP(&codesphere.Opts.Configs, "config", "c", nil, "Path to a Codesphere Private Cloud configuration file (yaml). Can be specified multiple times and merged in order")
108122
codesphere.cmd.PersistentFlags().StringVar(&codesphere.Opts.Vault, "vault", "", "Path to the SOPS-encrypted prod.vault.yaml file used for config templating")
109123
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.PrivKey, "priv-key", "k", "", "Path to the private key to encrypt/decrypt secrets")
110124
codesphere.cmd.PersistentFlags().StringSliceVarP(&codesphere.Opts.SkipSteps, "skip-steps", "s", []string{}, "Steps to be skipped. E.g. copy-dependencies, extract-dependencies, load-container-images, ceph, postgres, kubernetes, docker, argocd")
@@ -116,6 +130,7 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) {
116130
codesphere.cmd.PersistentFlags().BoolVar(&codesphere.Opts.ArgoCDForceConflicts, "argo-force-conflicts", false, "Force SSA ownership conflicts during ArgoCD install")
117131
codesphere.cmd.PersistentFlags().StringVar(&codesphere.Opts.ArgoCDRepoURL, "argo-repo", argocd.DefaultRepoURL, "ArgoCD Helm chart repository URL")
118132
codesphere.cmd.PersistentFlags().StringArrayVar(&codesphere.Opts.ArgoCDValues, "argo-values", nil, "ArgoCD values YAML file (can be specified multiple times)")
133+
codesphere.cmd.PersistentFlags().StringArrayVar(&codesphere.Opts.PCAppsValues, "pc-apps-values", nil, "pc-apps values YAML file (can be specified multiple times)")
119134

120135
util.MarkPersistentFlagRequired(codesphere.cmd, "package")
121136
util.MarkPersistentFlagRequired(codesphere.cmd, "config")
@@ -133,3 +148,103 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) {
133148
func sharedInstallCodesphereSteps() []string {
134149
return []string{"copy-dependencies", "extract-dependencies"}
135150
}
151+
152+
// prepareInstallConfig resolves the install command's repeated --config inputs
153+
// into a single config file for downstream installer steps.
154+
//
155+
// For each input config it optionally renders vault-backed template expressions,
156+
// parses the YAML into a generic map, and deep-merges the maps in flag order so
157+
// later --config values override earlier ones. The merged YAML is then written
158+
// to a dedicated temporary directory at a stable file name,
159+
// "<tmp>/config.yaml", parsed once through the config manager, and returned via
160+
// effectiveOpts.ConfigPath. The returned cleanup function removes any rendered
161+
// per-file temps as well as the merged config directory.
162+
func prepareInstallConfig(opts *InstallCodesphereOpts, cm installer.ConfigManager) (*InstallCodesphereOpts, files.RootConfig, func(), error) {
163+
configFiles := append([]string(nil), opts.Configs...)
164+
if len(configFiles) == 0 {
165+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("no config.yaml input provided: at least one config file is required")
166+
}
167+
168+
store := installer.NewLazyVaultTemplatingSecretStore(opts.Vault, opts.PrivKey)
169+
cleanupFns := []func(){}
170+
cleanup := func() {
171+
for i := len(cleanupFns) - 1; i >= 0; i-- {
172+
cleanupFns[i]()
173+
}
174+
}
175+
176+
merged := map[string]any{}
177+
for _, configPath := range configFiles {
178+
renderedPath := configPath
179+
if opts.Vault != "" {
180+
tmpPath, renderCleanup, err := configtemplating.RenderConfigFileToTemp(configPath, store)
181+
if err != nil {
182+
cleanup()
183+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to render config template %s: %w", configPath, err)
184+
}
185+
cleanupFns = append(cleanupFns, renderCleanup)
186+
renderedPath = tmpPath
187+
}
188+
189+
data, err := os.ReadFile(renderedPath)
190+
if err != nil {
191+
cleanup()
192+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to read config file %s: %w", renderedPath, err)
193+
}
194+
195+
var partial map[string]any
196+
if err := yaml.Unmarshal(data, &partial); err != nil {
197+
cleanup()
198+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to parse config file %s: %w", renderedPath, err)
199+
}
200+
if partial == nil {
201+
partial = map[string]any{}
202+
}
203+
merged = util.DeepMergeMaps(merged, partial)
204+
}
205+
206+
mergedBytes, err := yaml.Marshal(merged)
207+
if err != nil {
208+
cleanup()
209+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to marshal merged config.yaml: %w", err)
210+
}
211+
212+
mergedDir, err := os.MkdirTemp("", mergedInstallConfigDirPattern)
213+
if err != nil {
214+
cleanup()
215+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to create merged config directory: %w", err)
216+
}
217+
mergedPath := filepath.Join(mergedDir, mergedInstallConfigFileName)
218+
tmp, err := os.OpenFile(mergedPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
219+
if err != nil {
220+
cleanup()
221+
_ = os.RemoveAll(mergedDir)
222+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to create merged config file %s: %w", mergedPath, err)
223+
}
224+
if _, err := tmp.Write(mergedBytes); err != nil {
225+
_ = tmp.Close()
226+
_ = os.RemoveAll(mergedDir)
227+
cleanup()
228+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to write merged config file: %w", err)
229+
}
230+
if err := tmp.Close(); err != nil {
231+
_ = os.RemoveAll(mergedDir)
232+
cleanup()
233+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to close merged config file: %w", err)
234+
}
235+
cleanupFns = append(cleanupFns, func() {
236+
_ = os.RemoveAll(mergedDir)
237+
})
238+
239+
cfg, err := cm.ParseConfigYaml(mergedPath)
240+
if err != nil {
241+
cleanup()
242+
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to parse merged config.yaml: %w", err)
243+
}
244+
245+
effectiveOpts := *opts
246+
effectiveOpts.ConfigPath = mergedPath
247+
effectiveOpts.Configs = append([]string(nil), configFiles...)
248+
249+
return &effectiveOpts, cfg, cleanup, nil
250+
}

0 commit comments

Comments
 (0)