diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e01405e7..6ad7e169 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -108,6 +108,23 @@ jobs: 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 --git a/Makefile b/Makefile index dc0e9014..8bdaa128 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ build-all: $(addprefix bin/,$(bins)) bin/checksums.md 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 --git a/action/apply.go b/action/apply.go index d78cd16b..f9b54b91 100644 --- a/action/apply.go +++ b/action/apply.go @@ -35,8 +35,8 @@ type ApplyOptions struct { 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 { @@ -158,9 +158,11 @@ func (a Apply) Run() error { 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 --git a/cmd/apply.go b/cmd/apply.go index 7e6d079a..2fcce903 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -90,7 +90,7 @@ var applyCommand = &cli.Command{ 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 --git a/cmd/config_edit.go b/cmd/config_edit.go index f650d769..4c451cbd 100644 --- a/cmd/config_edit.go +++ b/cmd/config_edit.go @@ -19,7 +19,7 @@ var configEditCommand = &cli.Command{ 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 --git a/cmd/config_status.go b/cmd/config_status.go index f3d8378e..2a711c06 100644 --- a/cmd/config_status.go +++ b/cmd/config_status.go @@ -2,7 +2,6 @@ package cmd 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 @@ var configStatusCommand = &cli.Command{ }, 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 --git a/cmd/flags.go b/cmd/flags.go index f7c3a795..f331bdec 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -12,8 +12,11 @@ import ( "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 +25,10 @@ import ( "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 +60,11 @@ var ( 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 +117,73 @@ func actions(funcs ...func(*cli.Context) error) func(*cli.Context) error { // 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 +212,47 @@ func warnOldCache(_ *cli.Context) error { 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) + } + } + + 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 +447,18 @@ func displayLogo(_ *cli.Context) error { 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 --git a/go.mod b/go.mod index e012d59c..f506fb61 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( 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 diff --git a/go.sum b/go.sum index 750d7dd2..a3e0e0af 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm 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= diff --git a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go index b2c69ce7..6824c77a 100644 --- a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go +++ b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go @@ -14,10 +14,10 @@ const APIVersion = "k0sctl.k0sproject.io/v1beta1" // 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:"-"` } // Cluster describes launchpad.yaml configuration diff --git a/pkg/manifest/reader.go b/pkg/manifest/reader.go new file mode 100644 index 00000000..76f20129 --- /dev/null +++ b/pkg/manifest/reader.go @@ -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 --git a/pkg/manifest/reader_test.go b/pkg/manifest/reader_test.go new file mode 100644 index 00000000..005e3c8b --- /dev/null +++ b/pkg/manifest/reader_test.go @@ -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 --git a/smoke-test/Makefile b/smoke-test/Makefile index dc93d96e..657ef8a2 100644 --- a/smoke-test/Makefile +++ b/smoke-test/Makefile @@ -61,5 +61,9 @@ smoke-backup-restore: $(bootloose) id_rsa_k0s k0sctl 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 --git a/smoke-test/multidoc/k0sctl-multidoc-1.yaml b/smoke-test/multidoc/k0sctl-multidoc-1.yaml new file mode 100644 index 00000000..8820d762 --- /dev/null +++ b/smoke-test/multidoc/k0sctl-multidoc-1.yaml @@ -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 --git a/smoke-test/multidoc/k0sctl-multidoc-2.yaml b/smoke-test/multidoc/k0sctl-multidoc-2.yaml new file mode 100644 index 00000000..6bac97e8 --- /dev/null +++ b/smoke-test/multidoc/k0sctl-multidoc-2.yaml @@ -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 --git a/smoke-test/smoke-multidoc.sh b/smoke-test/smoke-multidoc.sh new file mode 100755 index 00000000..5f4ac910 --- /dev/null +++ b/smoke-test/smoke-multidoc.sh @@ -0,0 +1,32 @@ +#!/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" + +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" +