Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reading k0s config from a separate or multidoc YAML document #814

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 $@
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,26 @@ Embedded k0s cluster configuration. See [k0s configuration documentation](https:

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:
Expand Down
12 changes: 7 additions & 5 deletions action/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:")
Expand Down
2 changes: 1 addition & 1 deletion cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/config_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions cmd/config_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
}
Expand Down
146 changes: 113 additions & 33 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{}
)
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
8 changes: 4 additions & 4 deletions pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading