Skip to content

Commit

Permalink
feat: implement talosctl edit and patch config commands
Browse files Browse the repository at this point in the history
Fixes: #3209

Using parts of `kubectl` package to run the editor.
Also using the same approach as in `kubectl edit` command:
- add commented section to the top of the file with the description.
- if the config has errors, display validation errors in the commented
section at the top of the file.
- retry apply config until it succeeds.
- abort if no changes were detected or if the edited file is empty.

Patch currently supports jsonpatch only and can read it either from the
file or from the inline argument.

https://asciinema.org/a/wPawpctjoCFbJZKo2z2ATDXeC

Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
  • Loading branch information
Unix4ever committed Feb 25, 2021
1 parent c29cfaa commit 041620c
Show file tree
Hide file tree
Showing 13 changed files with 590 additions and 60 deletions.
20 changes: 16 additions & 4 deletions cmd/talosctl/cmd/talos/apply-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/talos-systems/talos/internal/pkg/tui/installer"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
"github.com/talos-systems/talos/pkg/resources/config"
)

var applyConfigCmdFlags struct {
Expand All @@ -29,16 +30,27 @@ var applyConfigCmdFlags struct {

// applyConfigCmd represents the applyConfiguration command.
var applyConfigCmd = &cobra.Command{
Use: "apply-config",
Short: "Apply a new configuration to a node",
Long: ``,
Args: cobra.NoArgs,
Use: "apply-config",
Aliases: []string{"apply"},
Short: "Apply a new configuration to a node",
Long: ``,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var (
cfgBytes []byte
e error
)

if len(args) > 0 {
cmd.Help() //nolint:errcheck

if args[0] != config.Type {
return fmt.Errorf("unknown positional argument %s", args[0])
} else if cmd.CalledAs() == "apply-config" {
return fmt.Errorf("expected no positional arguments")
}
}

if applyConfigCmdFlags.filename != "" {
cfgBytes, e = ioutil.ReadFile(applyConfigCmdFlags.filename)
if e != nil {
Expand Down
163 changes: 163 additions & 0 deletions cmd/talosctl/cmd/talos/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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 talos

import (
"bytes"
"context"
"fmt"
"io"
"runtime"
"strings"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/util/editor"
"k8s.io/kubectl/pkg/cmd/util/editor/crlf"

"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
"github.com/talos-systems/talos/pkg/resources/config"
)

var editCmdFlags struct {
namespace string
immediate bool
}

// editCmd represents the edit command.
var editCmd = &cobra.Command{
Use: "edit <type> [<id>]",
Short: "Edit a resource from 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.
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.`,
RunE: func(cmd *cobra.Command, args []string) error {
return WithClient(func(ctx context.Context, c *client.Client) error {
edit := editor.NewDefaultEditor([]string{
"TALOS_EDITOR",
"EDITOR",
})

resourceType := args[0]
if resourceType != config.Type {
return fmt.Errorf("only the config resource can be edited")
}

var lastError string

editFn := func(parentCtx context.Context, msg client.ResourceResponse) error {
if msg.Resource == nil {
return nil
}

for {
var (
buf bytes.Buffer
w io.Writer = &buf
id string
)

metadata := msg.Resource.Metadata()

if metadata != nil {
id = metadata.ID()
}

if runtime.GOOS == "windows" {
w = crlf.NewCRLFWriter(w)
}

_, err := w.Write([]byte(
fmt.Sprintf(
"# Editing %s/%s at node %s\n", resourceType, id, msg.Metadata.GetHostname(),
),
))
if err != nil {
return err
}

if lastError != "" {
lines := strings.Split(lastError, "\n")

_, err = w.Write([]byte(
fmt.Sprintf("#\n# %s\n", strings.Join(lines, "\n# ")),
))
if err != nil {
return err
}
}

body, err := yaml.Marshal(msg.Resource.Spec())
if err != nil {
return err
}

_, err = w.Write(body)
if err != nil {
return err
}

edited, _, err := edit.LaunchTempFile(fmt.Sprintf("%s-%s-edit-", resourceType, id), ".yaml", &buf)
if err != nil {
return err
}

editedWithoutComments := bytes.TrimSpace(cmdutil.StripComments(edited))

if len(bytes.TrimSpace(editedWithoutComments)) == 0 {
fmt.Println("Apply was skipped: empty file.")

break
}

if bytes.Equal(
editedWithoutComments,
bytes.TrimSpace(cmdutil.StripComments(body)),
) {
fmt.Println("Apply was skipped: no changes detected.")

break
}

_, err = c.ApplyConfiguration(parentCtx, &machine.ApplyConfigurationRequest{
Data: edited,
Immediate: editCmdFlags.immediate,
})
if err != nil {
lastError = err.Error()

continue
}

break
}

return nil
}

for _, node := range Nodes {
nodeCtx := client.WithNodes(ctx, node)
if err := helpers.ForEachResource(nodeCtx, c, editFn, editCmdFlags.namespace, args...); err != nil {
return err
}
}

return nil
})
},
}

func init() {
editCmd.Flags().StringVar(&editCmdFlags.namespace, "namespace", "", "resource namespace (default is to use default namespace per resource)")
editCmd.Flags().BoolVar(&editCmdFlags.immediate, "immediate", false, "apply the change immediately (without a reboot)")
addCommand(editCmd)
}
69 changes: 15 additions & 54 deletions cmd/talosctl/cmd/talos/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"google.golang.org/grpc/status"

"github.com/talos-systems/talos/cmd/talosctl/cmd/talos/output"
"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/pkg/machinery/client"
)

Expand Down Expand Up @@ -52,8 +53,7 @@ var getCmd = &cobra.Command{

var headerWritten bool

switch {
case getCmdFlags.watch: // get -w <type> OR get -w <type> <id>
if getCmdFlags.watch { // get -w <type> OR get -w <type> <id>
watchClient, err := c.Resources.Watch(ctx, getCmdFlags.namespace, resourceType, resourceID)
if err != nil {
return err
Expand Down Expand Up @@ -93,68 +93,29 @@ var getCmd = &cobra.Command{
}
}
}
case resourceID != "": // get <type> <id>
resp, err := c.Resources.Get(ctx, getCmdFlags.namespace, resourceType, resourceID)

for _, msg := range resp {
if msg.Definition != nil && !headerWritten {
if e := out.WriteHeader(msg.Definition, false); e != nil {
return e
}

headerWritten = true
}
}

if msg.Resource != nil {
if e := out.WriteResource(msg.Metadata.GetHostname(), msg.Resource, 0); e != nil {
return e
}
// get <type>
// get <type> <id>
printOut := func(parentCtx context.Context, msg client.ResourceResponse) error {
if msg.Definition != nil && !headerWritten {
if e := out.WriteHeader(msg.Definition, false); e != nil {
return e
}

headerWritten = true
}

if err != nil {
return err
}
default: // get <type>
listClient, err := c.Resources.List(ctx, getCmdFlags.namespace, resourceType)
if err != nil {
return err
}

for {
msg, err := listClient.Recv()
if err != nil {
if err == io.EOF || status.Code(err) == codes.Canceled {
return nil
}

if msg.Resource != nil {
if err := out.WriteResource(msg.Metadata.GetHostname(), msg.Resource, 0); err != nil {
return err
}

if msg.Metadata.GetError() != "" {
fmt.Fprintf(os.Stderr, "%s: %s\n", msg.Metadata.GetHostname(), msg.Metadata.GetError())

continue
}

if msg.Definition != nil && !headerWritten {
if e := out.WriteHeader(msg.Definition, false); e != nil {
return e
}

headerWritten = true
}

if msg.Resource != nil {
if err := out.WriteResource(msg.Metadata.GetHostname(), msg.Resource, 0); err != nil {
return err
}
}
}

return nil
}

return nil
return helpers.ForEachResource(ctx, c, printOut, getCmdFlags.namespace, args...)
})
},
}
Expand Down

0 comments on commit 041620c

Please sign in to comment.