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)

Reply via email to