Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func parsePrincipalID(principalID string) *managementClient.Principal {
}
}

func getKubeConfigForUser(ctx *cli.Context, user string) (*api.Config, error) {
func getKubeConfigForUserAndCluster(ctx *cli.Context, user, clusterID string) (*api.Config, error) {
cf, err := loadConfig(ctx)
if err != nil {
return nil, err
Expand All @@ -189,11 +189,15 @@ func getKubeConfigForUser(ctx *cli.Context, user string) (*api.Config, error) {
return nil, err
}

kubeConfig := focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, focusedServer.FocusedCluster())]
if clusterID == "" {
clusterID = focusedServer.FocusedCluster()
}

kubeConfig := focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, clusterID)]
return kubeConfig, nil
}

func setKubeConfigForUser(ctx *cli.Context, user string, kubeConfig *api.Config) error {
func setKubeConfigForUserAndCluster(ctx *cli.Context, user, clusterID string, kubeConfig *api.Config) error {
cf, err := loadConfig(ctx)
if err != nil {
return err
Expand All @@ -204,11 +208,15 @@ func setKubeConfigForUser(ctx *cli.Context, user string, kubeConfig *api.Config)
return err
}

if clusterID == "" {
clusterID = focusedServer.FocusedCluster()
}

if focusedServer.KubeConfigs == nil {
focusedServer.KubeConfigs = make(map[string]*api.Config)
}

focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, focusedServer.FocusedCluster())] = kubeConfig
focusedServer.KubeConfigs[fmt.Sprintf(kubeConfigKeyFormat, user, clusterID)] = kubeConfig
return cf.Write()
}

Expand Down
73 changes: 66 additions & 7 deletions cmd/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"

"github.com/rancher/norman/clientbase"
Expand All @@ -13,22 +14,65 @@ import (
"k8s.io/client-go/tools/clientcmd/api"
)

// clusterIDRegexp matches valid cluster ID formats: c-xxxxx or c-m-xxxxxxxx
var clusterIDRegexp = regexp.MustCompile(`^(c-[[:alnum:]]{5})|(c-m-[[:alnum:]]{8})$`)

func KubectlCommand() cli.Command {
return cli.Command{
Name: "kubectl",
Usage: "Run kubectl commands",
Description: "Use the current cluster context to run kubectl commands in the cluster",
Name: "kubectl",
Usage: "Run kubectl commands",
Description: `Use the current cluster context to run kubectl commands in the cluster.

Use --context to run commands in a different cluster without switching:
rancher kubectl --context c-xxxxx get pods`,
Action: runKubectl,
SkipFlagParsing: true,
}
}

// extractContextFlag parses and removes the --context flag from args.
// Returns the context value (empty if not provided) and the remaining args for kubectl.
func extractContextFlag(args cli.Args) (contextOverride string, kubectlArgs []string) {
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--context" && i+1 < len(args) {
contextOverride = args[i+1]
i++ // Skip the value
} else if val, found := strings.CutPrefix(arg, "--context="); found {
contextOverride = val
} else {
kubectlArgs = append(kubectlArgs, arg)
}
}
return contextOverride, kubectlArgs
}

// parseContextOverride validates and returns a cluster ID.
// Accepts formats: "local", "c-xxxxx", or "c-m-xxxxxxxx".
func parseContextOverride(context string) (string, error) {
if context == "" {
return "", fmt.Errorf("context cannot be empty")
}

if context == "local" {
return context, nil
}
if clusterIDRegexp.MatchString(context) {
return context, nil
}

return "", fmt.Errorf("invalid cluster ID format %q: expected local, c-xxxxx, or c-m-xxxxxxxx", context)
}

func runKubectl(ctx *cli.Context) error {
args := ctx.Args()
if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") {
return cli.ShowCommandHelp(ctx, "kubectl")
}

// Extract --context flag from args before passing to kubectl
contextOverride, kubectlArgs := extractContextFlag(args)

path, err := exec.LookPath("kubectl")
if err != nil {
return fmt.Errorf("kubectl is required to be set in your path to use this "+
Expand Down Expand Up @@ -58,7 +102,19 @@ func runKubectl(ctx *cli.Context) error {
}

currentUser := t.UserID
kubeConfig, err := getKubeConfigForUser(ctx, currentUser)

// Determine target cluster ID
var targetClusterID string
if contextOverride != "" {
targetClusterID, err = parseContextOverride(contextOverride)
if err != nil {
return err
}
} else {
targetClusterID = c.UserConfig.FocusedCluster()
}

kubeConfig, err := getKubeConfigForUserAndCluster(ctx, currentUser, targetClusterID)
if err != nil {
return err
}
Expand All @@ -76,8 +132,11 @@ func runKubectl(ctx *cli.Context) error {
}

if kubeConfig == nil || !isTokenValid {
cluster, err := getClusterByID(c, c.UserConfig.FocusedCluster())
cluster, err := getClusterByID(c, targetClusterID)
if err != nil {
if contextOverride != "" {
return fmt.Errorf("invalid --context %q: %w", contextOverride, err)
}
return err
}

Expand All @@ -92,7 +151,7 @@ func runKubectl(ctx *cli.Context) error {
return err
}

if err := setKubeConfigForUser(ctx, currentUser, kubeConfig); err != nil {
if err := setKubeConfigForUserAndCluster(ctx, currentUser, targetClusterID, kubeConfig); err != nil {
return err
}
}
Expand All @@ -110,7 +169,7 @@ func runKubectl(ctx *cli.Context) error {
return err
}

cmd := exec.Command(path, ctx.Args()...)
cmd := exec.Command(path, kubectlArgs...)
cmd.Env = append(os.Environ(), "KUBECONFIG="+tmpfile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
Expand Down