Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package k0sctl for openSUSE:Factory checked in at 2025-01-22 16:39:01 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/k0sctl (Old) and /work/SRC/openSUSE:Factory/.k0sctl.new.5589 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "k0sctl" Wed Jan 22 16:39:01 2025 rev:11 rq:1239504 version:0.22.0 Changes: -------- --- /work/SRC/openSUSE:Factory/k0sctl/k0sctl.changes 2024-12-20 15:28:15.762194144 +0100 +++ /work/SRC/openSUSE:Factory/.k0sctl.new.5589/k0sctl.changes 2025-01-22 16:39:01.932637765 +0100 @@ -1,0 +2,18 @@ +Wed Jan 22 11:37:45 UTC 2025 - opensuse_buildserv...@ojkastl.de + +- Update to version 0.22.0: + * Use cluster.StorageType() for ValidateEtcdMembers phase (#823) + * Fix multidoc smoke-test retry (#822) + * Bump github.com/go-playground/validator/v10 from 10.23.0 to + 10.24.0 (#821) + * Bump k8s.io/client-go from 0.32.0 to 0.32.1 (#820) + * Bump github.com/bmatcuk/doublestar/v4 from 4.7.1 to 4.8.0 + (#819) + * Bump golang.org/x/net from 0.30.0 to 0.33.0 (#818) + * Apply additional kube manifests from configs to cluster (#817) + * Do not set etcd peerAddress when cluster storage type is kine + (#816) + * Allow reading k0s config from a separate or multidoc YAML + document (#814) + +------------------------------------------------------------------- Old: ---- k0sctl-0.21.0.obscpio New: ---- k0sctl-0.22.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ k0sctl.spec ++++++ --- /var/tmp/diff_new_pack.XS14cY/_old 2025-01-22 16:39:03.008682324 +0100 +++ /var/tmp/diff_new_pack.XS14cY/_new 2025-01-22 16:39:03.012682490 +0100 @@ -1,7 +1,7 @@ # # spec file for package k0sctl # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2025 SUSE LLC # Copyright (c) 2021-2022 Orville Q. Song <orvi...@anislet.dev> # # All modifications and additions to the file contributed by third parties @@ -18,7 +18,7 @@ Name: k0sctl -Version: 0.21.0 +Version: 0.22.0 Release: 0 Summary: A bootstrapping and management tool for k0s clusters License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.XS14cY/_old 2025-01-22 16:39:03.040683649 +0100 +++ /var/tmp/diff_new_pack.XS14cY/_new 2025-01-22 16:39:03.044683815 +0100 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/k0sproject/k0sctl.git</param> <param name="scm">git</param> - <param name="revision">v0.21.0</param> + <param name="revision">v0.22.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.XS14cY/_old 2025-01-22 16:39:03.064684644 +0100 +++ /var/tmp/diff_new_pack.XS14cY/_new 2025-01-22 16:39:03.064684644 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/k0sproject/k0sctl.git</param> - <param name="changesrevision">082a528e9a36a4a80819733ffee3881f7f06dd13</param></service></servicedata> + <param name="changesrevision">929602e5d57527e1bdf39d607555e3d031f2cf5e</param></service></servicedata> (No newline at EOF) ++++++ k0sctl-0.21.0.obscpio -> k0sctl-0.22.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/.github/workflows/smoke.yml new/k0sctl-0.22.0/.github/workflows/smoke.yml --- old/k0sctl-0.21.0/.github/workflows/smoke.yml 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/.github/workflows/smoke.yml 2025-01-20 14:07:53.000000000 +0100 @@ -108,6 +108,23 @@ env: LINUX_IMAGE: ${{ matrix.image }} run: make smoke-basic-openssh + + smoke-multidoc: + strategy: + matrix: + image: + - quay.io/k0sproject/bootloose-alpine3.18 + name: Basic 1+1 smoke using multidoc yamls + needs: build + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/smoke-test-cache + - name: Run smoke tests + env: + LINUX_IMAGE: ${{ matrix.image }} + run: make smoke-multidoc smoke-files: strategy: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/Makefile new/k0sctl-0.22.0/Makefile --- old/k0sctl-0.21.0/Makefile 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/Makefile 2025-01-20 14:07:53.000000000 +0100 @@ -51,7 +51,7 @@ clean: rm -rf bin/ k0sctl -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 +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 .PHONY: $(smoketests) $(smoketests): k0sctl $(MAKE) -C smoke-test $@ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/README.md new/k0sctl-0.22.0/README.md --- old/k0sctl-0.21.0/README.md 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/README.md 2025-01-20 14:07:53.000000000 +0100 @@ -582,6 +582,26 @@ When left out, the output of `k0s config create` will be used. +You can also host the configuration in a separate file or as a separate YAML document in the same file in the standard k0s configuration format. + +```yaml +apiVersion: k0sctl.k0sproject.io/v1beta1 +kind: Cluster +spec: + hosts: + - role: single + ssh: + address: 10.0.0.1 +--- +apiVersion: k0s.k0sproject.io/v1beta1 +kind: ClusterConfig +metadata: + name: my-k0s-cluster +spec: + api: + externalAddress: 10.0.0.2 +``` + #### Tokens The following tokens can be used in the `k0sDownloadURL` and `files.[*].src` fields: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/action/apply.go new/k0sctl-0.22.0/action/apply.go --- old/k0sctl-0.21.0/action/apply.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/action/apply.go 2025-01-20 14:07:53.000000000 +0100 @@ -35,8 +35,8 @@ KubeconfigUser string // KubeconfigCluster is the cluster name to use in the kubeconfig KubeconfigCluster string - // ConfigPath is the path to the configuration file (used for kubeconfig command tip on success) - ConfigPath string + // ConfigPaths is the list of paths to the configuration files (used for kubeconfig command tip on success) + ConfigPaths []string } type Apply struct { @@ -90,6 +90,7 @@ &phase.ResetWorkers{NoDrain: opts.NoDrain}, &phase.ResetControllers{NoDrain: opts.NoDrain}, &phase.RunHooks{Stage: "after", Action: "apply"}, + &phase.ApplyManifests{}, unlockPhase, &phase.Disconnect{}, }, @@ -158,9 +159,11 @@ cmd.WriteString(executable) cmd.WriteString(" kubeconfig") - if a.ConfigPath != "" && a.ConfigPath != "-" && a.ConfigPath != "k0sctl.yaml" { - cmd.WriteString(" --config ") - cmd.WriteString(a.ConfigPath) + if len(a.ConfigPaths) > 0 && (len(a.ConfigPaths) != 1 && a.ConfigPaths[0] != "-" && a.ConfigPaths[0] != "k0sctl.yaml") { + for _, path := range a.ConfigPaths { + cmd.WriteString(" --config ") + cmd.WriteString(path) + } } log.Info("Tip: To access the cluster you can now fetch the admin kubeconfig using:") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/cmd/apply.go new/k0sctl-0.22.0/cmd/apply.go --- old/k0sctl-0.21.0/cmd/apply.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/cmd/apply.go 2025-01-20 14:07:53.000000000 +0100 @@ -90,7 +90,7 @@ NoDrain: ctx.Bool("no-drain"), DisableDowngradeCheck: ctx.Bool("disable-downgrade-check"), RestoreFrom: ctx.String("restore-from"), - ConfigPath: ctx.String("config"), + ConfigPaths: ctx.StringSlice("config"), } applyAction := action.NewApply(applyOpts) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/cmd/config_edit.go new/k0sctl-0.22.0/cmd/config_edit.go --- old/k0sctl-0.21.0/cmd/config_edit.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/cmd/config_edit.go 2025-01-20 14:07:53.000000000 +0100 @@ -19,7 +19,7 @@ Before: actions(initLogging, initConfig), Action: func(ctx *cli.Context) error { configEditAction := action.ConfigEdit{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), + Config: ctx.Context.Value(ctxConfigsKey{}).(*v1beta1.Cluster), Stdout: ctx.App.Writer, Stderr: ctx.App.ErrWriter, Stdin: ctx.App.Reader, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/cmd/config_status.go new/k0sctl-0.22.0/cmd/config_status.go --- old/k0sctl-0.21.0/cmd/config_status.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/cmd/config_status.go 2025-01-20 14:07:53.000000000 +0100 @@ -2,7 +2,6 @@ import ( "github.com/k0sproject/k0sctl/action" - "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" "github.com/urfave/cli/v2" ) @@ -23,8 +22,13 @@ }, Before: actions(initLogging, initConfig), Action: func(ctx *cli.Context) error { + cfg, err := readConfig(ctx) + if err != nil { + return err + } + configStatusAction := action.ConfigStatus{ - Config: ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster), + Config: cfg, Format: ctx.String("output"), Writer: ctx.App.Writer, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/cmd/flags.go new/k0sctl-0.22.0/cmd/flags.go --- old/k0sctl-0.21.0/cmd/flags.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/cmd/flags.go 2025-01-20 14:07:53.000000000 +0100 @@ -8,12 +8,16 @@ "path" "path/filepath" "runtime" + "strings" "time" "github.com/a8m/envsubst" "github.com/adrg/xdg" + glob "github.com/bmatcuk/doublestar/v4" + "github.com/k0sproject/dig" "github.com/k0sproject/k0sctl/phase" "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/pkg/manifest" "github.com/k0sproject/k0sctl/pkg/retry" k0sctl "github.com/k0sproject/k0sctl/version" "github.com/k0sproject/rig" @@ -22,11 +26,10 @@ "github.com/shiena/ansicolor" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "gopkg.in/yaml.v2" ) type ( - ctxConfigKey struct{} + ctxConfigsKey struct{} ctxManagerKey struct{} ctxLogFileKey struct{} ) @@ -58,11 +61,11 @@ Value: false, } - configFlag = &cli.StringFlag{ + configFlag = &cli.StringSliceFlag{ Name: "config", - Usage: "Path to cluster config yaml. Use '-' to read from stdin.", + Usage: "Path or glob to config yaml. Can be given multiple times. Use '-' to read from stdin.", Aliases: []string{"c"}, - Value: "k0sctl.yaml", + Value: cli.NewStringSlice("k0sctl.yaml"), TakesFile: true, } @@ -115,44 +118,73 @@ // initConfig takes the config flag, does some magic and replaces the value with the file contents func initConfig(ctx *cli.Context) error { - f := ctx.String("config") - if f == "" { + f := ctx.StringSlice("config") + if len(f) == 0 || f[0] == "" { return nil } - file, err := configReader(f) - if err != nil { - return err + var configs []string + // detect globs and expand + for _, p := range f { + if p == "-" || p == "k0sctl.yaml" { + configs = append(configs, p) + continue + } + stat, err := os.Stat(p) + if err == nil { + if stat.IsDir() { + p = path.Join(p, "**/*.{yml,yaml}") + } + } + base, pattern := glob.SplitPattern(p) + fsys := os.DirFS(base) + matches, err := glob.Glob(fsys, pattern) + if err != nil { + return err + } + log.Debugf("glob %s expanded to %v", p, matches) + for _, m := range matches { + configs = append(configs, path.Join(base, m)) + } } - defer file.Close() - content, err := io.ReadAll(file) - if err != nil { - return err + if len(configs) == 0 { + return fmt.Errorf("no configuration files found") } - subst, err := envsubst.Bytes(content) - if err != nil { - return err - } + log.Debugf("%d potential configuration files found", len(configs)) - log.Debugf("Loaded configuration:\n%s", subst) + manifestReader := &manifest.Reader{} - c := &v1beta1.Cluster{} - if err := yaml.UnmarshalStrict(subst, c); err != nil { - return err - } + for _, f := range configs { + file, err := configReader(f) + if err != nil { + return err + } + defer file.Close() - m, err := yaml.Marshal(c) - if err == nil { - log.Tracef("unmarshaled configuration:\n%s", m) + content, err := io.ReadAll(file) + if err != nil { + return err + } + + subst, err := envsubst.Bytes(content) + if err != nil { + return err + } + + log.Debugf("Loaded configuration from %s:\n%s", f, subst) + + if err := manifestReader.ParseBytes(subst); err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } } - if err := c.Validate(); err != nil { - return fmt.Errorf("configuration validation failed: %w", err) + if manifestReader.Len() == 0 { + return fmt.Errorf("no resource definition manifests found in configuration files") } - ctx.Context = context.WithValue(ctx.Context, ctxConfigKey{}, c) + ctx.Context = context.WithValue(ctx.Context, ctxConfigsKey{}, manifestReader) return nil } @@ -181,13 +213,64 @@ return nil } +func readConfig(ctx *cli.Context) (*v1beta1.Cluster, error) { + mr, err := ManifestReader(ctx.Context) + if err != nil { + return nil, fmt.Errorf("failed to get manifest reader: %w", err) + } + ctlConfigs, err := mr.GetResources(v1beta1.APIVersion, "Cluster") + if err != nil { + return nil, fmt.Errorf("failed to get cluster resources: %w", err) + } + if len(ctlConfigs) != 1 { + return nil, fmt.Errorf("expected exactly one cluster config, got %d", len(ctlConfigs)) + } + cfg := &v1beta1.Cluster{} + if err := ctlConfigs[0].Unmarshal(cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal cluster config: %w", err) + } + if k0sConfigs, err := mr.GetResources("k0s.k0sproject.io/v1beta1", "ClusterConfig"); err == nil && len(k0sConfigs) > 0 { + for _, k0sConfig := range k0sConfigs { + k0s := make(dig.Mapping) + log.Debugf("unmarshalling %d bytes of config from %v", len(k0sConfig.Raw), k0sConfig.Filename()) + if err := k0sConfig.Unmarshal(&k0s); err != nil { + return nil, fmt.Errorf("failed to unmarshal k0s config: %w", err) + } + log.Debugf("merging in k0s config from %v", k0sConfig.Filename()) + cfg.Spec.K0s.Config.Merge(k0s) + } + } + otherConfigs := mr.FilterResources(func(rd *manifest.ResourceDefinition) bool { + if strings.EqualFold(rd.APIVersion, v1beta1.APIVersion) && strings.EqualFold(rd.Kind, "cluster") { + return false + } + if strings.EqualFold(rd.APIVersion, "k0s.k0sproject.io/v1beta1") && strings.EqualFold(rd.Kind, "clusterconfig") { + return false + } + return true + }) + if len(otherConfigs) > 0 { + cfg.Metadata.Manifests = make(map[string][]byte) + log.Debugf("found %d additional resources in the configuration", len(otherConfigs)) + for _, otherConfig := range otherConfigs { + log.Debugf("found resource: %s (%d bytes)", otherConfig.Filename(), len(otherConfig.Raw)) + cfg.Metadata.Manifests[otherConfig.Filename()] = otherConfig.Raw + } + } + + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("cluster config validation failed: %w", err) + } + return cfg, nil +} + func initManager(ctx *cli.Context) error { - c, ok := ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster) - if c == nil || !ok { - return fmt.Errorf("cluster config not available in context") + cfg, err := readConfig(ctx) + if err != nil { + return err } - manager, err := phase.NewManager(c) + manager, err := phase.NewManager(cfg) if err != nil { return fmt.Errorf("failed to initialize phase manager: %w", err) } @@ -382,3 +465,18 @@ fmt.Print(logo) return nil } + +// ManifestReader returns a manifest reader from context +func ManifestReader(ctx context.Context) (*manifest.Reader, error) { + if ctx == nil { + return nil, fmt.Errorf("context is nil") + } + v := ctx.Value(ctxConfigsKey{}) + if v == nil { + return nil, fmt.Errorf("config reader not found in context") + } + if r, ok := v.(*manifest.Reader); ok { + return r, nil + } + return nil, fmt.Errorf("config reader in context is not of the correct type") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/go.mod new/k0sctl-0.22.0/go.mod --- old/k0sctl-0.21.0/go.mod 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/go.mod 2025-01-20 14:07:53.000000000 +0100 @@ -10,11 +10,11 @@ github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect github.com/a8m/envsubst v1.4.2 github.com/adrg/xdg v0.5.3 - github.com/bmatcuk/doublestar/v4 v4.7.1 + github.com/bmatcuk/doublestar/v4 v4.8.0 github.com/creasty/defaults v1.8.0 github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/hashicorp/go-version v1.7.0 // indirect - github.com/k0sproject/dig v0.3.1 + github.com/k0sproject/dig v0.4.0 github.com/k0sproject/rig v0.19.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect @@ -24,10 +24,10 @@ github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.5 - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,11 +35,11 @@ require ( github.com/alessio/shellescape v1.4.2 github.com/carlmjohnson/versioninfo v0.22.5 - github.com/go-playground/validator/v10 v10.23.0 + github.com/go-playground/validator/v10 v10.24.0 github.com/jellydator/validation v1.1.0 github.com/k0sproject/version v0.6.0 github.com/sergi/go-diff v1.3.1 - k8s.io/client-go v0.32.0 + k8s.io/client-go v0.32.1 ) require ( @@ -52,7 +52,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -88,7 +88,7 @@ golang.org/x/time v0.7.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apimachinery v0.32.0 // indirect + k8s.io/apimachinery v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/go.sum new/k0sctl-0.22.0/go.sum --- old/k0sctl-0.21.0/go.sum 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/go.sum 2025-01-20 14:07:53.000000000 +0100 @@ -19,8 +19,8 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b h1:baFN6AnR0SeC194X2D292IUZcHDs4JjStpqtE70fjXE= github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b/go.mod h1:Ram6ngyPDmP+0t6+4T2rymv0w0BS9N8Ch5vvUJccw5o= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= @@ -44,8 +44,8 @@ github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= -github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -60,8 +60,8 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -109,8 +109,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/k0sproject/dig v0.3.1 h1:/QK40lXQ/HEE3LMT3r/kST1ANhMVZiajNDXI+spbL9o= -github.com/k0sproject/dig v0.3.1/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4= +github.com/k0sproject/dig v0.4.0 h1:yBxFUUxNXAMGBg6b7c6ypxdx/o3RmhoI5v5ABOw5tn0= +github.com/k0sproject/dig v0.4.0/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4= github.com/k0sproject/rig v0.19.0 h1:aF/wJDfK45Ho2Z75Uap+u4Q4jHgr/1WfrHcOg2U9/n0= github.com/k0sproject/rig v0.19.0/go.mod h1:SNa9+xeVA6zQVYx+SINaa4ZihFPWrmo/6crHcdvJRFI= github.com/k0sproject/version v0.6.0 h1:Wi8wu9j+H36+okIQA47o/YHbzNpKeIYj8IjGdJOdqsI= @@ -202,8 +202,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -216,8 +216,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -236,13 +236,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -276,12 +276,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/phase/apply_manifests.go new/k0sctl-0.22.0/phase/apply_manifests.go --- old/k0sctl-0.21.0/phase/apply_manifests.go 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/phase/apply_manifests.go 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,69 @@ +package phase + +import ( + "bytes" + "fmt" + "io" + + "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1" + "github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster" + "github.com/k0sproject/rig/exec" + log "github.com/sirupsen/logrus" +) + +// ApplyManifests is a phase that applies additional manifests to the cluster +type ApplyManifests struct { + GenericPhase + leader *cluster.Host +} + +// Title for the phase +func (p *ApplyManifests) Title() string { + return "Apply additional manifests" +} + +// Prepare the phase +func (p *ApplyManifests) Prepare(config *v1beta1.Cluster) error { + p.Config = config + p.leader = p.Config.Spec.K0sLeader() + + return nil +} + +// ShouldRun is true when there are additional manifests to apply +func (p *ApplyManifests) ShouldRun() bool { + return len(p.Config.Metadata.Manifests) > 0 +} + +// Run the phase +func (p *ApplyManifests) Run() error { + for name, content := range p.Config.Metadata.Manifests { + if err := p.apply(name, content); err != nil { + return err + } + } + + return nil +} + +func (p *ApplyManifests) apply(name string, content []byte) error { + if !p.IsWet() { + p.DryMsgf(p.leader, "apply manifest %s (%d bytes)", name, len(content)) + return nil + } + + log.Infof("%s: apply manifest %s (%d bytes)", p.leader, name, len(content)) + kubectlCmd := p.leader.Configurer.KubectlCmdf(p.leader, p.leader.K0sDataDir(), "apply -f -") + var stdout, stderr bytes.Buffer + + cmd, err := p.leader.ExecStreams(kubectlCmd, io.NopCloser(bytes.NewReader(content)), &stdout, &stderr, exec.Sudo(p.leader)) + if err != nil { + return fmt.Errorf("failed to run apply for manifest %s: %w", name, err) + } + if err := cmd.Wait(); err != nil { + log.Errorf("%s: kubectl apply failed for manifest %s", p.leader, name) + log.Errorf("%s: kubectl apply stderr: %s", p.leader, stderr.String()) + } + log.Infof("%s: kubectl apply: %s", p.leader, stdout.String()) + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/phase/configure_k0s.go new/k0sctl-0.22.0/phase/configure_k0s.go --- old/k0sctl-0.21.0/phase/configure_k0s.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/phase/configure_k0s.go 2025-01-20 14:07:53.000000000 +0100 @@ -336,8 +336,10 @@ } } - if cfg.Dig("spec", "storage", "etcd", "peerAddress") != nil || h.PrivateAddress != "" { - cfg.DigMapping("spec", "storage", "etcd")["peerAddress"] = addr + if p.Config.StorageType() == "etcd" { + if cfg.Dig("spec", "storage", "etcd", "peerAddress") != nil || h.PrivateAddress != "" { + cfg.DigMapping("spec", "storage", "etcd")["peerAddress"] = addr + } } if _, ok := cfg["apiVersion"]; !ok { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/phase/validate_etcd_members.go new/k0sctl-0.22.0/phase/validate_etcd_members.go --- old/k0sctl-0.21.0/phase/validate_etcd_members.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/phase/validate_etcd_members.go 2025-01-20 14:07:53.000000000 +0100 @@ -42,13 +42,10 @@ return false } - if len(p.Config.Spec.K0s.Config) > 0 { - storageType := p.Config.Spec.K0s.Config.DigString("spec", "storage", "type") - if storageType != "" && storageType != "etcd" { - log.Debugf("%s: storage type is %q, not k0s managed etcd", p.Config.Spec.K0sLeader(), storageType) - return false - } + if s := p.Config.StorageType(); s != "etcd" { + log.Debugf("%s: storage type is %q, not k0s managed etcd", p.Config.Spec.K0sLeader(), s) } + return len(p.hosts) > 0 } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go new/k0sctl-0.22.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go --- old/k0sctl-0.21.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go 2025-01-20 14:07:53.000000000 +0100 @@ -14,10 +14,11 @@ // ClusterMetadata defines cluster metadata type ClusterMetadata struct { - Name string `yaml:"name" validate:"required" default:"k0s-cluster"` - User string `yaml:"user" default:"admin"` - Kubeconfig string `yaml:"-"` - EtcdMembers []string `yaml:"-"` + Name string `yaml:"name" validate:"required" default:"k0s-cluster"` + User string `yaml:"user" default:"admin"` + Kubeconfig string `yaml:"-"` + EtcdMembers []string `yaml:"-"` + Manifests map[string][]byte `yaml:"-"` } // Cluster describes launchpad.yaml configuration @@ -58,3 +59,26 @@ validation.Field(&c.Spec), ) } + +// StorageType returns the k0s storage type. +func (c *Cluster) StorageType() string { + if c.Spec == nil { + // default to etcd when there's no hosts or k0s spec, this should never happen. + return "etcd" + } + + if c.Spec.K0s != nil { + if t := c.Spec.K0s.Config.DigString("spec", "storage", "type"); t != "" { + // if storage type is set in k0s spec, return it + return t + } + } + + if h := c.Spec.K0sLeader(); h != nil && h.Role == "single" { + // default to "kine" on single node clusters + return "kine" + } + + // default to etcd otherwise + return "etcd" +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/pkg/manifest/reader.go new/k0sctl-0.22.0/pkg/manifest/reader.go --- old/k0sctl-0.21.0/pkg/manifest/reader.go 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/pkg/manifest/reader.go 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,182 @@ +package manifest + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path" + "regexp" + "strings" + "time" + + "gopkg.in/yaml.v2" +) + +// ResourceDefinition represents a single Kubernetes resource definition. +type ResourceDefinition struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + } `yaml:"metadata"` + Origin string `yaml:"-"` + Raw []byte `yaml:"-"` +} + +var fnRe = regexp.MustCompile(`[^\w\-\.]`) + +func safeFn(input string) string { + safe := fnRe.ReplaceAllString(input, "_") + safe = strings.Trim(safe, "._") + return safe +} + +// Filename returns a filename compatible name of the resource definition. +func (rd *ResourceDefinition) Filename() string { + if strings.HasSuffix(rd.Origin, ".yaml") || strings.HasSuffix(rd.Origin, ".yml") { + return path.Base(rd.Origin) + } + + if rd.Metadata.Name != "" { + return fmt.Sprintf("%s-%s.yaml", safeFn(rd.Kind), safeFn(rd.Metadata.Name)) + } + + return fmt.Sprintf("%s-%s-%d.yaml", safeFn(rd.APIVersion), safeFn(rd.Kind), time.Now().UnixNano()) +} + +// returns a Reader that reads the raw resource definition +func (rd *ResourceDefinition) Reader() *bytes.Reader { + return bytes.NewReader(rd.Raw) +} + +// Bytes returns the raw resource definition. +func (rd *ResourceDefinition) Bytes() []byte { + return rd.Raw +} + +// Unmarshal unmarshals the raw resource definition into the provided object. +func (rd *ResourceDefinition) Unmarshal(obj any) error { + if err := yaml.UnmarshalStrict(rd.Bytes(), obj); err != nil { + return fmt.Errorf("failed to unmarshal %s: %w", rd.Origin, err) + } + return nil +} + +func yamlDocumentSplit(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + // Look for the document separator + sepIndex := bytes.Index(data, []byte("\n---")) + if sepIndex >= 0 { + // Return everything up to the separator + return sepIndex + len("\n---"), data[:sepIndex], nil + } + + // If at EOF, return the remaining data + if atEOF { + return len(data), data, nil + } + + // Request more data + return 0, nil, nil +} + +// Reader reads Kubernetes resource definitions from input streams. +type Reader struct { + IgnoreErrors bool + manifests []*ResourceDefinition +} + +func name(r io.Reader) string { + if n, ok := r.(*os.File); ok { + return n.Name() + } + return "manifest" +} + +// Parse parses Kubernetes resource definitions from the provided input stream. They are then available via the Resources() or GetResources(apiVersion, kind) methods. +func (r *Reader) Parse(input io.Reader) error { + scanner := bufio.NewScanner(input) + scanner.Split(yamlDocumentSplit) + + for scanner.Scan() { + rawChunk := scanner.Bytes() + + // Skip empty chunks + if len(rawChunk) == 0 { + continue + } + + rd := &ResourceDefinition{} + if err := yaml.Unmarshal(rawChunk, rd); err != nil { + if r.IgnoreErrors { + continue + } + return fmt.Errorf("failed to decode resource %s: %w", name(input), err) + } + + if rd.APIVersion == "" || rd.Kind == "" { + if r.IgnoreErrors { + continue + } + return fmt.Errorf("missing apiVersion or kind in resource %s", name(input)) + } + + // Store the raw chunk + rd.Raw = append([]byte{}, rawChunk...) + r.manifests = append(r.manifests, rd) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading input: %w", err) + } + + return nil +} + +// ParseString parses Kubernetes resource definitions from the provided string. +func (r *Reader) ParseString(input string) error { + return r.Parse(strings.NewReader(input)) +} + +// ParseBytes parses Kubernetes resource definitions from the provided byte slice. +func (r *Reader) ParseBytes(input []byte) error { + return r.Parse(bytes.NewReader(input)) +} + +// Resources returns all parsed Kubernetes resource definitions. +func (r *Reader) Resources() []*ResourceDefinition { + return r.manifests +} + +// Len returns the number of parsed Kubernetes resource definitions. +func (r *Reader) Len() int { + return len(r.manifests) +} + +// FilterResources returns all parsed Kubernetes resource definitions that match the provided filter function. +func (r *Reader) FilterResources(filter func(rd *ResourceDefinition) bool) []*ResourceDefinition { + var resources []*ResourceDefinition + for _, rd := range r.manifests { + if filter(rd) { + resources = append(resources, rd) + } + } + return resources +} + +// GetResources returns all parsed Kubernetes resource definitions that match the provided apiVersion and kind. The matching is case-insensitive. +func (r *Reader) GetResources(apiVersion, kind string) ([]*ResourceDefinition, error) { + resources := r.FilterResources(func(rd *ResourceDefinition) bool { + return strings.EqualFold(rd.APIVersion, apiVersion) && strings.EqualFold(rd.Kind, kind) + }) + + if len(resources) == 0 { + return nil, fmt.Errorf("no resources found for apiVersion=%s, kind=%s", apiVersion, kind) + } + return resources, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/pkg/manifest/reader_test.go new/k0sctl-0.22.0/pkg/manifest/reader_test.go --- old/k0sctl-0.21.0/pkg/manifest/reader_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/pkg/manifest/reader_test.go 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,151 @@ +package manifest_test + +import ( + "strings" + "testing" + + "github.com/k0sproject/k0sctl/pkg/manifest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReader_ParseIgnoreErrors(t *testing.T) { + input := ` +apiVersion: v1 +kind: Pod +metadata: + name: pod1 +--- +invalid_yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: service1 +` + reader := strings.NewReader(input) + r := &manifest.Reader{IgnoreErrors: true} + + err := r.Parse(reader) + + // Ensure no critical errors even with invalid YAML + require.NoError(t, err, "Parse should not return an error with IgnoreErrors=true") + + // Assert that only valid manifests are parsed + require.Equal(t, 2, r.Len(), "Expected 2 valid manifests to be parsed") + + // Validate the parsed manifests + assert.Equal(t, "v1", r.Resources()[0].APIVersion, "Unexpected apiVersion for Pod") + assert.Equal(t, "Pod", r.Resources()[0].Kind, "Unexpected kind for Pod") + assert.Equal(t, "v1", r.Resources()[1].APIVersion, "Unexpected apiVersion for Service") + assert.Equal(t, "Service", r.Resources()[1].Kind, "Unexpected kind for Service") +} + +func TestReader_ParseMultipleReaders(t *testing.T) { + input1 := ` +apiVersion: v1 +kind: Pod +metadata: + name: pod1 +` + input2 := ` +apiVersion: v1 +kind: Service +metadata: + name: service1 +` + r := &manifest.Reader{} + + // Parse first reader + err := r.Parse(strings.NewReader(input1)) + require.NoError(t, err, "Parse should not return an error for input1") + + // Parse second reader + err = r.Parse(strings.NewReader(input2)) + require.NoError(t, err, "Parse should not return an error for input2") + + // Assert that both manifests are parsed + require.Equal(t, 2, r.Len(), "Expected 2 manifests to be parsed") + + // Validate the parsed manifests + pod := r.Resources()[0] + assert.Equal(t, "v1", pod.APIVersion, "Unexpected apiVersion for Pod") + assert.Equal(t, "Pod", pod.Kind, "Unexpected kind for Pod") + require.Len(t, pod.Raw, len(input1)) + + service := r.Resources()[1] + assert.Equal(t, "v1", service.APIVersion, "Unexpected apiVersion for Service") + assert.Equal(t, "Service", service.Kind, "Unexpected kind for Service") + require.Len(t, service.Raw, len(input2)) +} + +func TestReader_FilterResources(t *testing.T) { + input := ` +apiVersion: v1 +kind: Pod +metadata: + name: pod1 +--- +apiVersion: v1 +kind: Service +metadata: + name: service1 +--- +apiVersion: v2 +kind: Pod +metadata: + name: pod2 +` + r := &manifest.Reader{} + require.NoError(t, r.Parse(strings.NewReader(input))) + v1Pods := r.FilterResources(func(rd *manifest.ResourceDefinition) bool { + return rd.APIVersion == "v1" && rd.Kind == "Pod" + }) + v2Pods := r.FilterResources(func(rd *manifest.ResourceDefinition) bool { + return rd.APIVersion == "v2" && rd.Kind == "Pod" + }) + assert.Len(t, v1Pods, 1, "Expected 2 v1 Pod to be returned") + assert.Len(t, v2Pods, 1, "Expected 1 v2 Pod to be returned") + assert.Equal(t, "pod1", v1Pods[0].Metadata.Name, "Unexpected name for v1 Pod") + assert.Equal(t, "pod2", v2Pods[0].Metadata.Name, "Unexpected name for v2 Pod") + assert.NotEmpty(t, v1Pods[0].Raw, "Expected raw data to be populated") + assert.NotEmpty(t, v2Pods[0].Raw, "Expected raw data to be populated") +} + +func TestReader_GetResources(t *testing.T) { + input := ` +apiVersion: v1 +kind: Pod +metadata: + name: pod1 +--- +apiVersion: v1 +kind: Service +metadata: + name: service1 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +` + reader := strings.NewReader(input) + r := &manifest.Reader{} + + err := r.Parse(reader) + require.NoError(t, err, "Parse should not return an error") + + // Query for Pods + pods, err := r.GetResources("v1", "Pod") + require.NoError(t, err, "GetResources should not return an error for Pods") + assert.Len(t, pods, 2, "Expected 2 Pods to be returned") + + // Validate Pods + assert.Equal(t, "Pod", pods[0].Kind, "Unexpected kind for the first Pod") + assert.Equal(t, "Pod", pods[1].Kind, "Unexpected kind for the second Pod") + + // Query for Services + services, err := r.GetResources("v1", "Service") + require.NoError(t, err, "GetResources should not return an error for Services") + assert.Len(t, services, 1, "Expected 1 Service to be returned") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/smoke-test/Makefile new/k0sctl-0.22.0/smoke-test/Makefile --- old/k0sctl-0.21.0/smoke-test/Makefile 2024-12-13 10:36:00.000000000 +0100 +++ new/k0sctl-0.22.0/smoke-test/Makefile 2025-01-20 14:07:53.000000000 +0100 @@ -61,5 +61,9 @@ smoke-controller-swap: $(bootloose) id_rsa_k0s k0sctl BOOTLOOSE_TEMPLATE=bootloose-controller-swap.yaml.tpl K0SCTL_CONFIG=k0sctl-controller-swap.yaml ./smoke-controller-swap.sh +smoke-multidoc: $(bootloose) id_rsa_k0s k0sctl + ./smoke-multidoc.sh + + %.iid: Dockerfile.% docker build --iidfile '$@' - < '$<' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/smoke-test/multidoc/k0sctl-multidoc-1.yaml new/k0sctl-0.22.0/smoke-test/multidoc/k0sctl-multidoc-1.yaml --- old/k0sctl-0.21.0/smoke-test/multidoc/k0sctl-multidoc-1.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/smoke-test/multidoc/k0sctl-multidoc-1.yaml 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,25 @@ +apiVersion: k0sctl.k0sproject.io/v1beta1 +kind: cluster +spec: + hosts: + - role: controller + uploadBinary: true + os: "$OS_OVERRIDE" + ssh: + address: "127.0.0.1" + port: 9022 + keyPath: ./id_rsa_k0s + - role: worker + uploadBinary: true + os: "$OS_OVERRIDE" + ssh: + address: "127.0.0.1" + port: 9023 + keyPath: ./id_rsa_k0s + k0s: + version: "${K0S_VERSION}" + config: + spec: + telemetry: + enabled: false + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/smoke-test/multidoc/k0sctl-multidoc-2.yaml new/k0sctl-0.22.0/smoke-test/multidoc/k0sctl-multidoc-2.yaml --- old/k0sctl-0.21.0/smoke-test/multidoc/k0sctl-multidoc-2.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/smoke-test/multidoc/k0sctl-multidoc-2.yaml 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,17 @@ +apiVersion: k0s.k0sproject.io/v1beta1 +kind: clusterconfig +spec: + extensions: + helm: + concurrencyLevel: 5 +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - name: hello + image: nginx:alpine + ports: + - containerPort: 80 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.21.0/smoke-test/smoke-multidoc.sh new/k0sctl-0.22.0/smoke-test/smoke-multidoc.sh --- old/k0sctl-0.21.0/smoke-test/smoke-multidoc.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/k0sctl-0.22.0/smoke-test/smoke-multidoc.sh 2025-01-20 14:07:53.000000000 +0100 @@ -0,0 +1,64 @@ +#!/usr/bin/env sh + +K0SCTL_CONFIG=${K0SCTL_CONFIG:-"k0sctl.yaml"} + +set -e + + +. ./smoke.common.sh +trap cleanup EXIT + +deleteCluster +createCluster + +remoteCommand() { + local userhost="$1" + shift + bootloose ssh "${userhost}" -- "$@" +} + +echo "* Starting apply" +../k0sctl apply --config multidoc/ --kubeconfig-out applykubeconfig --debug +echo "* Apply OK" + +echo "* Downloading kubectl for local test" +downloadKubectl + +export KUBECONFIG=applykubeconfig + +echo "*Waiting until the test pod is running" +./kubectl wait --for=condition=Ready pod/hello --timeout=120s + +retries=10 +delay=2 +nginx_ready=false +i=1 + +while [ "$i" -le "$retries" ]; do + echo "* Attempt $i: Checking if nginx is ready..." + if kubectl exec pod/hello -- curl -s http://localhost/ | grep -q "Welcome to nginx!"; then + echo "nginx is ready!" + nginx_ready=true + break + fi + echo " - nginx is not ready" + sleep $delay + i=$((i + 1)) +done + +if [ "$nginx_ready" = false ]; then + echo "nginx failed to become ready after $retries attempts." + exit 1 +fi + +echo " - nginx is ready" + +remoteCommand root@manager0 "cat /etc/k0s/k0s.yaml" > k0syaml +echo Resulting k0s.yaml: +cat k0syaml +echo "* Verifying config merging works" +grep -q "concurrencyLevel: 5" k0syaml +grep -q "enabled: false" k0syaml + +echo "* Done" + ++++++ k0sctl.obsinfo ++++++ --- /var/tmp/diff_new_pack.XS14cY/_old 2025-01-22 16:39:03.200690276 +0100 +++ /var/tmp/diff_new_pack.XS14cY/_new 2025-01-22 16:39:03.204690441 +0100 @@ -1,5 +1,5 @@ name: k0sctl -version: 0.21.0 -mtime: 1734082560 -commit: 082a528e9a36a4a80819733ffee3881f7f06dd13 +version: 0.22.0 +mtime: 1737378473 +commit: 929602e5d57527e1bdf39d607555e3d031f2cf5e ++++++ vendor.tar.gz ++++++ ++++ 1961 lines of diff (skipped)