Skip to content

Commit 1d181af

Browse files
committed
Read config from multidoc/multiple YAMLs
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
1 parent 082a528 commit 1d181af

File tree

16 files changed

+564
-50
lines changed

16 files changed

+564
-50
lines changed

.github/workflows/smoke.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ jobs:
108108
env:
109109
LINUX_IMAGE: ${{ matrix.image }}
110110
run: make smoke-basic-openssh
111+
112+
smoke-multidoc:
113+
strategy:
114+
matrix:
115+
image:
116+
- quay.io/k0sproject/bootloose-alpine3.18
117+
name: Basic 1+1 smoke using multidoc yamls
118+
needs: build
119+
runs-on: ubuntu-20.04
120+
121+
steps:
122+
- uses: actions/checkout@v4
123+
- uses: ./.github/actions/smoke-test-cache
124+
- name: Run smoke tests
125+
env:
126+
LINUX_IMAGE: ${{ matrix.image }}
127+
run: make smoke-multidoc
111128

112129
smoke-files:
113130
strategy:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ build-all: $(addprefix bin/,$(bins)) bin/checksums.md
5151
clean:
5252
rm -rf bin/ k0sctl
5353

54-
smoketests := smoke-basic smoke-basic-rootless smoke-files smoke-upgrade smoke-reset smoke-os-override smoke-init smoke-backup-restore smoke-dynamic smoke-basic-openssh smoke-dryrun smoke-downloadurl smoke-controller-swap smoke-reinstall
54+
smoketests := smoke-basic smoke-basic-rootless smoke-files smoke-upgrade smoke-reset smoke-os-override smoke-init smoke-backup-restore smoke-dynamic smoke-basic-openssh smoke-dryrun smoke-downloadurl smoke-controller-swap smoke-reinstall smoke-multidoc
5555
.PHONY: $(smoketests)
5656
$(smoketests): k0sctl
5757
$(MAKE) -C smoke-test $@

action/apply.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ type ApplyOptions struct {
3535
KubeconfigUser string
3636
// KubeconfigCluster is the cluster name to use in the kubeconfig
3737
KubeconfigCluster string
38-
// ConfigPath is the path to the configuration file (used for kubeconfig command tip on success)
39-
ConfigPath string
38+
// ConfigPaths is the list of paths to the configuration files (used for kubeconfig command tip on success)
39+
ConfigPaths []string
4040
}
4141

4242
type Apply struct {
@@ -158,9 +158,11 @@ func (a Apply) Run() error {
158158
cmd.WriteString(executable)
159159
cmd.WriteString(" kubeconfig")
160160

161-
if a.ConfigPath != "" && a.ConfigPath != "-" && a.ConfigPath != "k0sctl.yaml" {
162-
cmd.WriteString(" --config ")
163-
cmd.WriteString(a.ConfigPath)
161+
if len(a.ConfigPaths) > 0 && (len(a.ConfigPaths) != 1 && a.ConfigPaths[0] != "-" && a.ConfigPaths[0] != "k0sctl.yaml") {
162+
for _, path := range a.ConfigPaths {
163+
cmd.WriteString(" --config ")
164+
cmd.WriteString(path)
165+
}
164166
}
165167

166168
log.Info("Tip: To access the cluster you can now fetch the admin kubeconfig using:")

cmd/apply.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ var applyCommand = &cli.Command{
9090
NoDrain: ctx.Bool("no-drain"),
9191
DisableDowngradeCheck: ctx.Bool("disable-downgrade-check"),
9292
RestoreFrom: ctx.String("restore-from"),
93-
ConfigPath: ctx.String("config"),
93+
ConfigPaths: ctx.StringSlice("config"),
9494
}
9595

9696
applyAction := action.NewApply(applyOpts)

cmd/config_edit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var configEditCommand = &cli.Command{
1919
Before: actions(initLogging, initConfig),
2020
Action: func(ctx *cli.Context) error {
2121
configEditAction := action.ConfigEdit{
22-
Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster),
22+
Config: ctx.Context.Value(ctxConfigsKey{}).(*v1beta1.Cluster),
2323
Stdout: ctx.App.Writer,
2424
Stderr: ctx.App.ErrWriter,
2525
Stdin: ctx.App.Reader,

cmd/config_status.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"github.com/k0sproject/k0sctl/action"
5-
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
65

76
"github.com/urfave/cli/v2"
87
)
@@ -23,8 +22,13 @@ var configStatusCommand = &cli.Command{
2322
},
2423
Before: actions(initLogging, initConfig),
2524
Action: func(ctx *cli.Context) error {
25+
cfg, err := readConfig(ctx)
26+
if err != nil {
27+
return err
28+
}
29+
2630
configStatusAction := action.ConfigStatus{
27-
Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster),
31+
Config: cfg,
2832
Format: ctx.String("output"),
2933
Writer: ctx.App.Writer,
3034
}

cmd/flags.go

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import (
1212

1313
"github.com/a8m/envsubst"
1414
"github.com/adrg/xdg"
15+
glob "github.com/bmatcuk/doublestar/v4"
16+
"github.com/k0sproject/dig"
1517
"github.com/k0sproject/k0sctl/phase"
1618
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
19+
"github.com/k0sproject/k0sctl/pkg/manifest"
1720
"github.com/k0sproject/k0sctl/pkg/retry"
1821
k0sctl "github.com/k0sproject/k0sctl/version"
1922
"github.com/k0sproject/rig"
@@ -22,11 +25,10 @@ import (
2225
"github.com/shiena/ansicolor"
2326
log "github.com/sirupsen/logrus"
2427
"github.com/urfave/cli/v2"
25-
"gopkg.in/yaml.v2"
2628
)
2729

2830
type (
29-
ctxConfigKey struct{}
31+
ctxConfigsKey struct{}
3032
ctxManagerKey struct{}
3133
ctxLogFileKey struct{}
3234
)
@@ -58,11 +60,11 @@ var (
5860
Value: false,
5961
}
6062

61-
configFlag = &cli.StringFlag{
63+
configFlag = &cli.StringSliceFlag{
6264
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.",
6466
Aliases: []string{"c"},
65-
Value: "k0sctl.yaml",
67+
Value: cli.NewStringSlice("k0sctl.yaml"),
6668
TakesFile: true,
6769
}
6870

@@ -115,44 +117,73 @@ func actions(funcs ...func(*cli.Context) error) func(*cli.Context) error {
115117

116118
// initConfig takes the config flag, does some magic and replaces the value with the file contents
117119
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] == "" {
120122
return nil
121123
}
122124

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+
}
126148
}
127-
defer file.Close()
128149

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")
132152
}
133153

134-
subst, err := envsubst.Bytes(content)
135-
if err != nil {
136-
return err
137-
}
154+
log.Debugf("%d potential configuration files found", len(configs))
138155

139-
log.Debugf("Loaded configuration:\n%s", subst)
156+
manifestReader := &manifest.Reader{}
140157

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()
145164

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+
}
149180
}
150181

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")
153184
}
154185

155-
ctx.Context = context.WithValue(ctx.Context, ctxConfigKey{}, c)
186+
ctx.Context = context.WithValue(ctx.Context, ctxConfigsKey{}, manifestReader)
156187

157188
return nil
158189
}
@@ -181,13 +212,47 @@ func warnOldCache(_ *cli.Context) error {
181212
return nil
182213
}
183214

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+
184249
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
188253
}
189254

190-
manager, err := phase.NewManager(c)
255+
manager, err := phase.NewManager(cfg)
191256
if err != nil {
192257
return fmt.Errorf("failed to initialize phase manager: %w", err)
193258
}
@@ -382,3 +447,18 @@ func displayLogo(_ *cli.Context) error {
382447
fmt.Print(logo)
383448
return nil
384449
}
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+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/creasty/defaults v1.8.0
1515
github.com/gofrs/uuid v4.4.0+incompatible // indirect
1616
github.com/hashicorp/go-version v1.7.0 // indirect
17-
github.com/k0sproject/dig v0.3.1
17+
github.com/k0sproject/dig v0.4.0
1818
github.com/k0sproject/rig v0.19.0
1919
github.com/logrusorgru/aurora v2.0.3+incompatible
2020
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
109109
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
110110
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
111111
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
112-
github.com/k0sproject/dig v0.3.1 h1:/QK40lXQ/HEE3LMT3r/kST1ANhMVZiajNDXI+spbL9o=
113-
github.com/k0sproject/dig v0.3.1/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4=
112+
github.com/k0sproject/dig v0.4.0 h1:yBxFUUxNXAMGBg6b7c6ypxdx/o3RmhoI5v5ABOw5tn0=
113+
github.com/k0sproject/dig v0.4.0/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4=
114114
github.com/k0sproject/rig v0.19.0 h1:aF/wJDfK45Ho2Z75Uap+u4Q4jHgr/1WfrHcOg2U9/n0=
115115
github.com/k0sproject/rig v0.19.0/go.mod h1:SNa9+xeVA6zQVYx+SINaa4ZihFPWrmo/6crHcdvJRFI=
116116
github.com/k0sproject/version v0.6.0 h1:Wi8wu9j+H36+okIQA47o/YHbzNpKeIYj8IjGdJOdqsI=

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ const APIVersion = "k0sctl.k0sproject.io/v1beta1"
1414

1515
// ClusterMetadata defines cluster metadata
1616
type ClusterMetadata struct {
17-
Name string `yaml:"name" validate:"required" default:"k0s-cluster"`
18-
User string `yaml:"user" default:"admin"`
19-
Kubeconfig string `yaml:"-"`
20-
EtcdMembers []string `yaml:"-"`
17+
Name string `yaml:"name" validate:"required" default:"k0s-cluster"`
18+
User string `yaml:"user" default:"admin"`
19+
Kubeconfig string `yaml:"-"`
20+
EtcdMembers []string `yaml:"-"`
2121
}
2222

2323
// Cluster describes launchpad.yaml configuration

0 commit comments

Comments
 (0)