44package cmd
55
66import (
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
4155func (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
8498func 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) {
133148func 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