Skip to content

Commit

Permalink
feat: add talosctl machineconfig patch command
Browse files Browse the repository at this point in the history
Add talosctl machineconfig patch command which accepts a machine config as input and a list of patches, applying the patches and writing the result to a file or to stdout.

Link `talosctl machineconfig gen` to `talosctl gen config`, so they work the same way.

Closes siderolabs#6562.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
  • Loading branch information
utkuozdemir committed Dec 2, 2022
1 parent d3cf061 commit 7ab140a
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 20 deletions.
40 changes: 22 additions & 18 deletions cmd/talosctl/cmd/mgmt/gen/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,30 @@ var genConfigCmdFlags struct {
withSecrets string
}

// genConfigCmd represents the `gen config` command.
var genConfigCmd = &cobra.Command{
Use: "config <cluster name> <cluster endpoint>",
Short: "Generates a set of configuration files for Talos cluster",
Long: `The cluster endpoint is the URL for the Kubernetes API. If you decide to use
// NewConfigCmd builds the config generation subcommand with the given name.
func NewConfigCmd(name string) *cobra.Command {
return &cobra.Command{
Use: fmt.Sprintf("%s <cluster name> <cluster endpoint>", name),
Short: "Generates a set of configuration files for Talos cluster",
Long: `The cluster endpoint is the URL for the Kubernetes API. If you decide to use
a control plane node, common in a single node control plane setup, use port 6443 as
this is the port that the API server binds to on every control plane node. For an HA
setup, usually involving a load balancer, use the IP and port of the load balancer.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
err := validateClusterEndpoint(args[1])
if err != nil {
return err
}

switch genConfigCmdFlags.configVersion {
case "v1alpha1":
return writeV1Alpha1Config(args)
}
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
err := validateClusterEndpoint(args[1])
if err != nil {
return err
}

return nil
},
switch genConfigCmdFlags.configVersion {
case "v1alpha1":
return writeV1Alpha1Config(args)
default:
return fmt.Errorf("unknown config version: %q", genConfigCmdFlags.configVersion)
}
},
}
}

func fixControlPlaneEndpoint(u *url.URL) *url.URL {
Expand Down Expand Up @@ -411,6 +413,8 @@ func validateClusterEndpoint(endpoint string) error {
}

func init() {
genConfigCmd := NewConfigCmd("config")

genConfigCmd.Flags().StringVar(&genConfigCmdFlags.installDisk, "install-disk", "/dev/sda", "the disk to install to")
genConfigCmd.Flags().StringVar(&genConfigCmdFlags.installImage, "install-image", helpers.DefaultImage(images.DefaultInstallerImageRepository), "the image used to perform an installation")
genConfigCmd.Flags().StringSliceVar(&genConfigCmdFlags.additionalSANs, "additional-sans", []string{}, "additional Subject-Alt-Names for the APIServer certificate")
Expand Down
12 changes: 12 additions & 0 deletions cmd/talosctl/cmd/mgmt/machineconfig/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package machineconfig

import "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/gen"

func init() {
// alias for `talosctl gen config`
Cmd.AddCommand(gen.NewConfigCmd("gen"))
}
14 changes: 14 additions & 0 deletions cmd/talosctl/cmd/mgmt/machineconfig/machineconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package machineconfig

import "github.com/spf13/cobra"

// Cmd represents the `machineconfig` command.
var Cmd = &cobra.Command{
Use: "machineconfig",
Short: "Machine config related commands",
Aliases: []string{"mc"},
}
73 changes: 73 additions & 0 deletions cmd/talosctl/cmd/mgmt/machineconfig/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package machineconfig

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"

"github.com/siderolabs/talos/pkg/machinery/config/configpatcher"
)

var patchCmdFlags struct {
patches []string
output string
}

// patchCmd represents the `machineconfig patch` command.
var patchCmd = &cobra.Command{
Use: "patch <machineconfig-file>",
Short: "Patch a machine config",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
data, err := os.ReadFile(args[0])
if err != nil {
return err
}

patches, err := configpatcher.LoadPatches(patchCmdFlags.patches)
if err != nil {
return err
}

patched, err := configpatcher.Apply(configpatcher.WithBytes(data), patches)
if err != nil {
return err
}

patchedData, err := patched.Bytes()
if err != nil {
return err
}

if patchCmdFlags.output == "" { // write to stdout
fmt.Printf("%s\n", patchedData)

return nil
}

// write to file

parentDir := filepath.Dir(patchCmdFlags.output)

// Create dir path, ignoring "already exists" messages
if err := os.MkdirAll(parentDir, os.ModePerm); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create output dir: %w", err)
}

return os.WriteFile(patchCmdFlags.output, patchedData, 0o644)
},
}

func init() {
// use StringArrayVarP instead of StringSliceVarP to prevent cobra from splitting the patch string on commas
patchCmd.Flags().StringArrayVarP(&patchCmdFlags.patches, "patch", "p", nil, "patch generated machineconfigs (applied to all node types), use @file to read a patch from file")
patchCmd.Flags().StringVarP(&patchCmdFlags.output, "output", "o", "", "output destination. if not specified, output will be printed to stdout")

Cmd.AddCommand(patchCmd)
}
2 changes: 2 additions & 0 deletions cmd/talosctl/cmd/mgmt/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/debug"
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/gen"
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/inject"
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/machineconfig"
)

// Commands is a list of commands published by the package.
Expand All @@ -30,4 +31,5 @@ func init() {
addCommand(gen.Cmd)
addCommand(debug.Cmd)
addCommand(inject.Cmd)
addCommand(machineconfig.Cmd)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ require (
go.etcd.io/etcd/client/v3 v3.5.6
go.etcd.io/etcd/etcdutl/v3 v3.5.6
go.uber.org/atomic v1.10.0
go.uber.org/multierr v1.8.0
go.uber.org/zap v1.23.0
go4.org/netipx v0.0.0-20220925034521-797b0c90d8ab
golang.org/x/net v0.2.0
Expand Down Expand Up @@ -276,7 +277,6 @@ require (
go.opentelemetry.io/otel v1.10.0 // indirect
go.opentelemetry.io/otel/trace v1.10.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/oauth2 v0.1.0 // indirect
Expand Down
20 changes: 20 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,26 @@ Talos now uses `registry.k8s.io` instead of `k8s.gcr.io` for Kubernetes containe
See [Kubernetes documentation](https://kubernetes.io/blog/2022/11/28/registry-k8s-io-faster-cheaper-ga/) for additional details.
If using registry mirrors, or in air-gapped installations you may need to update your configuration.
"""

[notes.talosctl_machineconfig_patch]
title = "talosctl machineconfig patch"
description = """\
A new subcommand, `machineconfig patch` is added to `talosctl` to allow patching of machine configuration.
It accepts a machineconfig file and a list of patches as input and outputs the patched machine configuration.
Patches can be sourced from the command line or from a file. Output can be written to a file or to stdout.
Example:
```bash
talosctl machineconfig patch controlplane.yaml \
--patch '[{"op":"replace","path":"/cluster/clusterName","value":"patch1"}]' \
--patch @/path/to/patch2.json
```
Additionally, `talosctl machineconfig gen` subcommand is introduced as an alias to `talosctl gen config`.
"""

[make_deps]
Expand Down
2 changes: 1 addition & 1 deletion internal/integration/cli/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1/generate"
)

// GenSuite verifies dmesg command.
// GenSuite verifies gen command.
type GenSuite struct {
base.CLISuite

Expand Down
152 changes: 152 additions & 0 deletions internal/integration/cli/machineconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//go:build integration_cli

package cli

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"go.uber.org/multierr"

"github.com/siderolabs/talos/internal/integration/base"
)

// MachineConfigSuite verifies machineconfig command.
type MachineConfigSuite struct {
base.CLISuite

savedCwd string
}

// SuiteName ...
func (suite *MachineConfigSuite) SuiteName() string {
return "cli.MachineConfigSuite"
}

// SetupTest ...
func (suite *MachineConfigSuite) SetupTest() {
var err error
suite.savedCwd, err = os.Getwd()
suite.Require().NoError(err)
}

// TearDownTest ...
func (suite *MachineConfigSuite) TearDownTest() {
if suite.savedCwd != "" {
suite.Require().NoError(os.Chdir(suite.savedCwd))
}
}

// TestGen tests the gen subcommand.
// Identical to GenSuite.TestGenConfigToStdoutControlPlane
// The remaining functionality is checked for `talosctl gen config` in GenSuite.
func (suite *MachineConfigSuite) TestGen() {
suite.RunCLI([]string{
"gen", "config",
"foo", "https://192.168.0.1:6443",
"--output-types", "controlplane",
"--output", "-",
}, base.StdoutMatchFunc(func(output string) error {
expected := "type: controlplane"
if !strings.Contains(output, expected) {
return fmt.Errorf("stdout does not contain %q: %q", expected, output)
}

return nil
}))
}

// TestPatchPrintStdout tests the patch subcommand with output set to stdout
// with multiple patches from the command line and from file.
func (suite *MachineConfigSuite) TestPatchPrintStdout() {
patch1, err := json.Marshal([]map[string]interface{}{
{
"op": "replace",
"path": "/cluster/clusterName",
"value": "replaced",
},
})
suite.Require().NoError(err)

patch2, err := json.Marshal([]map[string]interface{}{
{
"op": "replace",
"path": "/cluster/controlPlane/endpoint",
"value": "replaced",
},
})
suite.Require().NoError(err)

patch2Path := filepath.Join(suite.T().TempDir(), "patch2.json")

err = os.WriteFile(patch2Path, patch2, 0o644)
suite.Require().NoError(err)

mc := filepath.Join(suite.T().TempDir(), "input.yaml")

suite.RunCLI([]string{
"gen", "config",
"foo", "https://192.168.0.1:6443",
"--output-types", "controlplane",
"--output", mc,
})

suite.RunCLI([]string{
"machineconfig", "patch", mc, "--patch", string(patch1), "-p", "@" + patch2Path,
}, base.StdoutMatchFunc(func(output string) error {
var matchErr error

if !strings.Contains(output, "clusterName: replaced") {
matchErr = multierr.Append(matchErr, fmt.Errorf("clusterName not replaced"))
}

if !strings.Contains(output, "endpoint: replaced") {
matchErr = multierr.Append(matchErr, fmt.Errorf("endpoint not replaced"))
}

return matchErr
}))
}

// TestPatchWriteToFile tests the patch subcommand with output set to a file.
func (suite *MachineConfigSuite) TestPatchWriteToFile() {
patch1, err := json.Marshal([]map[string]interface{}{
{
"op": "replace",
"path": "/cluster/clusterName",
"value": "replaced",
},
})
suite.Require().NoError(err)

mc := filepath.Join(suite.T().TempDir(), "input2.yaml")

suite.RunCLI([]string{
"gen", "config",
"foo", "https://192.168.0.1:6443",
"--output-types", "controlplane",
"--output", mc,
})

outputFile := filepath.Join(suite.T().TempDir(), "inner", "output.yaml")

suite.RunCLI([]string{
"machineconfig", "patch", mc, "--patch", string(patch1), "--output", outputFile,
}, base.StdoutEmpty())

outputBytes, err := os.ReadFile(outputFile)
suite.Assert().NoError(err)

suite.Assert().Contains(string(outputBytes), "clusterName: replaced")
}

func init() {
allSuites = append(allSuites, new(MachineConfigSuite))
}
Loading

0 comments on commit 7ab140a

Please sign in to comment.