Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: switch to previous kubeconfig using kubectl switch - #174

Merged
merged 5 commits into from
Mar 7, 2025
Merged
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
33 changes: 3 additions & 30 deletions .github/configs/commitlint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,33 +1,6 @@
// Extending the conventional commit rules
export default {
extends: ["@commitlint/config-conventional"],
rules: {
// Enforce specific commit types
"type-enum": [
2, // Error level
"always",
[
"feat", // New feature
"fix", // Bug fix
"docs", // Documentation changes
"style", // Formatting, whitespace, missing semicolons, etc.
"refactor",// Code restructuring without functional changes
"perf", // Performance improvements
"test", // Adding or modifying tests
"chore", // Changes to build process or auxiliary tools
"ci", // Continuous Integration changes
"revert" // Reverting a previous commit
]
],
// Enforce case style for commit messages
"subject-case": [
2, // Error level
"always",
[
"sentence-case", // Example: "Fix login button issue"
"start-case", // Example: "Fix Login Button Issue"
"lower-case", // Example: "fix login button issue"
]
]
}
};
'header-max-length': [2, 'always', 128],
},
};
1 change: 0 additions & 1 deletion .github/configs/labeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ area/docker:
- changed-files:
- any-glob-to-any-file: ".dockerignore"
- any-glob-to-any-file: "Dockerfile"
- any-glob-to-any-file: ".devcontainer/devcontainer.json"

area/github:
- changed-files:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
kubectl-switch
dist/
configs/
108 changes: 16 additions & 92 deletions cmd/context.go
Original file line number Diff line number Diff line change
@@ -1,93 +1,44 @@
package cmd

import (
"os"
"path/filepath"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/mirceanton/kubectl-switch/pkg/kubeconfig"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
)

// contextCmd represents the switch command to change Kubernetes contexts.
var contextManager = kubeconfig.NewContextManager(manager)

var contextCmd = &cobra.Command{
Use: "context",
Aliases: []string{"ctx"}, // Add this line to define the alias
Aliases: []string{"ctx"},
Short: "Switch the active Kubernetes context",
Args: cobra.MaximumNArgs(1), // Accept at most one argument (the context name)
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Determine the kubeconfig directory
if configDir == "" {
configDir = os.Getenv("KUBECONFIG_DIR")
if configDir == "" {
log.Fatal("kubeconfig directory not provided.")
log.Fatal("Please provide the directory containing kubeconfig files via the --config-dir flag or KUBECONFIG_DIR environment variable")
}
}
if strings.HasPrefix(configDir, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Failed to determine home directory: %v", err)
}
configDir = filepath.Join(homeDir, configDir[2:])
}

// Get all kubeconfig files in the config directory
files, err := os.ReadDir(configDir)
// Validate and get the config directory
validConfigDir, err := contextManager.ValidateConfigDir(configDir)
if err != nil {
log.Fatalf("Failed to read directory: %v", err)
log.Fatal(err)
}

// Parse all kubeconfig files in the directory
contextMap := make(map[string]string) // Map storing the context name (key) and the corresponding kubeconfig file (value)
var contextNames []string // List of context names for the interactive prompt

for _, file := range files {
// Skip directories
if file.IsDir() {
continue
}

// Skip files that are not YAML
if filepath.Ext(file.Name()) != ".yaml" && filepath.Ext(file.Name()) != ".yml" {
continue
}

// Parse the kubeconfig file
path := filepath.Join(configDir, file.Name())
kubeconfig, err := clientcmd.LoadFromFile(path)
if err != nil {
log.WithFields(log.Fields{"file": file.Name()}).Warnf("Failed to parse kubeconfig file: %v", err)
continue
}

// Add context details to the map
for contextName := range kubeconfig.Contexts {
if _, exists := contextMap[contextName]; exists {
log.Fatalf("Duplicate context name '%s' found in files:\n- %s\n- %s", contextName, contextMap[contextName], path)
}
contextMap[contextName] = path
contextNames = append(contextNames, contextName)
}
// Get all contexts from kubeconfig files
contextMap, contextNames, err := contextManager.GetContextsFromDir(validConfigDir)
if err != nil {
log.Fatalf("Failed to read kubeconfig files: %v", err)
}

// Check if any contexts were found
if len(contextMap) == 0 {
log.Fatal("No kubernetes contexts found in the provided directory: ", configDir)
log.Fatal("No kubernetes contexts found in the provided directory: ", validConfigDir)
}

// Determine the target context
var selectedContext string
if len(args) == 1 {
// Non-interactive mode: use the provided cluster name
selectedContext = args[0]
if _, exists := contextMap[selectedContext]; !exists {
log.Fatalf("Context '%s' not found", selectedContext)
}
} else {
// Interactive mode: show list of clusters
prompt := &survey.Select{
Message: "Choose a context:",
Options: contextNames,
Expand All @@ -98,36 +49,9 @@ var contextCmd = &cobra.Command{
}
}

// Determine the target location for copying the file
destPath := os.Getenv("KUBECONFIG")
if destPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Failed to determine home directory: %v", err)
}
destPath = filepath.Join(homeDir, ".kube", "config")
}

// Ensure the destination directory exists
destDir := filepath.Dir(destPath)
err = os.MkdirAll(destDir, 0o755)
if err != nil {
log.Fatalf("Failed to create directory %s: %v", destDir, err)
}

// Load the kubeconfig file for the selected context
kubeconfig, err := clientcmd.LoadFromFile(contextMap[selectedContext])
if err != nil {
log.WithFields(log.Fields{"source": contextMap[selectedContext]}).Fatalf("Failed to parse kubeconfig file: %v", err)
}

// Update the current context
kubeconfig.CurrentContext = selectedContext

// Write the updated kubeconfig back to the file
err = clientcmd.WriteToFile(*kubeconfig, destPath)
if err != nil {
log.Fatalf("Error writing kubeconfig file: %v", err)
// Switch to the selected context
if err := contextManager.SwitchContext(contextMap[selectedContext], selectedContext); err != nil {
log.Fatalf("Failed to switch context: %v", err)
}

log.Infof("Switched to context '%s'", selectedContext)
Expand Down
63 changes: 10 additions & 53 deletions cmd/namespace.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,31 @@
package cmd

import (
"context"
"os"
"path/filepath"

"github.com/AlecAivazis/survey/v2"
"github.com/mirceanton/kubectl-switch/pkg/kubeconfig"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)

// namespaceCmd represents the command to change Kubernetes namespaces.
var namespaceManager = kubeconfig.NewNamespaceManager(manager)

var namespaceCmd = &cobra.Command{
Use: "namespace",
Aliases: []string{"ns"}, // Add this line to define the alias
Aliases: []string{"ns"},
Short: "Switch the active Kubernetes namespace",
Args: cobra.MaximumNArgs(1), // Accept at most one argument (the namespace name)
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Determine the location of the kubeconfig file
kubeconfigPath := os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Failed to determine home directory: %v", err)
}
kubeconfigPath = filepath.Join(homeDir, ".kube", "config")
}

// Build the Kubernetes client configuration
config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
log.Fatalf("Error building kubeconfig: %v", err)
}

// Create the Kubernetes clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatalf("Error creating Kubernetes client: %v", err)
}

// Get all namespaces
var namespaceNames []string
namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
for _, ns := range namespaces.Items {
namespaceNames = append(namespaceNames, ns.Name)
}
// Get all namespaces from the current context
namespaceNames, err := namespaceManager.GetNamespaces()
if err != nil {
log.Fatalf("Error listing namespaces: %v", err)
}

// Determine the target namespace
var selectedNamespace string
if len(args) == 1 {
// Non-interactive mode: use the provided ma,es[ace] name
selectedNamespace = args[0]
} else {
// Interactive mode: show list of clusters
prompt := &survey.Select{
Message: "Choose a namespace:",
Options: namespaceNames,
Expand All @@ -69,19 +36,9 @@ var namespaceCmd = &cobra.Command{
}
}

// Load the kubeconfig file
kubeconfig, err := clientcmd.LoadFromFile(kubeconfigPath)
if err != nil {
log.WithFields(log.Fields{"source": kubeconfigPath}).Fatalf("Failed to parse kubeconfig file: %v", err)
}

// Update the kubeconfig
kubeconfig.Contexts[kubeconfig.CurrentContext].Namespace = selectedNamespace

// Write the updated kubeconfig back to the file
err = clientcmd.WriteToFile(*kubeconfig, kubeconfigPath)
if err != nil {
log.Fatalf("Error writing kubeconfig file: %v", err)
// Switch to the selected namespace
if err := namespaceManager.SwitchNamespace(selectedNamespace); err != nil {
log.Fatalf("Failed to switch namespace: %v", err)
}

log.Infof("Switched to namespace '%s'", selectedNamespace)
Expand Down
18 changes: 17 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,36 @@ package cmd
import (
"os"

"github.com/mirceanton/kubectl-switch/pkg/kubeconfig"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var (
configDir string // Global flag for the kubeconfig directory
version string // The version of the tool, set at build time
manager = kubeconfig.NewManager()
)

var rootCmd = &cobra.Command{
Use: "kubectl-switch",
Short: "A tool to switch Kubernetes contexts",
Long: `kubectl-switch is a CLI tool to switch Kubernetes contexts from multiple kubeconfig files.`,
Version: version,
RunE: func(cmd *cobra.Command, args []string) error {
// Check if the "-" argument is provided to switch to the previous config
if len(args) == 1 && args[0] == "-" {
if err := manager.SwitchToPrevious(); err != nil {
if err == kubeconfig.ErrNoPreviousConfig {
log.Fatal("No previous configuration found")
} else {
log.Fatalf("Failed to switch to previous config: %v", err)
}
}
return nil
}
return cmd.Help()
},
}

func Execute() {
Expand All @@ -26,6 +43,5 @@ func Execute() {
}

func init() {
// Add any global flags here
rootCmd.PersistentFlags().StringVarP(&configDir, "kubeconfig-dir", "", "", "Directory containing kubeconfig files")
}
Loading