Skip to content

Commit

Permalink
rpk context: expand, refine
Browse files Browse the repository at this point in the history
* Adds set
* Allows creating a context from a cloud cluster, improves output
* Removes --empty flag from create
* Removes duplicated edit code (moved to os package)
* Removes two-arg form of duplicate,rename, changes these commands to
  duplicate-to, rename-to (TBD on if we stick with this)
  • Loading branch information
twmb committed May 9, 2023
1 parent 4e32c89 commit e73ac86
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 136 deletions.
5 changes: 3 additions & 2 deletions src/go/rpk/pkg/cli/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ func NewCommand(fs afero.Fs, p *config.Params) *cobra.Command {
cmd.AddCommand(
newCreateCommand(fs, p),
newDeleteCommand(fs, p),
newDuplicateCommand(fs, p),
newDuplicateToCommand(fs, p),
newEditCommand(fs, p),
newListCommand(fs, p),
newPrintCommand(fs, p),
newRenameCommand(fs, p),
newRenameToCommand(fs, p),
newSetCommand(fs, p),
newUseCommand(fs, p),
)

Expand Down
137 changes: 109 additions & 28 deletions src/go/rpk/pkg/cli/context/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,34 @@
package context

import (
"errors"
"context"
"fmt"
"strings"
"time"

"github.com/redpanda-data/redpanda/src/go/rpk/pkg/cloudapi"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/config"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/oauth"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/oauth/providers/auth0"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/out"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)

func newCreateCommand(fs afero.Fs, p *config.Params) *cobra.Command {
var (
set []string
empty bool
fromSimple string
fromCloud string
description string
)

cmd := &cobra.Command{
Use: "create [NAME]",
Short: "Create an rpk context",
Args: cobra.MaximumNArgs(1),
Run: func(_ *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, args []string) {
cfg, err := p.Load(fs)
out.MaybeDie(err, "unable to load config: %v", err)

Expand All @@ -47,48 +51,68 @@ func newCreateCommand(fs afero.Fs, p *config.Params) *cobra.Command {
if name == "" {
out.Die("context name cannot be empty")
}
err = createCtx(fs, y, cfg, name, set, empty, fromSimple, description)

ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
defer cancel()
cloudMTLS, cloudSASL, err := createCtx(fs, ctx, y, cfg, fromSimple, fromCloud, set, name, description)
out.MaybeDieErr(err)

fmt.Printf("Created and switched to new context %q.\n", name)

if cloudMTLS {
fmt.Println(`
This cluster uses mTLS. Please ensure you have client certificates on your
machine an then run
rpk context set kafka_api.tls.ca_cert /path/to/ca.pem
rpk context set kafka_api.tls.client_cert /path/to/client.pem
rpk context set kafka_api.tls.client_key /path/to/key.pem`)
}
if cloudSASL {
fmt.Println(`
If your cluster requires SASL, generate SASL credentials in the UI and then set
them in rpk with
rpk context set kafka_api.sasl.user {sasl_username}
rpk context set kafka_api.sasl.password {sasl_password}`)
}
},
}

cmd.Flags().StringSliceVarP(&set, "set", "s", nil, "Create and switch to a new context, filling context fields with key=value pairs")
cmd.Flags().BoolVar(&empty, "empty", false, "Create an empty context")
cmd.Flags().StringVar(&fromSimple, "from-simple", "", "Create and switch to a new context from a (simpler to define) redpanda.yaml file")
cmd.Flags().StringVar(&description, "description", "", "Optional description of the context")
cmd.Flags().StringVar(&fromCloud, "from-cloud", "", "Create and switch to a new context generated from a Redpanda Cloud cluster ID")
cmd.Flags().StringVarP(&description, "description", "d", "", "Optional description of the context")

cmd.RegisterFlagCompletionFunc("set", createSetCompletion)

return cmd
}

// This returns whether the command should print cloud mTLS or SASL messages.
func createCtx(
fs afero.Fs,
ctx context.Context,
y *config.RpkYaml,
cfg *config.Config,
name string,
set []string,
empty bool,
fromSimple string,
fromCloud string,
set []string,
name string,
description string,
) error {
if len(set) > 0 && empty || len(set) > 0 && fromSimple != "" || empty && fromSimple != "" {
return errors.New("only one of --set, --empty, or --from-simple can be used at a time")
) (cloudMTLS, cloudSASL bool, err error) {
if fromCloud != "" && fromSimple != "" {
return false, false, fmt.Errorf("cannot use --from-cloud and --from-simple together")
}
if cx := y.Context(name); cx != nil {
return fmt.Errorf("context %q already exists", name)
return false, false, fmt.Errorf("context %q already exists", name)
}

var cx config.RpkContext
switch {
case empty:

case len(set) > 0:
for _, kv := range set {
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
return fmt.Errorf("invalid key=value pair %q", kv)
}
config.Set(&cx, split[0], split[1])
case fromCloud != "":
var err error
cx, cloudMTLS, cloudSASL, err = createCloudContext(ctx, y, cfg, fromCloud)
if err != nil {
return false, false, err
}

case fromSimple != "":
Expand All @@ -99,28 +123,85 @@ func createCtx(
default:
raw, err := afero.ReadFile(fs, fromSimple)
if err != nil {
return fmt.Errorf("unable to read file %q: %v", fromSimple, err)
return false, false, fmt.Errorf("unable to read file %q: %v", fromSimple, err)
}
var rpyaml config.RedpandaYaml
if err := yaml.Unmarshal(raw, &rpyaml); err != nil {
return fmt.Errorf("unable to yaml decode file %q: %v", fromSimple, err)
return false, false, fmt.Errorf("unable to yaml decode file %q: %v", fromSimple, err)
}
nodeCfg = rpyaml.Rpk
}
cx = config.RpkContext{
KafkaAPI: nodeCfg.KafkaAPI,
AdminAPI: nodeCfg.AdminAPI,
}
}

default:
for _, kv := range set {
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
return false, false, fmt.Errorf("invalid key=value pair %q", kv)
}
err := config.Set(&cx, split[0], split[1])
if err != nil {
return false, false, err
}
}
if cloudSASL && cx.KafkaAPI.SASL != nil {
cloudSASL = false
}

cx.Name = name
cx.Description = description
y.CurrentContext = name
y.Contexts = append([]config.RpkContext{cx}, y.Contexts...)
if err := y.Write(fs); err != nil {
return fmt.Errorf("unable to write rpk file: %v", err)
return false, false, fmt.Errorf("unable to write rpk file: %v", err)
}
return
}

func createSetCompletion(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var possibilities []string
for _, p := range setPossibilities {
if strings.HasPrefix(p, toComplete) {
possibilities = append(possibilities, p+"=")
}
}
return possibilities, cobra.ShellCompDirectiveNoSpace
}

func createCloudContext(ctx context.Context, y *config.RpkYaml, cfg *config.Config, clusterID string) (cx config.RpkContext, cloudMTLS, cloudSASL bool, err error) {
a := y.Auth(y.CurrentCloudAuth)
if a == nil {
return cx, false, false, fmt.Errorf("missing auth for current_cloud_auth %q", y.CurrentCloudAuth)
}

overrides := cfg.DevOverrides()
auth0Cl := auth0.NewClient(overrides)
expired, err := oauth.ValidateToken(a.AuthToken, auth0Cl.Audience(), a.ClientID)
if err != nil {
return cx, false, false, err
}
if expired {
return cx, false, false, fmt.Errorf("token for %q has expired, please login again", y.CurrentCloudAuth)
}
cl := cloudapi.NewClient(overrides.CloudAPIURL, a.AuthToken)

c, err := cl.Cluster(ctx, clusterID)
if err != nil {
return cx, false, false, fmt.Errorf("unable to request details for cluster %q: %w", err)
}
if len(c.Status.Listeners.Kafka.Default.URLs) == 0 {
return cx, false, false, fmt.Errorf("cluster %q has no kafka listeners", clusterID)
}
cx.KafkaAPI.Brokers = c.Status.Listeners.Kafka.Default.URLs
if l := c.Spec.KafkaListeners.Listeners; len(l) > 0 {
if l[0].TLS != nil {
cx.KafkaAPI.TLS = new(config.TLS)
cloudMTLS = l[0].TLS.RequireClientAuth
}
cloudSASL = l[0].SASL != nil
}
return nil
return cx, cloudMTLS, cloudSASL, nil
}
39 changes: 24 additions & 15 deletions src/go/rpk/pkg/cli/context/duplicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ import (
"github.com/spf13/cobra"
)

func newDuplicateCommand(fs afero.Fs, p *config.Params) *cobra.Command {
var description string
func newDuplicateToCommand(fs afero.Fs, p *config.Params) *cobra.Command {
var (
from string
description string
)
cmd := &cobra.Command{
Use: "duplicate (FROM) [TO]",
Use: "duplicate-to [NAME]",
Short: "Duplicate an rpk context to a new name",
Args: cobra.RangeArgs(1, 2),
Args: cobra.ExactArgs(1),
ValidArgsFunction: validContexts(fs, p),
Run: func(_ *cobra.Command, args []string) {
cfg, err := p.Load(fs)
Expand All @@ -34,31 +37,37 @@ func newDuplicateCommand(fs afero.Fs, p *config.Params) *cobra.Command {
out.Die("rpk.yaml file does not exist")
}

if len(args) == 1 {
args = append([]string{y.CurrentContext}, args[0])
to := args[0]
if from == "" {
from = y.CurrentContext
}
from, to := args[0], args[1]

cx := y.Context(from)
if cx == nil {
out.Die("--from context %q does not exist", from)
}
if y.Context(to) != nil {
out.Die("--to context %q already exists", to)
out.Die("destination context %q already exists", to)
}

dup := *cx
dup.Name = to
if description != "" {
dup.Description = ""
dup.Description = description
}
if y.CurrentContext == from {
y.CurrentContext = to
}
y.Contexts = append([]config.RpkContext{dup}, y.Contexts...)
err = y.Write(fs)
out.MaybeDieErr(err)
fmt.Printf("Duplicated context %q to %q.\n", from, to)

if y.CurrentContext == to {
fmt.Printf("Duplicated and set the current context to %q from %q.\n", to, from)
} else {
fmt.Printf("Duplicated to context %q from %q.\n", to, from)
}
},
}
cmd.Flags().StringVarP(&description, "description", "d", "", "Optional description for the new context, otherwise this keeps the old escription")
cmd.MarkFlagRequired("from")
cmd.MarkFlagRequired("to")
cmd.Flags().StringVarP(&description, "description", "d", "", "Optional description for the new context, otherwise this keeps the old description")
cmd.Flags().StringVarP(&from, "from", "f", "", "Context to duplicate, otherwise the current context is used")
return cmd
}
62 changes: 3 additions & 59 deletions src/go/rpk/pkg/cli/context/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,20 @@
package context

import (
"errors"
"fmt"
"os"
"os/exec"

"github.com/redpanda-data/redpanda/src/go/rpk/pkg/config"
rpkos "github.com/redpanda-data/redpanda/src/go/rpk/pkg/os"
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/out"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

func newEditCommand(fs afero.Fs, p *config.Params) *cobra.Command {
var raw bool
cmd := &cobra.Command{
Use: "edit [NAME]",
Short: "Edit an rpk context",
Short: "Edit an rpk cloud context",
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: validContexts(fs, p),
Run: func(_ *cobra.Command, args []string) {
Expand All @@ -51,17 +48,9 @@ func newEditCommand(fs afero.Fs, p *config.Params) *cobra.Command {
out.Die("context %s does not exist", name)
}

f, err := yaml.Marshal(cx)
out.MaybeDie(err, "unable to encode context: %v", err)

read, err := executeEdit(fs, f)
update, err := rpkos.EditTmpYAMLFile(fs, *cx)
out.MaybeDieErr(err)

var update config.RpkContext
if err := yaml.Unmarshal(read, &update); err != nil {
out.Die("unable to parse edited context: %v", err)
}

var renamed, updatedCurrent bool
if update.Name != name {
renamed = true
Expand Down Expand Up @@ -89,48 +78,3 @@ func newEditCommand(fs afero.Fs, p *config.Params) *cobra.Command {
cmd.Flags().BoolVar(&raw, "raw", false, "Edit context directly as it exists in rpk.yaml without any environment variables nor flags applied")
return cmd
}

func executeEdit(fs afero.Fs, f []byte) ([]byte, error) {
file, err := afero.TempFile(fs, "/tmp", "rpk_context_*.yaml")
filename := file.Name()
defer func() {
if err := fs.Remove(filename); err != nil {
fmt.Fprintf(os.Stderr, "unable to remove temporary file %q\n", filename)
}
}()
if err != nil {
return nil, fmt.Errorf("unable to create temporary file %q: %v", filename, err)
}
if _, err = file.Write(f); err != nil {
return nil, fmt.Errorf("failed to write out temporary file %q: %v", filename, err)
}
if err = file.Close(); err != nil {
return nil, fmt.Errorf("error closing temporary file %q: %v", filename, err)
}

// Launch editor
editor := os.Getenv("EDITOR")
if editor == "" {
const fallbackEditor = "/usr/bin/nano"
if _, err := os.Stat(fallbackEditor); err != nil {
return nil, errors.New("please set $EDITOR to use this command")
} else {
editor = fallbackEditor
}
}

child := exec.Command(editor, filename)
child.Stdout = os.Stdout
child.Stderr = os.Stderr
child.Stdin = os.Stdin
err = child.Run()
if err != nil {
return nil, fmt.Errorf("error running editor: %v", err)
}

read, err := afero.ReadFile(fs, filename)
if err != nil {
return nil, fmt.Errorf("error reading temporary file %q: %v", filename, err)
}
return read, err
}
Loading

0 comments on commit e73ac86

Please sign in to comment.