diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 584f372e..d17672e5 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -192,6 +192,27 @@ jobs: - name: Run OS override smoke test run: make smoke-os-override + smoke-downloadurl: + name: k0sDownloadURL smoke test + needs: build + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + check-latest: true + + - {"name":"Compiled binary cache","uses":"actions/download-artifact@v4","with":{"name":"k0sctl","path":"."}} + - {"name":"K0sctl cache","uses":"actions/cache@v3","with":{"path":"/var/cache/k0sctl/k0s\n~/.cache/k0sctl/k0s\n","key":"k0sctl-cache"}} + - {"name":"Kubectl cache","uses":"actions/cache@v3","with":{"path":"smoke-test/kubectl\n","key":"kubectl-${{ hashFiles('smoke-test/smoke.common.sh') }}","restore-keys":"kubectl-"}} + - {"name":"Make binaries executable","run":"chmod +x k0sctl || true\nchmod +x smoke-test/kubectl || true"} + + - name: Run k0sDownloadURL smoke test + run: make smoke-downloadurl + smoke-upgrade: strategy: matrix: diff --git a/Makefile b/Makefile index 99d742c8..d9f3c671 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-files smoke-upgrade smoke-reset smoke-os-override smoke-init smoke-backup-restore smoke-dynamic smoke-basic-openssh smoke-dryrun +smoketests := smoke-basic smoke-files smoke-upgrade smoke-reset smoke-os-override smoke-init smoke-backup-restore smoke-dynamic smoke-basic-openssh smoke-dryrun smoke-downloadurl .PHONY: $(smoketests) $(smoketests): k0sctl $(MAKE) -C smoke-test $@ diff --git a/README.md b/README.md index d8fd7fb4..f6469323 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,10 @@ When `false`, the k0s binary downloading is performed on the target host itself A path to a file on the local host that contains a k0s binary to be uploaded to the host. Can be used to test drive a custom development build of k0s. +###### `spec.hosts[*].k0sDownloadURL` <string> (optional) + +A URL to download the k0s binary from. The default is to download from the [k0s repository](https://github.com/k0sproject/k0s). The URL can contain '%'-prefixed tokens that will be replaced with the host's information, see [tokens](#tokens). + ###### `spec.hosts[*].hostname` <string> (optional) Override host's hostname. When not set, the hostname reported by the operating system is used. @@ -341,7 +345,7 @@ Example: ``` * `name`: name of the file "bundle", used only for logging purposes (optional) -* `src`: File path, an URL or [Glob pattern](https://golang.org/pkg/path/filepath/#Match) to match files to be uploaded. URL sources will be directly downloaded using the target host (required) +* `src`: File path, an URL or [Glob pattern](https://golang.org/pkg/path/filepath/#Match) to match files to be uploaded. URL sources will be directly downloaded using the target host. If the value is a URL, '%'-prefixed tokens can be used, see [tokens](#tokens). (required) * `dstDir`: Destination directory for the file(s). `k0sctl` will create full directory structure if it does not already exist on the host (default: user home) * `dst`: Destination filename for the file. Only usable for single file uploads (default: basename of file) * `perm`: File permission mode for uploaded file(s) (default: same as local) @@ -595,3 +599,23 @@ See also: Embedded k0s cluster configuration. See [k0s configuration documentation](https://docs.k0sproject.io/main/configuration/) for details. When left out, the output of `k0s config create` will be used. + +#### Tokens + +The following tokens can be used in the `k0sDownloadURL` and `files.[*].src` fields: + +- `%%` - literal `%` +- `%p` - host architecture (arm, arm64, amd64) +- `%v` - k0s version (v1.21.0+k0s.0) +- `%x` - k0s binary extension (currently always empty) + +Any other tokens will be output as-is including the `%` character. + +Example: + +```yaml + - role: controller + k0sDownloadURL: https://files.example.com/k0s%20files/k0s-%v-%p%x + # Expands to https://files.example.com/k0s%20files/k0s-v1.21.0+k0s.0-amd64 +``` + diff --git a/phase/download_k0s.go b/phase/download_k0s.go index 0cbbea6c..4dca3af7 100644 --- a/phase/download_k0s.go +++ b/phase/download_k0s.go @@ -65,9 +65,16 @@ func (p *DownloadK0s) downloadK0s(h *cluster.Host) error { } log.Infof("%s: downloading k0s %s", h, p.Config.Spec.K0s.Version) - if err := h.Configurer.DownloadK0s(h, tmp, p.Config.Spec.K0s.Version, h.Metadata.Arch); err != nil { + if h.K0sDownloadURL != "" { + expandedURL := h.ExpandTokens(h.K0sDownloadURL, p.Config.Spec.K0s.Version) + log.Infof("%s: downloading k0s binary from %s", h, expandedURL) + if err := h.Configurer.DownloadURL(h, expandedURL, tmp); err != nil { + return fmt.Errorf("failed to download k0s binary: %w", err) + } + } else if err := h.Configurer.DownloadK0s(h, tmp, p.Config.Spec.K0s.Version, h.Metadata.Arch); err != nil { return err } + if err := h.Execf(`chmod +x "%s"`, tmp, exec.Sudo(h)); err != nil { logrus.Warnf("%s: failed to chmod k0s temp binary: %v", h, err.Error()) } diff --git a/phase/uploadfiles.go b/phase/uploadfiles.go index 717db2e1..ab967488 100644 --- a/phase/uploadfiles.go +++ b/phase/uploadfiles.go @@ -168,9 +168,9 @@ func (p *UploadFiles) uploadURL(h *cluster.Host, f *cluster.UploadFile) error { return err } - err := p.Wet(h, fmt.Sprintf("download file %s => %s", f.Source, f.DestinationFile), func() error { - - return h.Configurer.DownloadURL(h, f.Source, f.DestinationFile, exec.Sudo(h)) + expandedURL := h.ExpandTokens(f.Source, p.Config.Spec.K0s.Version) + err := p.Wet(h, fmt.Sprintf("download file %s => %s", expandedURL, f.DestinationFile), func() error { + return h.Configurer.DownloadURL(h, expandedURL, f.DestinationFile, exec.Sudo(h)) }) if err != nil { return err diff --git a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.go b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.go index 8ecaf0c9..c5effb5f 100644 --- a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.go +++ b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.go @@ -2,6 +2,7 @@ package cluster import ( "fmt" + "net/url" gos "os" gopath "path" "regexp" @@ -36,6 +37,7 @@ type Host struct { Environment map[string]string `yaml:"environment,flow,omitempty"` UploadBinary bool `yaml:"uploadBinary,omitempty"` K0sBinaryPath string `yaml:"k0sBinaryPath,omitempty"` + K0sDownloadURL string `yaml:"k0sDownloadURL,omitempty"` InstallFlags Flags `yaml:"installFlags,omitempty"` Files []*UploadFile `yaml:"files,omitempty"` OSIDOverride string `yaml:"os,omitempty"` @@ -448,7 +450,6 @@ func (h *Host) CheckHTTPStatus(url string, expected ...int) error { } return fmt.Errorf("expected response code %d but received %d", expected, status) - } // NeedCurl returns true when the curl package is needed on the host @@ -515,3 +516,55 @@ func (h *Host) FileChanged(lpath, rpath string) bool { return false } + +// ExpandTokens expands percent-sign prefixed tokens in a string, mainly for the download URLs. +// The supported tokens are: +// +// - %% - literal % +// - %p - host architecture (arm, arm64, amd64) +// - %v - k0s version (v1.21.0+k0s.0) +// - %x - k0s binary extension (.exe on Windows) +// +// Any unknown token is output as-is with the leading % included. +func (h *Host) ExpandTokens(input string, k0sVersion *version.Version) string { + if input == "" { + return "" + } + builder := strings.Builder{} + var inPercent bool + for i := 0; i < len(input); i++ { + currCh := input[i] + if inPercent { + inPercent = false + switch currCh { + case '%': + // Literal %. + builder.WriteByte('%') + case 'p': + // Host architecture (arm, arm64, amd64). + builder.WriteString(h.Metadata.Arch) + case 'v': + // K0s version (v1.21.0+k0s.0) + builder.WriteString(url.QueryEscape(k0sVersion.String())) + case 'x': + // K0s binary extension (.exe on Windows). + if h.IsConnected() && h.IsWindows() { + builder.WriteString(".exe") + } + default: + // Unknown token, just output it with the leading %. + builder.WriteByte('%') + builder.WriteByte(currCh) + } + } else if currCh == '%' { + inPercent = true + } else { + builder.WriteByte(currCh) + } + } + if inPercent { + // Trailing %. + builder.WriteByte('%') + } + return builder.String() +} diff --git a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host_test.go b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host_test.go index c6457568..5fcfcded 100644 --- a/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host_test.go +++ b/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host_test.go @@ -9,6 +9,7 @@ import ( "github.com/k0sproject/rig" "github.com/k0sproject/rig/exec" "github.com/k0sproject/rig/os" + "github.com/k0sproject/version" "github.com/stretchr/testify/require" ) @@ -144,3 +145,14 @@ func TestBinaryPath(t *testing.T) { h.Configurer.SetPath("K0sBinaryPath", "/foo/bar/k0s") require.Equal(t, "/foo/bar", h.k0sBinaryPathDir()) } + +func TestExpandTokens(t *testing.T) { + h := Host{ + Metadata: HostMetadata{ + Arch: "amd64", + }, + } + ver, err := version.NewVersion("v1.0.0+k0s.0") + require.NoError(t, err) + require.Equal(t, "test%20expand/k0s-v1.0.0%2Bk0s.0-amd64", h.ExpandTokens("test%20expand/k0s-%v-%p%x", ver)) +} diff --git a/smoke-test/Makefile b/smoke-test/Makefile index 5d910d5b..c352505d 100644 --- a/smoke-test/Makefile +++ b/smoke-test/Makefile @@ -46,6 +46,9 @@ smoke-reset: $(bootloose) id_rsa_k0s k0sctl smoke-os-override: $(bootloose) id_rsa_k0s k0sctl BOOTLOOSE_TEMPLATE=bootloose.yaml.osoverride.tpl K0SCTL_CONFIG=k0sctl-single.yaml OS_RELEASE_PATH=$(realpath os-release) OS_OVERRIDE="ubuntu" ./smoke-basic.sh +smoke-downloadurl: $(bootloose) id_rsa_k0s k0sctl + BOOTLOOSE_TEMPLATE=bootloose.yaml.single.tpl K0SCTL_CONFIG=k0sctl-downloadurl.yaml ./smoke-basic.sh + smoke-backup-restore: $(bootloose) id_rsa_k0s k0sctl ./smoke-backup-restore.sh diff --git a/smoke-test/bootloose.yaml.single.tpl b/smoke-test/bootloose.yaml.single.tpl new file mode 100644 index 00000000..423a87b0 --- /dev/null +++ b/smoke-test/bootloose.yaml.single.tpl @@ -0,0 +1,23 @@ +cluster: + name: k0s + privateKey: ./id_rsa_k0s +machines: +- count: 1 + backend: docker + spec: + image: quay.io/k0sproject/bootloose-ubuntu20.04 + name: manager%d + privileged: true + volumes: + - type: bind + source: /lib/modules + destination: /lib/modules + - type: volume + destination: /var/lib/k0s + portMappings: + - containerPort: 22 + hostPort: 9022 + - containerPort: 443 + hostPort: 443 + - containerPort: 6443 + hostPort: 6443 diff --git a/smoke-test/k0sctl-downloadurl.yaml b/smoke-test/k0sctl-downloadurl.yaml new file mode 100644 index 00000000..fe951caa --- /dev/null +++ b/smoke-test/k0sctl-downloadurl.yaml @@ -0,0 +1,21 @@ +apiVersion: k0sctl.k0sproject.io/v1beta1 +kind: cluster +spec: + hosts: + - role: single + k0sDownloadURL: https://github.com/k0sproject/k0s/releases/download/%v/k0s-%v-%p + ssh: + address: "127.0.0.1" + port: 9022 + keyPath: ./id_rsa_k0s + hooks: + apply: + before: + - "echo hello > apply.hook" + after: + - "grep -q hello apply.hook" + k0s: + config: + spec: + telemetry: + enabled: false