Skip to content
Open
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
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,9 @@ metadata:

A list of cluster hosts. Host requirements:

* Currently only linux targets are supported
* The user must either be root or have passwordless `sudo` access.
* Linux nodes are supported for all roles.
* Windows nodes can join as `worker` hosts when reachable over SSH or WinRM. This support is experimental and requires k0s version >= 1.34.
* On Linux, the SSH user must either be root or have passwordless `sudo` (or `doas`) access. Windows workers must allow WinRM access for the configured user (defaults to `Administrator`).
* The host must fulfill the k0s system requirements

See [host object documentation](#host-fields) below.
Expand Down Expand Up @@ -463,6 +464,10 @@ Useful in case fact gathering picks the wrong private network interface.
Override private IP address selected by host fact gathering.
Useful in case fact gathering picks the wrong IPAddress.

##### `spec.hosts[*].reset` <boolean> (optional) (default: `false`)

If set to `true` k0sctl will remove the node from kubernetes and reset k0s on the host.

```yaml
- role: worker
os: debian
Expand All @@ -485,7 +490,9 @@ spec:
keyPath: ~/.ssh/id_rsa
```

It's also possible to tunnel connections through a bastion host. The bastion configuration has all the same fields as any SSH connection:
Windows worker nodes can also use the SSH transport when an SSH server is available on the host.

It's also possible to tunnel connections over SSH through a bastion host. The bastion configuration has all the same fields as any SSH connection:

```yaml
spec:
Expand Down Expand Up @@ -621,9 +628,71 @@ openSSH:
StrictHostkeyChecking: false # -o StrictHostkeyChecking: no
```

###### `spec.hosts[*].reset` <boolean> (optional) (default: `false`)
##### `spec.hosts[*].winrm` <mapping> (optional)

If set to `true` k0sctl will remove the node from kubernetes and reset k0s on the host.
WinRM connection options for Windows worker nodes. Use this transport when targeting Windows hosts that prefer WinRM instead of SSH. Windows support is limited to the `worker` role and requires k0s version >= 1.34.

Example:

```yaml
spec:
hosts:
- role: worker
winrm:
address: win-worker-1.internal
user: Administrator
password: ${WINRM_PASSWORD}
useHTTPS: true
insecure: false
```

###### `spec.hosts[*].winrm.address` <string> (required)

IP address or hostname of the host.

###### `spec.hosts[*].winrm.user` <string> (optional) (default: `Administrator`)

WinRM user name. The user must have administrative privileges. Windows does not provide a built-in way to elevate privileges over WinRM, so the user must already have them.

###### `spec.hosts[*].winrm.port` <number> (optional) (default: `5985`)

TCP port for the WinRM endpoint. When `useHTTPS` is `true`, the default port automatically switches to `5986`.

###### `spec.hosts[*].winrm.password` <string> (optional)

Password for the WinRM user. Required unless certificate-based authentication is configured. Consider using environment variable substitution to avoid storing plaintext passwords in the configuration file.

###### `spec.hosts[*].winrm.useHTTPS` <boolean> (optional) (default: `false`)

Enable HTTPS for WinRM. When enabled, set `caCertPath` (and optionally `certPath`/`keyPath`) to verify the remote endpoint.

###### `spec.hosts[*].winrm.insecure` <boolean> (optional) (default: `false`)

Skip TLS certificate verification when connecting over HTTPS. Use only in trusted environments.

###### `spec.hosts[*].winrm.useNTLM` <boolean> (optional) (default: `false`)

Use NTLM authentication instead of basic authentication.

###### `spec.hosts[*].winrm.caCertPath` <string> (optional)

Path to a CA bundle used to validate the WinRM server certificate.

###### `spec.hosts[*].winrm.certPath` <string> (optional)

Client certificate for mutual TLS authentication.

###### `spec.hosts[*].winrm.keyPath` <string> (optional)

Private key that matches `certPath`.

###### `spec.hosts[*].winrm.tlsServerName` <string> (optional)

Override the TLS server name used during certificate verification.

###### `spec.hosts[*].winrm.bastion` <mapping> (optional)

SSH connection details for a bastion host to reach the host. The fields match those documented under `spec.hosts[*].ssh`.

### K0s Fields

Expand Down
16 changes: 8 additions & 8 deletions action/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ type Apply struct {
// gatherK0sFacts := &phase.GatherK0sFacts{} // advisable to get the title from the phase itself instead of hardcoding the title.
// apply.Phases.InsertBefore(gatherK0sFacts.Title(), &myCustomPhase{}) // insert a custom phase before the GatherK0sFacts phase
func NewApply(opts ApplyOptions) *Apply {
lockPhase := &phase.Lock{}
unlockPhase := lockPhase.UnlockPhase()
// lockPhase := &phase.Lock{}
// unlockPhase := lockPhase.UnlockPhase()
apply := &Apply{
ApplyOptions: opts,
Phases: phase.Phases{
&phase.DefaultK0sVersion{},
&phase.Connect{},
&phase.DetectOS{},
lockPhase,
&phase.Connect{},
&phase.DetectOS{},
// lockPhase,
&phase.PrepareHosts{},
&phase.GatherFacts{},
&phase.ValidateHosts{},
Expand Down Expand Up @@ -90,13 +90,13 @@ func NewApply(opts ApplyOptions) *Apply {
&phase.ResetControllers{NoDrain: opts.NoDrain},
&phase.RunHooks{Stage: "after", Action: "apply"},
&phase.ApplyManifests{},
unlockPhase,
&phase.Disconnect{},
// unlockPhase,
},
}
if opts.KubeconfigOut != nil {
apply.Phases.InsertBefore(unlockPhase.Title(), &phase.GetKubeconfig{APIAddress: opts.KubeconfigAPIAddress, User: opts.KubeconfigUser, Cluster: opts.KubeconfigCluster})
apply.Phases = append(apply.Phases, &phase.GetKubeconfig{APIAddress: opts.KubeconfigAPIAddress, User: opts.KubeconfigUser, Cluster: opts.KubeconfigCluster})
}
apply.Phases = append(apply.Phases, &phase.Disconnect{})

return apply
}
Expand Down
65 changes: 65 additions & 0 deletions configurer/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package configurer

import (
"time"

"github.com/k0sproject/rig/exec"
"github.com/k0sproject/rig/os"
)

// Configurer defines the per-host operations required for managing a host.
type Configurer interface {
Kind() string
OSKind() string
Quote(string) string
CheckPrivilege(os.Host) error
StartService(os.Host, string) error
StopService(os.Host, string) error
RestartService(os.Host, string) error
ServiceIsRunning(os.Host, string) bool
Arch(os.Host) (string, error)
K0sCmdf(string, ...interface{}) string
K0sBinaryPath() string
K0sConfigPath() string
DataDirDefaultPath() string
K0sJoinTokenPath() string
WriteFile(os.Host, string, string, string) error
UpdateEnvironment(os.Host, map[string]string) error
DaemonReload(os.Host) error
ReplaceK0sTokenPath(os.Host, string) error
ServiceScriptPath(os.Host, string) (string, error)
ReadFile(os.Host, string) (string, error)
FileExist(os.Host, string) bool
Chmod(os.Host, string, string, ...exec.Option) error
Chown(os.Host, string, string, ...exec.Option) error
DownloadURL(os.Host, string, string, ...exec.Option) error
InstallPackage(os.Host, ...string) error
FileContains(os.Host, string, string) bool
MoveFile(os.Host, string, string) error
MkDir(os.Host, string, ...exec.Option) error
DeleteFile(os.Host, string) error
CommandExist(os.Host, string) bool
Hostname(os.Host) string
KubectlCmdf(os.Host, string, string, ...interface{}) string
KubeconfigPath(os.Host, string) string
IsContainer(os.Host) bool
FixContainer(os.Host) error
HTTPStatus(os.Host, string) (int, error)
PrivateInterface(os.Host) (string, error)
PrivateAddress(os.Host, string, string) (string, error)
TempDir(os.Host) (string, error)
TempFile(os.Host) (string, error)
UpdateServiceEnvironment(os.Host, string, map[string]string) error
CleanupServiceEnvironment(os.Host, string) error
Stat(os.Host, string, ...exec.Option) (*os.FileInfo, error)
DeleteDir(os.Host, string, ...exec.Option) error
K0sctlLockFilePath(os.Host) string
UpsertFile(os.Host, string, string) error
MachineID(os.Host) (string, error)
SetPath(string, string)
SystemTime(os.Host) (time.Time, error)
Touch(os.Host, string, time.Time, ...exec.Option) error
Dir(string) string
Base(string) string
HostPath(string) string
}
115 changes: 56 additions & 59 deletions configurer/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ import (
"al.essio.dev/pkg/shellescape"
"github.com/k0sproject/rig/exec"
"github.com/k0sproject/rig/os"
"github.com/k0sproject/version"
)

// Linux is a base module for various linux OS support packages
type Linux struct {
paths map[string]string
pathMu sync.Mutex
paths map[string]string
pathMu sync.RWMutex
pathOnce sync.Once
}

// OSKind returns the identifier for Linux hosts
func (l *Linux) OSKind() string {
return "linux"
}

// NOTE The Linux struct does not embed rig/os.Linux because it will confuse
Expand All @@ -30,59 +35,53 @@ type Linux struct {
// path as a parameter.

func (l *Linux) initPaths() {
if l.paths != nil {
return
}
l.paths = map[string]string{
"K0sBinaryPath": "/usr/local/bin/k0s",
"K0sConfigPath": "/etc/k0s/k0s.yaml",
"K0sJoinTokenPath": "/etc/k0s/k0stoken",
"DataDirDefaultPath": "/var/lib/k0s",
}
l.pathOnce.Do(func() {
l.paths = map[string]string{
"K0sBinaryPath": "/usr/local/bin/k0s",
"K0sConfigPath": "/etc/k0s/k0s.yaml",
"K0sJoinTokenPath": "/etc/k0s/k0stoken",
"DataDirDefaultPath": "/var/lib/k0s",
}
})
}

func (l *Linux) path(key string) string {
l.initPaths()
l.pathMu.RLock()
defer l.pathMu.RUnlock()
return l.paths[key]
}

// K0sBinaryPath returns the path to the k0s binary on the host
func (l *Linux) K0sBinaryPath() string {
l.pathMu.Lock()
defer l.pathMu.Unlock()

l.initPaths()
return l.paths["K0sBinaryPath"]
return l.path("K0sBinaryPath")
}

// K0sConfigPath returns the path to the k0s config file on the host
func (l *Linux) K0sConfigPath() string {
l.pathMu.Lock()
defer l.pathMu.Unlock()

l.initPaths()
return l.paths["K0sConfigPath"]
return l.path("K0sConfigPath")
}

// K0sJoinTokenPath returns the path to the k0s join token file on the host
func (l *Linux) K0sJoinTokenPath() string {
l.pathMu.Lock()
defer l.pathMu.Unlock()

l.initPaths()
return l.paths["K0sJoinTokenPath"]
return l.path("K0sJoinTokenPath")
}

// DataDirDefaultPath returns the path to the k0s data dir on the host
func (l *Linux) DataDirDefaultPath() string {
l.pathMu.Lock()
defer l.pathMu.Unlock()
return l.path("DataDirDefaultPath")
}

l.initPaths()
return l.paths["DataDirDefaultPath"]
// Quote wraps shellescape.Quote for consumers that need OS-aware escaping
func (l *Linux) Quote(value string) string {
return shellescape.Quote(value)
}

// SetPath sets a path for a key
func (l *Linux) SetPath(key, value string) {
l.initPaths()
l.pathMu.Lock()
defer l.pathMu.Unlock()

l.initPaths()
l.paths[key] = value
}

Expand All @@ -109,21 +108,6 @@ func (l *Linux) K0sCmdf(template string, args ...interface{}) string {
return fmt.Sprintf("%s %s", l.K0sBinaryPath(), fmt.Sprintf(template, args...))
}

func (l *Linux) K0sBinaryVersion(h os.Host) (*version.Version, error) {
k0sVersionCmd := l.K0sCmdf("version")
output, err := h.ExecOutput(k0sVersionCmd, exec.Sudo(h))
if err != nil {
return nil, err
}

version, err := version.NewVersion(output)
if err != nil {
return nil, err
}

return version, nil
}

// K0sctlLockFilePath returns a path to a lock file
func (l *Linux) K0sctlLockFilePath(h os.Host) string {
if h.Exec("test -d /run/lock", exec.Sudo(h)) == nil {
Expand Down Expand Up @@ -168,17 +152,6 @@ func (l *Linux) DownloadURL(h os.Host, url, destination string, opts ...exec.Opt
return nil
}

// DownloadK0s performs k0s binary download from github on the host
func (l *Linux) DownloadK0s(h os.Host, path string, version *version.Version, arch string, opts ...exec.Option) error {
v := strings.ReplaceAll(strings.TrimPrefix(version.String(), "v"), "+", "%2B")
url := fmt.Sprintf("https://github.com/k0sproject/k0s/releases/download/v%[1]s/k0s-v%[1]s-%[2]s", v, arch)
if err := l.DownloadURL(h, url, path, opts...); err != nil {
return fmt.Errorf("failed to download k0s - check connectivity and k0s version validity: %w", err)
}

return nil
}

// ReplaceK0sTokenPath replaces the config path in the service stub
func (l *Linux) ReplaceK0sTokenPath(h os.Host, spath string) error {
return h.Exec(fmt.Sprintf("sed -i 's^REPLACEME^%s^g' %s", l.K0sJoinTokenPath(), spath))
Expand All @@ -194,6 +167,15 @@ func (l *Linux) MoveFile(h os.Host, src, dst string) error {
return h.Execf(`mv "%s" "%s"`, src, dst, exec.Sudo(h))
}

// Chown sets owner for a file or directory
func (l *Linux) Chown(h os.Host, path, owner string, opts ...exec.Option) error {
if len(opts) == 0 {
opts = []exec.Option{exec.Sudo(h)}
}
cmd := fmt.Sprintf(`chown %s %s`, shellescape.Quote(owner), shellescape.Quote(path))
return h.Exec(cmd, opts...)
}

// KubeconfigPath returns the path to a kubeconfig on the host
func (l *Linux) KubeconfigPath(h os.Host, dataDir string) string {
linux := &os.Linux{}
Expand Down Expand Up @@ -319,3 +301,18 @@ func (l *Linux) SystemTime(h os.Host) (time.Time, error) {
}
return time.Unix(unixTime, 0), nil
}

// Dir returns the directory part of a path
func (l *Linux) Dir(p string) string {
return path.Dir(p)
}

// Base returns the base part of a path
func (l *Linux) Base(p string) string {
return path.Base(p)
}

// HostPath returns the given path unchanged for linux hosts
func (l *Linux) HostPath(p string) string {
return p
}
Loading