@@ -12,8 +12,11 @@ import (
12
12
13
13
"github.com/a8m/envsubst"
14
14
"github.com/adrg/xdg"
15
+ glob "github.com/bmatcuk/doublestar/v4"
16
+ "github.com/k0sproject/dig"
15
17
"github.com/k0sproject/k0sctl/phase"
16
18
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
19
+ "github.com/k0sproject/k0sctl/pkg/manifest"
17
20
"github.com/k0sproject/k0sctl/pkg/retry"
18
21
k0sctl "github.com/k0sproject/k0sctl/version"
19
22
"github.com/k0sproject/rig"
@@ -22,11 +25,10 @@ import (
22
25
"github.com/shiena/ansicolor"
23
26
log "github.com/sirupsen/logrus"
24
27
"github.com/urfave/cli/v2"
25
- "gopkg.in/yaml.v2"
26
28
)
27
29
28
30
type (
29
- ctxConfigKey struct {}
31
+ ctxConfigsKey struct {}
30
32
ctxManagerKey struct {}
31
33
ctxLogFileKey struct {}
32
34
)
@@ -58,11 +60,11 @@ var (
58
60
Value : false ,
59
61
}
60
62
61
- configFlag = & cli.StringFlag {
63
+ configFlag = & cli.StringSliceFlag {
62
64
Name : "config" ,
63
- Usage : "Path to cluster config yaml. Use '-' to read from stdin." ,
65
+ Usage : "Path or glob to config yaml. Can be given multiple times . Use '-' to read from stdin." ,
64
66
Aliases : []string {"c" },
65
- Value : "k0sctl.yaml" ,
67
+ Value : cli . NewStringSlice ( "k0sctl.yaml" ) ,
66
68
TakesFile : true ,
67
69
}
68
70
@@ -115,44 +117,73 @@ func actions(funcs ...func(*cli.Context) error) func(*cli.Context) error {
115
117
116
118
// initConfig takes the config flag, does some magic and replaces the value with the file contents
117
119
func initConfig (ctx * cli.Context ) error {
118
- f := ctx .String ("config" )
119
- if f == "" {
120
+ f := ctx .StringSlice ("config" )
121
+ if len ( f ) == 0 || f [ 0 ] == "" {
120
122
return nil
121
123
}
122
124
123
- file , err := configReader (f )
124
- if err != nil {
125
- return err
125
+ var configs []string
126
+ // detect globs and expand
127
+ for _ , p := range f {
128
+ if p == "-" || p == "k0sctl.yaml" {
129
+ configs = append (configs , p )
130
+ continue
131
+ }
132
+ stat , err := os .Stat (p )
133
+ if err == nil {
134
+ if stat .IsDir () {
135
+ p = path .Join (p , "**/*.{yml,yaml}" )
136
+ }
137
+ }
138
+ base , pattern := glob .SplitPattern (p )
139
+ fsys := os .DirFS (base )
140
+ matches , err := glob .Glob (fsys , pattern )
141
+ if err != nil {
142
+ return err
143
+ }
144
+ log .Debugf ("glob %s expanded to %v" , p , matches )
145
+ for _ , m := range matches {
146
+ configs = append (configs , path .Join (base , m ))
147
+ }
126
148
}
127
- defer file .Close ()
128
149
129
- content , err := io .ReadAll (file )
130
- if err != nil {
131
- return err
150
+ if len (configs ) == 0 {
151
+ return fmt .Errorf ("no configuration files found" )
132
152
}
133
153
134
- subst , err := envsubst .Bytes (content )
135
- if err != nil {
136
- return err
137
- }
154
+ log .Debugf ("%d potential configuration files found" , len (configs ))
138
155
139
- log . Debugf ( "Loaded configuration: \n %s" , subst )
156
+ manifestReader := & manifest. Reader {}
140
157
141
- c := & v1beta1.Cluster {}
142
- if err := yaml .UnmarshalStrict (subst , c ); err != nil {
143
- return err
144
- }
158
+ for _ , f := range configs {
159
+ file , err := configReader (f )
160
+ if err != nil {
161
+ return err
162
+ }
163
+ defer file .Close ()
145
164
146
- m , err := yaml .Marshal (c )
147
- if err == nil {
148
- log .Tracef ("unmarshaled configuration:\n %s" , m )
165
+ content , err := io .ReadAll (file )
166
+ if err != nil {
167
+ return err
168
+ }
169
+
170
+ subst , err := envsubst .Bytes (content )
171
+ if err != nil {
172
+ return err
173
+ }
174
+
175
+ log .Debugf ("Loaded configuration from %s:\n %s" , f , subst )
176
+
177
+ if err := manifestReader .ParseBytes (subst ); err != nil {
178
+ return fmt .Errorf ("failed to parse config: %w" , err )
179
+ }
149
180
}
150
181
151
- if err := c . Validate (); err != nil {
152
- return fmt .Errorf ("configuration validation failed: %w" , err )
182
+ if manifestReader . Len () == 0 {
183
+ return fmt .Errorf ("no resource definition manifests found in configuration files" )
153
184
}
154
185
155
- ctx .Context = context .WithValue (ctx .Context , ctxConfigKey {}, c )
186
+ ctx .Context = context .WithValue (ctx .Context , ctxConfigsKey {}, manifestReader )
156
187
157
188
return nil
158
189
}
@@ -181,13 +212,47 @@ func warnOldCache(_ *cli.Context) error {
181
212
return nil
182
213
}
183
214
215
+ func readConfig (ctx * cli.Context ) (* v1beta1.Cluster , error ) {
216
+ mr , err := ManifestReader (ctx .Context )
217
+ if err != nil {
218
+ return nil , fmt .Errorf ("failed to get manifest reader: %w" , err )
219
+ }
220
+ ctlConfigs , err := mr .GetResources (v1beta1 .APIVersion , "Cluster" )
221
+ if err != nil {
222
+ return nil , fmt .Errorf ("failed to get cluster resources: %w" , err )
223
+ }
224
+ if len (ctlConfigs ) != 1 {
225
+ return nil , fmt .Errorf ("expected exactly one cluster config, got %d" , len (ctlConfigs ))
226
+ }
227
+ cfg := & v1beta1.Cluster {}
228
+ if err := ctlConfigs [0 ].Unmarshal (cfg ); err != nil {
229
+ return nil , fmt .Errorf ("failed to unmarshal cluster config: %w" , err )
230
+ }
231
+ if k0sConfigs , err := mr .GetResources ("k0s.k0sproject.io/v1beta1" , "ClusterConfig" ); err == nil && len (k0sConfigs ) > 0 {
232
+ for _ , k0sConfig := range k0sConfigs {
233
+ k0s := make (dig.Mapping )
234
+ log .Debugf ("unmarshalling %d bytes of config from %v" , len (k0sConfig .Raw ), k0sConfig .Filename ())
235
+ if err := k0sConfig .Unmarshal (& k0s ); err != nil {
236
+ return nil , fmt .Errorf ("failed to unmarshal k0s config: %w" , err )
237
+ }
238
+ log .Debugf ("merging in k0s config from %v" , k0sConfig .Filename ())
239
+ cfg .Spec .K0s .Config .Merge (k0s )
240
+ }
241
+ }
242
+
243
+ if err := cfg .Validate (); err != nil {
244
+ return nil , fmt .Errorf ("cluster config validation failed: %w" , err )
245
+ }
246
+ return cfg , nil
247
+ }
248
+
184
249
func initManager (ctx * cli.Context ) error {
185
- c , ok := ctx . Context . Value ( ctxConfigKey {}).( * v1beta1. Cluster )
186
- if c == nil || ! ok {
187
- return fmt . Errorf ( "cluster config not available in context" )
250
+ cfg , err := readConfig ( ctx )
251
+ if err != nil {
252
+ return err
188
253
}
189
254
190
- manager , err := phase .NewManager (c )
255
+ manager , err := phase .NewManager (cfg )
191
256
if err != nil {
192
257
return fmt .Errorf ("failed to initialize phase manager: %w" , err )
193
258
}
@@ -382,3 +447,18 @@ func displayLogo(_ *cli.Context) error {
382
447
fmt .Print (logo )
383
448
return nil
384
449
}
450
+
451
+ // ManifestReader returns a manifest reader from context
452
+ func ManifestReader (ctx context.Context ) (* manifest.Reader , error ) {
453
+ if ctx == nil {
454
+ return nil , fmt .Errorf ("context is nil" )
455
+ }
456
+ v := ctx .Value (ctxConfigsKey {})
457
+ if v == nil {
458
+ return nil , fmt .Errorf ("config reader not found in context" )
459
+ }
460
+ if r , ok := v .(* manifest.Reader ); ok {
461
+ return r , nil
462
+ }
463
+ return nil , fmt .Errorf ("config reader in context is not of the correct type" )
464
+ }
0 commit comments