Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package k0sctl for openSUSE:Factory checked in at 2026-07-07 21:01:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/k0sctl (Old) and /work/SRC/openSUSE:Factory/.k0sctl.new.1982 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "k0sctl" Tue Jul 7 21:01:30 2026 rev:25 rq:1364054 version:0.32.1 Changes: -------- --- /work/SRC/openSUSE:Factory/k0sctl/k0sctl.changes 2026-07-03 16:11:26.862872152 +0200 +++ /work/SRC/openSUSE:Factory/.k0sctl.new.1982/k0sctl.changes 2026-07-07 21:03:29.370860160 +0200 @@ -1,0 +2,10 @@ +Mon Jul 06 12:35:25 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.32.1: + * chore: finetune rig v2 migration note + * build(deps): bump github.com/k0sproject/rig/v2 from 2.0.1 to + 2.1.0 (#1110) + * build(deps): bump github.com/k0sproject/rig/v2 from 2.0.0 to + 2.0.1 + +------------------------------------------------------------------- Old: ---- k0sctl-0.32.0.obscpio New: ---- k0sctl-0.32.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ k0sctl.spec ++++++ --- /var/tmp/diff_new_pack.CchIsA/_old 2026-07-07 21:03:31.282925731 +0200 +++ /var/tmp/diff_new_pack.CchIsA/_new 2026-07-07 21:03:31.282925731 +0200 @@ -18,7 +18,7 @@ Name: k0sctl -Version: 0.32.0 +Version: 0.32.1 Release: 0 Summary: A bootstrapping and management tool for k0s clusters License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.CchIsA/_old 2026-07-07 21:03:31.322927103 +0200 +++ /var/tmp/diff_new_pack.CchIsA/_new 2026-07-07 21:03:31.326927241 +0200 @@ -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">refs/tags/v0.32.0</param> + <param name="revision">refs/tags/v0.32.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.CchIsA/_old 2026-07-07 21:03:31.354928200 +0200 +++ /var/tmp/diff_new_pack.CchIsA/_new 2026-07-07 21:03:31.358928338 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/k0sproject/k0sctl.git</param> - <param name="changesrevision">64fda2ad15fe647fb4c47e1f6ecb1c39b5ea050c</param></service></servicedata> + <param name="changesrevision">db3dfb711f8b95e6f3bf4297db60dd5fbd337f85</param></service></servicedata> (No newline at EOF) ++++++ k0sctl-0.32.0.obscpio -> k0sctl-0.32.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.32.0/cmd/flags.go new/k0sctl-0.32.1/cmd/flags.go --- old/k0sctl-0.32.0/cmd/flags.go 2026-07-03 08:56:55.000000000 +0200 +++ new/k0sctl-0.32.1/cmd/flags.go 2026-07-06 13:40:55.000000000 +0200 @@ -281,11 +281,11 @@ func warnRigMigration(ctx *cli.Context) error { var warningRows []string warningRows = append(warningRows, "") - warningRows = append(warningRows, " ▌ This release replaces k0sctl's host connection and remote execution") - warningRows = append(warningRows, " ▌ layer (rig v2). Behavior should be unchanged, but if you hit") - warningRows = append(warningRows, " ▌ unexpected connection, sudo, OS detection or file transfer issues, please") - warningRows = append(warningRows, " ▌ report them at https://github.com/k0sproject/k0sctl/issues and roll back") - warningRows = append(warningRows, " ▌ to v0.31.1 until the issue is resolved.") + warningRows = append(warningRows, " ▌ k0sctl's host connection and remote execution functionality has been") + warningRows = append(warningRows, " ▌ updated. Behavior should be unchanged, but if you encounter any") + warningRows = append(warningRows, " ▌ issues, please report them at:") + warningRows = append(warningRows, " ▌ hem at https://github.com/k0sproject/k0sctl/issues") + warningRows = append(warningRows, " ▌ and roll back to v0.31.1 until the issue is resolved.") warningRows = append(warningRows, "") for _, row := range warningRows { fmt.Fprintln(ctx.App.ErrWriter, row) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.32.0/go.mod new/k0sctl-0.32.1/go.mod --- old/k0sctl-0.32.0/go.mod 2026-07-03 08:56:55.000000000 +0200 +++ new/k0sctl-0.32.1/go.mod 2026-07-06 13:40:55.000000000 +0200 @@ -33,7 +33,7 @@ require ( github.com/carlmjohnson/versioninfo v0.22.5 github.com/jellydator/validation v1.2.0 - github.com/k0sproject/rig/v2 v2.0.0 + github.com/k0sproject/rig/v2 v2.1.0 github.com/k0sproject/version v0.8.0 github.com/samber/slog-logrus/v2 v2.5.4 github.com/sergi/go-diff v1.4.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k0sctl-0.32.0/go.sum new/k0sctl-0.32.1/go.sum --- old/k0sctl-0.32.0/go.sum 2026-07-03 08:56:55.000000000 +0200 +++ new/k0sctl-0.32.1/go.sum 2026-07-06 13:40:55.000000000 +0200 @@ -108,8 +108,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/v2 v2.0.0 h1:PcRzPiugIReXk6kpFsQ9yCsC/OvQWwIh2U/UB0TT8Us= -github.com/k0sproject/rig/v2 v2.0.0/go.mod h1:4bp1yGRyoANCEe9aFAAJzttFvTRWMPnbleYBQUaY38c= +github.com/k0sproject/rig/v2 v2.1.0 h1:Xt7FtMlyyknnpAVN8HKP4JZmZsDWZz39pw+FrwEcpl4= +github.com/k0sproject/rig/v2 v2.1.0/go.mod h1:4bp1yGRyoANCEe9aFAAJzttFvTRWMPnbleYBQUaY38c= github.com/k0sproject/version v0.8.0 h1:Yh1SFDeBqQ7etrGwffY8bWKdbAUjeBOhiZ6oQuuz4sM= github.com/k0sproject/version v0.8.0/go.mod h1:iNV3O8blndsQhxZ8zACfpQhrLDlrTvDlCzx+vgCFtSI= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= ++++++ k0sctl.obsinfo ++++++ --- /var/tmp/diff_new_pack.CchIsA/_old 2026-07-07 21:03:31.770942467 +0200 +++ /var/tmp/diff_new_pack.CchIsA/_new 2026-07-07 21:03:31.778942742 +0200 @@ -1,5 +1,5 @@ name: k0sctl -version: 0.32.0 -mtime: 1783061815 -commit: 64fda2ad15fe647fb4c47e1f6ecb1c39b5ea050c +version: 0.32.1 +mtime: 1783338055 +commit: db3dfb711f8b95e6f3bf4297db60dd5fbd337f85 ++++++ vendor.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/client.go new/vendor/github.com/k0sproject/rig/v2/client.go --- old/vendor/github.com/k0sproject/rig/v2/client.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/client.go 2026-07-06 13:40:55.000000000 +0200 @@ -228,6 +228,7 @@ c.Runner = c.options.GetRunner(c.connection) log.InjectLogger(logger, c.Runner) + c.injectCommandGate(c.Runner) c.SudoProvider = c.options.GetSudoProvider(c.Runner) c.InitSystemProvider = c.options.GetInitSystemProvider(c.Runner) @@ -238,6 +239,24 @@ return c.initErr } +// injectCommandGate installs the configured [cmd.CommandGate] onto the given +// runner when the runner supports it. This is how a gate set once on the client +// reaches every derived runner: the base runner here, and sudo runners via the +// re-run of setup during [Client.Clone]. A nil configured gate is applied too, +// clearing any gate a reused runner may already carry so that +// WithCommandGate(nil) reliably disables gating. +func (c *Client) injectCommandGate(runner cmd.Runner) { + setter, ok := runner.(cmd.CommandGateSetter) + if !ok { + if c.options.commandGate != nil { + c.Log().Warn("command gate configured but the runner does not support it; commands will run ungated", + "runner", fmt.Sprintf("%T", runner)) + } + return + } + setter.SetCommandGate(c.options.commandGate) +} + // Service returns a manager for a named service on the remote host using // the host's init system if one can be detected. This can be used to // start, stop, restart, and check the status of services. @@ -371,14 +390,38 @@ // ExecInteractive executes a command on the host and passes stdin/stdout/stderr as-is to the session. // The session is terminated when ctx is cancelled or its deadline is exceeded. -func (c *Client) ExecInteractive(ctx context.Context, cmd string, stdin io.Reader, stdout, stderr io.Writer) error { - if conn, ok := c.connection.(protocol.InteractiveExecer); ok { - if err := conn.ExecInteractive(ctx, cmd, stdin, stdout, stderr); err != nil { - return fmt.Errorf("exec interactive: %w", err) +// +// A configured [cmd.CommandGate] is consulted for the command before the +// session starts. Because interactive exec runs directly on the connection +// rather than through the runner, the gate sees the raw command as given here, +// without sudo/shell decoration or secret redaction, and commands typed inside +// the interactive session are not gated. +func (c *Client) ExecInteractive(ctx context.Context, command string, stdin io.Reader, stdout, stderr io.Writer) error { + conn, ok := c.connection.(protocol.InteractiveExecer) + if !ok { + return errInteractiveNotSupported + } + // Short-circuit a cancelled/expired context before consulting the gate, so + // a gate implementation is never asked to prompt for a doomed session. This + // mirrors Executor.Start. + if err := ctx.Err(); err != nil { + return fmt.Errorf("exec interactive: %w", err) + } + if gate := c.options.commandGate; gate != nil { + if err := gate.AllowCommand(ctx, c.String(), command); err != nil { + // A context cancellation/deadline is not a rejection: wrap it + // without cmd.ErrCommandRejected so errors.Is reports the context + // error but not a rejection, letting callers distinguish the two. + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return fmt.Errorf("exec interactive: %w", err) + } + return fmt.Errorf("exec interactive: %w: %w", cmd.ErrCommandRejected, err) } - return nil } - return errInteractiveNotSupported + if err := conn.ExecInteractive(ctx, command, stdin, stdout, stderr); err != nil { + return fmt.Errorf("exec interactive: %w", err) + } + return nil } // The provider Getters would be available and working via the embedding already, but the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/clientoptions.go new/vendor/github.com/k0sproject/rig/v2/clientoptions.go --- old/vendor/github.com/k0sproject/rig/v2/clientoptions.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/clientoptions.go 2026-07-06 13:40:55.000000000 +0200 @@ -1,6 +1,7 @@ package rig import ( + "context" "errors" "fmt" @@ -14,7 +15,10 @@ "github.com/k0sproject/rig/v2/sudo" ) -var errNilOSRelease = errors.New("os release provider returned nil release without error") +var ( + errNilOSRelease = errors.New("os release provider returned nil release without error") + errConfirmDeclined = errors.New("declined by confirmation callback") +) // ConnectionFactory can create connections. When a connection is not given, the factory is used // to build a connection. @@ -33,6 +37,7 @@ connection protocol.Connection connectionFactory ConnectionFactory runner cmd.Runner + commandGate cmd.CommandGate retryConnection bool providersContainer } @@ -135,6 +140,7 @@ connection: o.connection, connectionFactory: o.connectionFactory, runner: o.runner, + commandGate: o.commandGate, retryConnection: o.retryConnection, providersContainer: o.providersContainer, } @@ -201,6 +207,53 @@ } } +// WithCommandGate installs a [cmd.CommandGate] that is consulted before +// commands run on the client and all of its derived runners (sudo, filesystem, +// services). Returning a non-nil error from the gate aborts the command, +// wrapped in [cmd.ErrCommandRejected]; a context cancellation/deadline error is +// the exception and propagates as a context error without [cmd.ErrCommandRejected]. +// Use [WithConfirmFunc] for the common case of a yes/no confirmation prompt. +// See [cmd.CommandGate] for exactly which commands are gated and which internal +// paths are not. +// +// The gate reaches a runner only if that runner implements +// [cmd.CommandGateSetter]. The default runner does; a custom runner supplied +// via [WithRunner] that does not implement it runs ungated, and the client +// logs a warning during setup. +func WithCommandGate(gate cmd.CommandGate) ClientOption { + return func(o *ClientOptions) { + o.commandGate = gate + } +} + +// WithConfirmFunc installs a confirmation callback consulted before commands +// run on the client and all of its derived runners (sudo, filesystem, +// services). The callback receives the host string and the human-readable, +// redacted command; returning false aborts the command with +// [cmd.ErrCommandRejected]. This is a convenience wrapper around +// [WithCommandGate]; see [cmd.CommandGate] for which commands are gated. A nil +// fn disables gating, matching WithCommandGate(nil). +// +// One exception: [Client.ExecInteractive] runs directly on the connection, so +// its command reaches the callback raw — undecorated and unredacted. Avoid +// logging the callback argument as-is if interactive commands may carry +// secrets. +func WithConfirmFunc(fn func(host, command string) bool) ClientOption { + if fn == nil { + return WithCommandGate(nil) + } + gate := cmd.CommandGateFunc(func(ctx context.Context, host, command string) error { + if err := ctx.Err(); err != nil { + return err //nolint:wrapcheck // pass the context error through unchanged so it isn't misread as a rejection + } + if fn(host, command) { + return nil + } + return errConfirmDeclined + }) + return WithCommandGate(gate) +} + // WithConnectionFactory is a functional option that sets the connection factory to use for connecting. func WithConnectionFactory(factory ConnectionFactory) ClientOption { return func(o *ClientOptions) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go new/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go --- old/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/k0sproject/rig/v2/cmd/confirm.go 2026-07-06 13:40:55.000000000 +0200 @@ -0,0 +1,55 @@ +package cmd + +import "context" + +// CommandGate is consulted before a command runs, on the [Executor] execution +// path that every Exec/Start call (and thus every remotefs and service call) +// funnels through. Returning a non-nil error aborts the command before it +// starts; the error is propagated to the caller wrapped in [ErrCommandRejected]. +// +// A context cancellation or deadline error is the one exception: it is +// propagated without [ErrCommandRejected], so errors.Is reports the context +// error but not a rejection. Callers that treat rejections specially can thus +// distinguish an explicit refusal from a cancelled/expired context. +// +// A gate fires once per command at the outermost runner, so a command that is +// wrapped by sudo (or any other decorator chain) is presented in its final, +// fully wrapped form exactly once. On that path the command argument is the +// human-readable form — decorated, sudo-wrapped, PowerShell-decoded, and with +// any registered secrets redacted — suitable for a confirmation prompt or +// audit log. +// +// The following do not go through this path and are therefore not gated: +// - [Executor.StartProcess], the low-level primitive used for runner chaining. +// - The built-in Windows-detection probe (ver.exe), which runs via +// StartProcess at runner construction to decide command formatting. +// - Privilege-escalation probes (sudo/doas availability, UID-0 and +// Windows-admin checks), which run [Ungated] so a rejecting gate surfaces +// an error rather than silently disabling sudo. +// +// [github.com/k0sproject/rig/v2.Client.ExecInteractive] consults the gate +// separately, with the raw (undecorated, unredacted) command. +// +// AllowCommand may block (for example to prompt on stdin); it should honor the +// supplied context and return its error when the caller cancels. +type CommandGate interface { + // AllowCommand reports whether the command may run on the named host. + // A nil return allows the command; a non-nil error aborts it. + AllowCommand(ctx context.Context, host, command string) error +} + +// CommandGateFunc adapts an ordinary function to the [CommandGate] interface. +type CommandGateFunc func(ctx context.Context, host, command string) error + +// AllowCommand implements [CommandGate]. +func (f CommandGateFunc) AllowCommand(ctx context.Context, host, command string) error { + return f(ctx, host, command) +} + +// CommandGateSetter is implemented by runners that support installing a +// [CommandGate]. [Executor] implements it, allowing a gate configured on a +// [github.com/k0sproject/rig/v2.Client] to be propagated to every runner the +// client derives (sudo, filesystem, services). +type CommandGateSetter interface { + SetCommandGate(gate CommandGate) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go new/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go --- old/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/cmd/execoptions.go 2026-07-06 13:40:55.000000000 +0200 @@ -51,6 +51,8 @@ traceOut io.Writer traceErr io.Writer outputClosers []io.Closer + + skipGate bool } // Format returns the command string with all per-call decorators applied. @@ -71,6 +73,11 @@ return o.logCommand } +// SkipGate returns true if the [CommandGate] should not be consulted for this command. +func (o *ExecOptions) SkipGate() bool { + return o.skipGate +} + var ( errCharDevice = errors.New("reader is a character device") errUnknownReader = errors.New("unknown type of reader") @@ -337,6 +344,15 @@ } } +// Ungated exec option for exempting a single command from the runner's +// [CommandGate]. Use it for internal probes and detection commands that must +// run unconditionally, so a rejecting gate cannot silently change behavior. +func Ungated() ExecOption { + return func(o *ExecOptions) { + o.skipGate = true + } +} + // Trace exec option for attaching a [Tracer] to a single command execution. // If the Tracer also implements [OutputTracer], per-line stdout and stderr // hooks are enabled automatically. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/executor.go new/vendor/github.com/k0sproject/rig/v2/cmd/executor.go --- old/vendor/github.com/k0sproject/rig/v2/cmd/executor.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/cmd/executor.go 2026-07-06 13:40:55.000000000 +0200 @@ -50,6 +50,7 @@ isWin func() bool osKnown atomic.Bool tracer Tracer + gate CommandGate } func isWinFunc(conn protocol.ProcessStarter) func() bool { @@ -82,6 +83,16 @@ r.tracer = t } +// SetCommandGate installs a [CommandGate] that is consulted by the Start and +// Exec family of methods before a command runs. A nil gate disables gating. +// The gate is not consulted by [Executor.StartProcess], which exists only to +// satisfy the connection interface for runner chaining. SetCommandGate must +// not be called concurrently with Start, Exec, or any other command-execution +// method. +func (r *Executor) SetCommandGate(gate CommandGate) { + r.gate = gate +} + func (r *Executor) formatCommandForOS(command string, execOpts *ExecOptions, isWindows bool) string { cmd := r.Format(execOpts.Format(command)) if isWindows && !isExe(cmd) { @@ -285,9 +296,38 @@ return strings.Join(parts, " ") } +// closeAll closes every closer, ignoring individual close errors. +func closeAll(closers []io.Closer) { + for _, c := range closers { + _ = c.Close() + } +} + +// gateCommand consults the runner's [CommandGate] for the given command, which +// must already be in redacted, human-readable form. It returns nil when there +// is no gate, when the command opts out via [Ungated], or when the gate allows +// the command. A non-nil return means the command must not be started: a +// rejection wraps [ErrCommandRejected], while a context cancellation/deadline +// is wrapped without [ErrCommandRejected] so that errors.Is reports the context +// error but not a rejection, letting callers tell the two apart. +func (r *Executor) gateCommand(ctx context.Context, execOpts *ExecOptions, redacted string) error { + if r.gate == nil || execOpts.SkipGate() { + return nil + } + err := r.gate.AllowCommand(ctx, r.String(), redacted) + if err == nil { + return nil + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + log.Trace(ctx, "command gate cancelled", log.HostAttr(r), log.KeyCommand, redacted, log.KeyError, err) + return fmt.Errorf("command gate: %w", err) + } + log.Trace(ctx, "command rejected by gate", log.HostAttr(r), log.KeyCommand, redacted, log.KeyError, err) + return fmt.Errorf("%w: %w", ErrCommandRejected, err) +} + // Start starts the command and returns a Waiter. func (r *Executor) Start(ctx context.Context, command string, opts ...ExecOption) (protocol.Waiter, error) { - log.Trace(ctx, "starting command", log.HostAttr(r), log.KeyCommand, command) if ctx.Err() != nil { return nil, fmt.Errorf("runner context error: %w", ctx.Err()) } @@ -320,8 +360,24 @@ // non-prefixed commands through cmd.exe. cmd := r.formatCommand(command, execOpts) + // redactedCmd is the human-readable command (secrets masked, PowerShell + // -EncodedCommand decoded), computed once and reused for gating and every + // log line below. Tracer callbacks deliberately receive the raw formatted + // cmd, as their contract documents. + redactedCmd := execOpts.Redact(decodeEncoded(cmd)) + + // Consult the gate before emitting any command-logging or tracer + // lifecycle events so a rejected command produces no dangling + // "executing"/CommandFormatted/ProcessStarted events. + if err := r.gateCommand(ctx, execOpts, redactedCmd); err != nil { + closeAll(traceClosers) + return nil, err + } + + log.Trace(ctx, "starting command", log.HostAttr(r), log.KeyCommand, redactedCmd) + if execOpts.LogCommand() { - r.Log().Debug("executing command", log.KeyCommand, execOpts.Redact(decodeEncoded(cmd))) + r.Log().Debug("executing command", log.KeyCommand, redactedCmd) } if tracer != nil { @@ -334,18 +390,14 @@ waiter, err := r.connection.StartProcess(ctx, cmd, execOpts.Stdin(), stdout, stderr) //nolint:contextcheck // Stdin() uses trace logger which takes context if err != nil { - for _, c := range traceClosers { - _ = c.Close() - } - log.Trace(ctx, "start process failed", log.HostAttr(r), log.KeyCommand, cmd, log.KeyError, err) + closeAll(traceClosers) + log.Trace(ctx, "start process failed", log.HostAttr(r), log.KeyCommand, redactedCmd, log.KeyError, err) return nil, fmt.Errorf("runner start command: %w", err) } if waiter == nil { - for _, c := range traceClosers { - _ = c.Close() - } - log.Trace(ctx, "start process returned nil waiter", log.HostAttr(r), log.KeyCommand, cmd, log.KeyError, err) + closeAll(traceClosers) + log.Trace(ctx, "start process returned nil waiter", log.HostAttr(r), log.KeyCommand, redactedCmd, log.KeyError, errInternal) return nil, fmt.Errorf("%w: connection returned no error but a nil waiter", errInternal) } @@ -405,20 +457,22 @@ opts = append(opts, Stdout(out)) - log.Trace(ctx, "starting command for execoutput", log.HostAttr(r), log.KeyCommand, command) + // Start gates the command and logs it once in its canonical redacted, + // decoded, fully decorated form. These traces only mark the execoutput + // lifecycle, so they deliberately omit the command to avoid re-deriving + // (and drifting from) that canonical form. proc, err := r.Start(ctx, command, opts...) if err != nil { return "", fmt.Errorf("start command: %w", err) } - log.Trace(ctx, "waiting on command", log.HostAttr(r), log.KeyCommand, command) + log.Trace(ctx, "waiting on command", log.HostAttr(r)) if err := proc.Wait(); err != nil { - log.Trace(ctx, "waiting returned an error", log.HostAttr(r), log.KeyCommand, command, log.KeyError, err) + log.Trace(ctx, "waiting returned an error", log.HostAttr(r), log.KeyError, err) return "", fmt.Errorf("command result: %w", err) } - log.Trace(ctx, "command finished", log.HostAttr(r), log.KeyCommand, command) - execOpts := Build(opts...) - return execOpts.FormatOutput(out.String()), nil + log.Trace(ctx, "command finished", log.HostAttr(r)) + return Build(opts...).FormatOutput(out.String()), nil } // ExecOutput executes the command and returns the stdout output or an error. @@ -435,14 +489,17 @@ return pipeR } opts = append(opts, Stdout(pipeW)) + // ExecContext -> Start gates and logs the command once in its canonical + // redacted, decoded form. These traces only mark the execreader lifecycle, + // so they deliberately omit the command to avoid re-deriving it here. go func() { if err := r.ExecContext(ctx, command, opts...); err != nil { - log.Trace(ctx, "execreader: execcontext returned an error", log.HostAttr(r), log.KeyCommand, command, log.KeyError, err) + log.Trace(ctx, "execreader: execcontext returned an error", log.HostAttr(r), log.KeyError, err) pipeW.CloseWithError(fmt.Errorf("exec reader: %w", err)) } else { pipeW.Close() } - log.Trace(ctx, "execreader: execcontext finished", log.HostAttr(r), log.KeyCommand, command) + log.Trace(ctx, "execreader: execcontext finished", log.HostAttr(r)) }() return pipeR } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/cmd/runner.go new/vendor/github.com/k0sproject/rig/v2/cmd/runner.go --- old/vendor/github.com/k0sproject/rig/v2/cmd/runner.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/cmd/runner.go 2026-07-06 13:40:55.000000000 +0200 @@ -17,6 +17,9 @@ // ErrWroteStderr is returned when a windows command writes to stderr, unless AllowWinStderr is set. ErrWroteStderr = errors.New("command wrote output to stderr") + + // ErrCommandRejected is returned when a [CommandGate] refuses to allow a command to run. + ErrCommandRejected = errors.New("command rejected") ) // DecorateFunc is a function that takes a string and returns a decorated string. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go new/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go --- old/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/powershell/powershell.go 2026-07-06 13:40:55.000000000 +0200 @@ -107,15 +107,16 @@ } // Cmd builds a command-line for executing a PowerShell command or script. -// Scripts that contain newlines, double-quotes, or cmd.exe metacharacters -// are passed via -EncodedCommand to avoid shell expansion; simple one-liners -// are passed via -Command so they remain readable in logs. -// cmd.exe metacharacters guarded: " % ! ^ & | < >. +// The command is always passed via -EncodedCommand (base64 UTF-16LE): the +// payload is opaque to both cmd.exe and PowerShell, so it survives intact +// regardless of the outer login shell. This matters when the remote host runs +// OpenSSH with DefaultShell=PowerShell, where a plain -Command "..." argument +// would be parsed and its $-tokens expanded by the outer shell before the +// inner powershell.exe ever sees them. The trade-off is unreadable command +// lines, but the runner's debug log decodes -EncodedCommand back to the +// original script. func Cmd(psCmd string) string { - if strings.ContainsAny(psCmd, "\n\r\"%!^&|<>") { - return "powershell.exe -NonInteractive -ExecutionPolicy Unrestricted -NoP -E " + EncodeCmd(psCmd) - } - return "powershell.exe -NonInteractive -ExecutionPolicy Unrestricted -NoP -Command \"" + withProgressPreference(psCmd) + "\"" + return "powershell.exe -NonInteractive -ExecutionPolicy Unrestricted -NoP -E " + EncodeCmd(psCmd) } // SingleQuote quotes and escapes a string in a format that is accepted by powershell scriptlets diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/doas.go new/vendor/github.com/k0sproject/rig/v2/sudo/doas.go --- old/vendor/github.com/k0sproject/rig/v2/sudo/doas.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/sudo/doas.go 2026-07-06 13:40:55.000000000 +0200 @@ -16,7 +16,9 @@ if c.IsWindows() { return nil, false } - if c.Exec(Doas("true")) != nil { + // Ungated: a CommandGate that rejects this probe would silently + // disable doas rather than surface an error, so it must always run. + if c.Exec(Doas("true"), cmd.Ungated()) != nil { return nil, false } return cmd.NewExecutor(c, Doas), true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/noop.go new/vendor/github.com/k0sproject/rig/v2/sudo/noop.go --- old/vendor/github.com/k0sproject/rig/v2/sudo/noop.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/sudo/noop.go 2026-07-06 13:40:55.000000000 +0200 @@ -15,7 +15,9 @@ if c.IsWindows() { return nil, false } - if c.Exec(`[ "$(id -u)" = 0 ]`) != nil { + // Ungated: a CommandGate that rejects this probe would silently change + // sudo detection, so it must always run. + if c.Exec(`[ "$(id -u)" = 0 ]`, cmd.Ungated()) != nil { return nil, false } return cmd.NewExecutor(c, Noop), true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go new/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go --- old/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/sudo/sudo.go 2026-07-06 13:40:55.000000000 +0200 @@ -16,7 +16,9 @@ if c.IsWindows() { return nil, false } - if c.Exec(Sudo("true")) != nil { + // Ungated: a CommandGate that rejects this probe would silently + // disable sudo rather than surface an error, so it must always run. + if c.Exec(Sudo("true"), cmd.Ungated()) != nil { return nil, false } return cmd.NewExecutor(c, Sudo), true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/k0sproject/rig/v2/sudo/windows.go new/vendor/github.com/k0sproject/rig/v2/sudo/windows.go --- old/vendor/github.com/k0sproject/rig/v2/sudo/windows.go 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/github.com/k0sproject/rig/v2/sudo/windows.go 2026-07-06 13:40:55.000000000 +0200 @@ -16,7 +16,9 @@ return nil, false } const isAdminCmd = `if (-not (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 1 }` - if runner.Exec(isAdminCmd, cmd.PS()) != nil { + // Ungated: a CommandGate that rejects this probe would silently change + // privilege detection, so it must always run. + if runner.Exec(isAdminCmd, cmd.PS(), cmd.Ungated()) != nil { return nil, false } return cmd.NewExecutor(runner, Noop), true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2026-07-03 08:56:55.000000000 +0200 +++ new/vendor/modules.txt 2026-07-06 13:40:55.000000000 +0200 @@ -143,7 +143,7 @@ # github.com/k0sproject/dig v0.4.0 ## explicit; go 1.21 github.com/k0sproject/dig -# github.com/k0sproject/rig/v2 v2.0.0 +# github.com/k0sproject/rig/v2 v2.1.0 ## explicit; go 1.26.0 github.com/k0sproject/rig/v2 github.com/k0sproject/rig/v2/byteslice
