From e2ee39b8ac54ada49dd0a7ffaab4b0ae5d684792 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Thu, 27 Nov 2025 19:19:08 +0400 Subject: [PATCH] fix: support specifying patch file without '@' symbol Try to be more smart while parsing `--config-patch` (and similar) flags: * we still support inline patches * if the flag value doesn't look like a patch, try to use it as a filename directly This avoids common confusion with `--config-patch=patch.yaml` returning an error "expected a mapping node". Also clarify/updated documentation for `talosctl edit` and `talosctl patch`, as they only work for the machineconfig, there is no other usecase now. Signed-off-by: Andrey Smirnov --- cmd/talosctl/cmd/talos/edit.go | 8 ++++---- cmd/talosctl/cmd/talos/patch.go | 4 ++-- internal/integration/cli/apply-config.go | 7 +++++-- pkg/machinery/config/configpatcher/load.go | 18 ++++++++++++++++-- .../config/configpatcher/load_test.go | 13 +++++++++++++ website/content/v1.12/reference/cli.md | 16 ++++++++-------- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/cmd/talosctl/cmd/talos/edit.go b/cmd/talosctl/cmd/talos/edit.go index e4dd2581d..08069edf5 100644 --- a/cmd/talosctl/cmd/talos/edit.go +++ b/cmd/talosctl/cmd/talos/edit.go @@ -178,11 +178,11 @@ func addEditingComment(in string) string { // editCmd represents the edit command. var editCmd = &cobra.Command{ - Use: "edit []", - Short: "Edit a resource from the default editor.", + Use: "edit machineconfig", + Short: "Edit Talos node machine configuration with the default editor.", Args: cobra.RangeArgs(1, 2), - Long: `The edit command allows you to directly edit any API resource -you can retrieve via the command line tools. + Long: `The edit command allows you to directly edit the machine configuration +of a Talos node using your preferred text editor. It will open the editor defined by your TALOS_EDITOR, or EDITOR environment variables, or fall back to 'vi' for Linux diff --git a/cmd/talosctl/cmd/talos/patch.go b/cmd/talosctl/cmd/talos/patch.go index 0a00007d7..e8c02fb95 100644 --- a/cmd/talosctl/cmd/talos/patch.go +++ b/cmd/talosctl/cmd/talos/patch.go @@ -114,8 +114,8 @@ func patchFn(c *client.Client, patches []configpatcher.Patch) func(context.Conte // patchCmd represents the edit command. var patchCmd = &cobra.Command{ - Use: "patch []", - Short: "Update field(s) of a resource using a JSON patch.", + Use: "patch machineconfig", + Short: "Patch machine configuration of a Talos node with a local patch.", Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { return WithClient(func(ctx context.Context, c *client.Client) error { diff --git a/internal/integration/cli/apply-config.go b/internal/integration/cli/apply-config.go index 829dc8124..413e0cea7 100644 --- a/internal/integration/cli/apply-config.go +++ b/internal/integration/cli/apply-config.go @@ -52,7 +52,7 @@ func (suite *ApplyConfigSuite) TestApplyWithPatch() { patchPath := filepath.Join(tmpDir, "patch.yaml") suite.Require().NoError(os.WriteFile(patchPath, dummyAPPatch, 0o777)) - suite.RunCLI([]string{"apply-config", "--nodes", node, "--config-patch", "@" + patchPath, "-f", configPath}, + suite.RunCLI([]string{"apply-config", "--nodes", node, "--config-patch", patchPath, "-f", configPath}, base.StdoutEmpty(), base.StderrNotEmpty(), base.StderrShouldMatch(regexp.MustCompile("Applied configuration without a reboot")), @@ -71,12 +71,15 @@ func (suite *ApplyConfigSuite) TestApplyWithPatch() { suite.Require().NoError(os.WriteFile(patchPath, deleteDummyAPPatch, 0o777)) - suite.RunCLI([]string{"apply-config", "--nodes", node, "--config-patch", "@" + patchPath, "-f", configPath}, + suite.RunCLI([]string{"apply-config", "--nodes", node, "--config-patch", patchPath, "-f", configPath}, base.StdoutEmpty(), base.StderrNotEmpty(), base.StderrShouldMatch(regexp.MustCompile("Applied configuration without a reboot")), ) + // sleep a bit to let the config propagate + time.Sleep(1 * time.Second) + suite.RunCLI([]string{"get", "--nodes", node, "links"}, base.StdoutShouldNotMatch(regexp.MustCompile("dummy-ap-patch")), base.WithRetry(retry.Constant(15*time.Second, retry.WithUnits(time.Second))), diff --git a/pkg/machinery/config/configpatcher/load.go b/pkg/machinery/config/configpatcher/load.go index b756aaa7f..f299eefce 100644 --- a/pkg/machinery/config/configpatcher/load.go +++ b/pkg/machinery/config/configpatcher/load.go @@ -71,6 +71,10 @@ func LoadPatch(in []byte) (Patch, error) { } // LoadPatches loads the JSON patch either from value literal or from a file if the patch starts with '@'. +// +// It also tries to guess if the filename was given without '@' prefix. +// +//nolint:gocyclo func LoadPatches(in []string) ([]Patch, error) { var result []Patch @@ -81,14 +85,24 @@ func LoadPatches(in []string) ([]Patch, error) { err error ) - if strings.HasPrefix(patchString, "@") { + switch { + case strings.HasPrefix(patchString, "@"): filename := patchString[1:] contents, err = os.ReadFile(filename) if err != nil { return result, err } - } else { + case !strings.ContainsAny(patchString, "\n ") && + !strings.HasPrefix(patchString, "[") && + !strings.HasPrefix(patchString, "{"): + // any valid patch supplied inline should contain either '\n' or space, or start with '[' or '{' + // so if none of this is true, assume it's a filename, but without '@' prefix + contents, err = os.ReadFile(patchString) + if err != nil { + return result, err + } + default: contents = []byte(patchString) } diff --git a/pkg/machinery/config/configpatcher/load_test.go b/pkg/machinery/config/configpatcher/load_test.go index 05530462a..f9a874a77 100644 --- a/pkg/machinery/config/configpatcher/load_test.go +++ b/pkg/machinery/config/configpatcher/load_test.go @@ -112,3 +112,16 @@ func TestLoadMixedPatches(t *testing.T) { assert.Implements(t, (*configpatcher.StrategicMergePatch)(nil), patchList[1]) assert.IsType(t, jsonpatch.Patch{}, patchList[2]) } + +func TestLoadStraightFilename(t *testing.T) { + patchList, err := configpatcher.LoadPatches([]string{ + "testdata/strategic.yaml", + `[{"op":"replace","path":"/some","value": []}]`, + }) + require.NoError(t, err) + + require.Len(t, patchList, 2) + + assert.Implements(t, (*configpatcher.StrategicMergePatch)(nil), patchList[0]) + assert.IsType(t, jsonpatch.Patch{}, patchList[1]) +} diff --git a/website/content/v1.12/reference/cli.md b/website/content/v1.12/reference/cli.md index 4a3c28fd6..ba2b6498b 100644 --- a/website/content/v1.12/reference/cli.md +++ b/website/content/v1.12/reference/cli.md @@ -940,19 +940,19 @@ talosctl dmesg [flags] ## talosctl edit -Edit a resource from the default editor. +Edit Talos node machine configuration with the default editor. ### Synopsis -The edit command allows you to directly edit any API resource -you can retrieve via the command line tools. +The edit command allows you to directly edit the machine configuration +of a Talos node using your preferred text editor. It will open the editor defined by your TALOS_EDITOR, or EDITOR environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows. ``` -talosctl edit [] [flags] +talosctl edit machineconfig [flags] ``` ### Options @@ -2572,10 +2572,10 @@ talosctl netstat [flags] ## talosctl patch -Update field(s) of a resource using a JSON patch. +Patch machine configuration of a Talos node with a local patch. ``` -talosctl patch [] [flags] +talosctl patch machineconfig [flags] ``` ### Options @@ -3248,7 +3248,7 @@ A CLI for out-of-band management of Kubernetes nodes created by Talos * [talosctl copy](#talosctl-copy) - Copy data out from the node * [talosctl dashboard](#talosctl-dashboard) - Cluster dashboard with node overview, logs and real-time metrics * [talosctl dmesg](#talosctl-dmesg) - Retrieve kernel logs -* [talosctl edit](#talosctl-edit) - Edit a resource from the default editor. +* [talosctl edit](#talosctl-edit) - Edit Talos node machine configuration with the default editor. * [talosctl etcd](#talosctl-etcd) - Manage etcd * [talosctl events](#talosctl-events) - Stream runtime events * [talosctl gen](#talosctl-gen) - Generate CAs, certificates, and private keys @@ -3265,7 +3265,7 @@ A CLI for out-of-band management of Kubernetes nodes created by Talos * [talosctl meta](#talosctl-meta) - Write and delete keys in the META partition * [talosctl mounts](#talosctl-mounts) - List mounts * [talosctl netstat](#talosctl-netstat) - Show network connections and sockets -* [talosctl patch](#talosctl-patch) - Update field(s) of a resource using a JSON patch. +* [talosctl patch](#talosctl-patch) - Patch machine configuration of a Talos node with a local patch. * [talosctl pcap](#talosctl-pcap) - Capture the network packets from the node. * [talosctl processes](#talosctl-processes) - List running processes * [talosctl read](#talosctl-read) - Read a file on the machine