Skip to content

Commit

Permalink
feat: add context locks to prevent certain kubectl commands
Browse files Browse the repository at this point in the history
  • Loading branch information
idebeijer committed Aug 16, 2024
1 parent db89798 commit de8a021
Show file tree
Hide file tree
Showing 13 changed files with 378 additions and 60 deletions.
5 changes: 3 additions & 2 deletions cmd/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/idebeijer/kubert/internal/config"
"github.com/idebeijer/kubert/internal/fzf"
"github.com/idebeijer/kubert/internal/kubeconfig"
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -162,10 +163,10 @@ func launchShellWithKubeconfig(kubeconfigPath string) error {
if err := os.Setenv("KUBECONFIG", kubeconfigPath); err != nil {
return fmt.Errorf("failed to set KUBECONFIG environment variable: %w", err)
}
if err := os.Setenv(KubertShellActiveEnvVar, "1"); err != nil {
if err := os.Setenv(kubert.ShellActiveEnvVar, "1"); err != nil {
return fmt.Errorf("failed to set KUBERT_SHELL environment variable: %w", err)
}
if err := os.Setenv(KubertShellKubeconfigEnvVar, kubeconfigPath); err != nil {
if err := os.Setenv(kubert.ShellKubeconfigEnvVar, kubeconfigPath); err != nil {
return fmt.Errorf("failed to set KUBERT_SHELL_KUBECONFIG environment variable: %w", err)
}

Expand Down
26 changes: 26 additions & 0 deletions cmd/contextlock/contextlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package contextlock

import "github.com/spf13/cobra"

func NewCommand() *cobra.Command {
cmd := &cobra.Command{}

cmd = &cobra.Command{
Use: "context-lock",
Short: "Lock and unlock contexts",
Long: `Lock and unlock contexts.
This will allow you to lock and unlock contexts for the "kubert kubectl" command. This can be useful if you want to prevent accidentally running certain kubectl commands to a cluster.
Keep in mind, this will only work when using kubectl through the "kubert kubectl" command. Direct commands using just "kubectl" will not be blocked. (If you use this feature, you could set an alias for "kubectl" or "k" to "kubert kubectl".)
Both "lock" and "unlock" will set an explicit setting for the given context. That means if either of those has been set, kubert will ignore the default setting. If you want to use the default setting again, use "kubert context-lock delete <context>".
What kubectl commands should be blocked can be configured in the kubert configuration file.`,
}

cmd.AddCommand(NewLockCommand())
cmd.AddCommand(NewUnlockCommand())
cmd.AddCommand(NewDeleteCommand())

return cmd
}
40 changes: 40 additions & 0 deletions cmd/contextlock/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package contextlock

import (
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/idebeijer/kubert/internal/util"
"github.com/spf13/cobra"
)

func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Delete lock setting for the current context",
Long: `Delete lock setting for the current context.
This will delete the explicit lock/unlock setting for the current context. So if either "lock" or "unlock" was set, it will be removed and the default will be used.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return kubert.ShellPreFlightCheck()
},
RunE: func(cmd *cobra.Command, args []string) error {
sm, err := state.NewManager()
if err != nil {
return err
}

clientConfig, err := util.KubeClientConfig()
if err != nil {
return err
}

if err := sm.DeleteContextLock(clientConfig.CurrentContext); err != nil {
return err
}

return nil
},
}

return cmd
}
40 changes: 40 additions & 0 deletions cmd/contextlock/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package contextlock

import (
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/idebeijer/kubert/internal/util"
"github.com/spf13/cobra"
)

func NewLockCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "lock",
Short: "Lock current context",
Long: `Lock current context.
This will set an explicit "lock" for the current context. That means it wil override the default setting. If the current context should use the default again, use "kubert context-lock delete".`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return kubert.ShellPreFlightCheck()
},
RunE: func(cmd *cobra.Command, args []string) error {
sm, err := state.NewManager()
if err != nil {
return err
}

clientConfig, err := util.KubeClientConfig()
if err != nil {
return err
}

if err := sm.SetContextLock(clientConfig.CurrentContext, true); err != nil {
return err
}

return nil
},
}

return cmd
}
40 changes: 40 additions & 0 deletions cmd/contextlock/unlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package contextlock

import (
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/idebeijer/kubert/internal/util"
"github.com/spf13/cobra"
)

func NewUnlockCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "unlock",
Short: "Unlock current context",
Long: `Unlock current context.
This will set an explicit "unlock" for the current context. That means it wil override the default setting. If the current context should use the default again, use "kubert context-lock delete".`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return kubert.ShellPreFlightCheck()
},
RunE: func(cmd *cobra.Command, args []string) error {
sm, err := state.NewManager()
if err != nil {
return err
}

clientConfig, err := util.KubeClientConfig()
if err != nil {
return err
}

if err := sm.SetContextLock(clientConfig.CurrentContext, false); err != nil {
return err
}

return nil
},
}

return cmd
}
77 changes: 69 additions & 8 deletions cmd/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"

"github.com/idebeijer/kubert/internal/config"
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/idebeijer/kubert/internal/util"
"github.com/spf13/cobra"
)

Expand All @@ -14,15 +19,48 @@ func NewKubectlCommand() *cobra.Command {

cmd = &cobra.Command{
Use: "kubectl",
Short: "Wrapper for kubectl, with some extra features",
Long: `Wrapper for kubectl, with some extra features`,
Short: "Wrapper for kubectl",
Long: `Wrapper for kubectl, to support context locking.`,
DisableFlagParsing: true,
Aliases: []string{"namespace"},
PreRunE: func(cmd *cobra.Command, args []string) error {
return kubectlPreflightCheck()
_, err := exec.LookPath("kubectl")
if err != nil {
return fmt.Errorf("kubectl not found in PATH")
}

return kubert.ShellPreFlightCheck()
},
SilenceUsage: true,
ValidArgsFunction: validKubectlArgsFunction,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Cfg

sm, err := state.NewManager()
if err != nil {
return err
}

clientConfig, err := util.KubeClientConfig()
if err != nil {
return err
}

locked, err := isContextLocked(sm, clientConfig.CurrentContext, cfg)
if err != nil {
return err
}

if locked {
if isCommandBlocked(args, cfg.Contexts.BlockedKubectlCommands) {
fmt.Printf("Oops: you tried to run the kubectl command \"%s\" in the locked context \"%s\".\n\n"+
"The command has not been executed because the \"%s\" command is on the blocked list, and the current context is locked.\n"+
"Use 'kubert context-lock unlock' to unlock the current context.\n"+
"Exiting...\n", args[0], clientConfig.CurrentContext, args[0])
return nil
}
}

kubectlCmd := exec.Command("kubectl", args...)
kubectlCmd.Stdin = os.Stdin
kubectlCmd.Stdout = os.Stdout
Expand Down Expand Up @@ -64,10 +102,33 @@ func validKubectlArgsFunction(cmd *cobra.Command, args []string, toComplete stri
return validCompletions, cobra.ShellCompDirectiveDefault
}

func kubectlPreflightCheck() error {
_, err := exec.LookPath("kubectl")
if err != nil {
return fmt.Errorf("kubectl not found in PATH")
func isCommandBlocked(args []string, blockedCmds []string) bool {
if len(args) > 0 {
for _, blockedCmd := range blockedCmds {
if args[0] == blockedCmd {
return true
}
}
}
return nil
return false
}

func isContextLocked(sm *state.Manager, context string, cfg config.Config) (bool, error) {
contextInfo, _ := sm.ContextInfo(context)
if contextInfo.Locked == nil && cfg.Contexts.DefaultLocked != nil {
regex, err := regexp.Compile(*cfg.Contexts.DefaultLocked)
if err != nil {
return false, fmt.Errorf("failed to compile regex: %w", err)
}

if regex.MatchString(context) {
return true, nil
}
}

if contextInfo.Locked != nil && *contextInfo.Locked == true {
return true, nil
}

return false, nil
}
37 changes: 2 additions & 35 deletions cmd/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

"github.com/idebeijer/kubert/internal/fzf"
"github.com/idebeijer/kubert/internal/kubert"
"github.com/idebeijer/kubert/internal/state"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -22,7 +23,7 @@ func NewNamespaceCommand() *cobra.Command {
Long: `Switch to a different namespace in the current Kubert shell. Other shells with the same context will not be affected.`,
Aliases: []string{"namespace"},
PreRunE: func(cmd *cobra.Command, args []string) error {
return preflightCheck()
return kubert.ShellPreFlightCheck()
},
ValidArgsFunction: validNamespaceArgsFunction,
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -117,10 +118,6 @@ func switchNamespace(sm *state.Manager, namespace string, namespaces []string) e
}

kubeconfigPath := os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
kubeconfigPath = clientcmd.RecommendedHomeFile
}

config, err := clientcmd.LoadFromFile(kubeconfigPath)
if err != nil {
return err
Expand All @@ -139,36 +136,6 @@ func switchNamespace(sm *state.Manager, namespace string, namespaces []string) e
return nil
}

func preflightCheck() error {
kubeconfigPath := os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
kubeconfigPath = clientcmd.RecommendedHomeFile
}

if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
return fmt.Errorf("kubeconfig file not found at %s", kubeconfigPath)
}

if kubertActive := os.Getenv(KubertShellActiveEnvVar); kubertActive != "1" {
return fmt.Errorf("shell not started by kubert")
}

kubertKubeconfig := os.Getenv(KubertShellKubeconfigEnvVar)
if kubertKubeconfig == "" {
return fmt.Errorf("kubeconfig file not found in environment")
}

// Check if the kubeconfig file is the same as the one set by kubert,
// if not, it means that the user or some other process has changed the KUBECONFIG environment variable
// and kubert should not interfere with it, so kubert will choose to exit instead
if kubertKubeconfig != kubeconfigPath {
return fmt.Errorf("KUBECONFIG environment variable does not match kubert kubeconfig," +
" to prevent kubert from interfering with your original kubeconfigs, please start a new shell with kubert")
}

return nil
}

func validNamespaceArgsFunction(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := context.Background()

Expand Down
10 changes: 2 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,14 @@ import (
"log/slog"
"os"

"github.com/idebeijer/kubert/cmd/contextlock"
"github.com/idebeijer/kubert/cmd/kubeconfig"
"github.com/idebeijer/kubert/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)

const (
// KubertShellActiveEnvVar is the environment variable that is set to indicate that Kubert is active.
KubertShellActiveEnvVar = "KUBERT_SHELL_ACTIVE"

// KubertShellKubeconfigEnvVar is the environment variable that is set to the path of the temporary kubeconfig file.
KubertShellKubeconfigEnvVar = "KUBERT_SHELL_KUBECONFIG"
)

type RootCmd struct {
*cobra.Command

Expand Down Expand Up @@ -57,6 +50,7 @@ func (c *RootCmd) initFlags() {

func (c *RootCmd) addCommands() {
c.AddCommand(kubeconfig.NewCommand())
c.AddCommand(contextlock.NewCommand())
c.AddCommand(NewContextCommand())
c.AddCommand(NewNamespaceCommand())
c.AddCommand(NewKubectlCommand())
Expand Down
Loading

0 comments on commit de8a021

Please sign in to comment.