Skip to content
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
25 changes: 17 additions & 8 deletions cmd/switcher/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ var (
return err
}

kubeconfigPath, contextName, err := history.SetPreviousContext(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName)
kubeconfigPath, contextName, storeName, sourcePath, err := history.SetPreviousContext(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName, storeName, sourcePath)
return err
},
}
Expand All @@ -63,8 +63,8 @@ var (
return err
}

kubeconfigPath, contextName, err := history.SetLastContext(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName)
kubeconfigPath, contextName, storeName, sourcePath, err := history.SetLastContext(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName, storeName, sourcePath)
return err
},
}
Expand Down Expand Up @@ -126,8 +126,8 @@ var (
return err
}

kubeconfigPath, contextName, err := set_context.SetContext(args[0], stores, config, stateDirectory, noIndex, true)
reportNewContext(kubeconfigPath, contextName)
kubeconfigPath, contextName, storeName, sourcePath, err := set_context.SetContext(args[0], stores, config, stateDirectory, noIndex, true)
reportNewContext(kubeconfigPath, contextName, storeName, sourcePath)
return err
},
SilenceUsage: true,
Expand Down Expand Up @@ -257,13 +257,22 @@ func setFlagsForContextCommands(command *cobra.Command) {
"show preview of the selected kubeconfig. Possibly makes sense to disable when using vault as the kubeconfig store to prevent excessive requests against the API.")
}

func reportNewContext(kubeconfigPath *string, contextName *string) {
func reportNewContext(kubeconfigPath, contextName, storeName, sourcePath *string) {
if kubeconfigPath == nil || contextName == nil {
return
}

storeField := ""
if storeName != nil {
storeField = *storeName
}
sourceField := ""
if sourcePath != nil {
sourceField = *sourcePath
}

// print kubeconfig path and context name to std.out
// captured by calling script setting KUBECONFIG environment variable
// prefixed with "__ " to distinguish kubeconfig path output from other responses (e.g., errors, list of context, ...)
fmt.Printf("__ %s,%s", *kubeconfigPath, *contextName)
fmt.Printf("__ %s,%s,%s,%s", *kubeconfigPath, *contextName, storeField, sourceField)
}
4 changes: 2 additions & 2 deletions cmd/switcher/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ var (
return err
}

kubeconfigPath, contextName, err := history.SwitchToHistory(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName)
kubeconfigPath, contextName, storeName, sourcePath, err := history.SwitchToHistory(stores, config, stateDirectory, noIndex)
reportNewContext(kubeconfigPath, contextName, storeName, sourcePath)
return err
},
}
Expand Down
34 changes: 34 additions & 0 deletions cmd/switcher/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ function switch(){
KUBECONFIG_PATH="${remainder%%,*}"; remainder="${remainder#*,}"
SELECTED_CONTEXT="${remainder%%,*}"; remainder="${remainder#*,}"

# Parse optional store name and source path (fields 3 and 4)
if [[ "$remainder" == *,* ]]; then
KUBESWITCH_STORE="${remainder%%,*}"; remainder="${remainder#*,}"
KUBESWITCH_PATH="$remainder"
else
KUBESWITCH_STORE=""
KUBESWITCH_PATH=""
fi

if [ -z ${KUBECONFIG_PATH+x} ]; then
# KUBECONFIG_PATH is not set
printf "%s\n" "$RESPONSE"
Expand All @@ -91,6 +100,8 @@ function switch(){
fi

export KUBECONFIG="$KUBECONFIG_PATH"
export KUBESWITCH_STORE
export KUBESWITCH_PATH
printf "switched to context %s\n" "$SELECTED_CONTEXT"
}`

Expand Down Expand Up @@ -147,6 +158,18 @@ function kubeswitch
return
end

# Parse optional store name and source path
if set -q split_info[3]
set -gx KUBESWITCH_STORE $split_info[3]
else
set -gx KUBESWITCH_STORE ""
end
if set -q split_info[4]
set -gx KUBESWITCH_PATH $split_info[4]
else
set -gx KUBESWITCH_PATH ""
end

if test ! -e "$KUBECONFIG_PATH"
echo "ERROR: \"$KUBECONFIG_PATH\" does not exist"
return 1
Expand Down Expand Up @@ -220,6 +243,17 @@ function kubeswitch {
Write-Output $KUBECONFIG_PATH
$SELECTED_CONTEXT = $remainder.split(",")[1]

if ($remainder.split(",").Count -ge 3) {
$env:KUBESWITCH_STORE = $remainder.split(",")[2]
} else {
$env:KUBESWITCH_STORE = ""
}
if ($remainder.split(",").Count -ge 4) {
$env:KUBESWITCH_PATH = $remainder.split(",")[3]
} else {
$env:KUBESWITCH_PATH = ""
}

if (-not $KUBECONFIG_PATH) {
Write-Output $RESPONSE
return
Expand Down
14 changes: 9 additions & 5 deletions cmd/switcher/switcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,29 +98,33 @@ var (
return currentContextCmd.RunE(cmd, args)
}

// Handle special cases first
if len(args) > 0 {
switch args[0] {
case "-":
return previousContextCmd.RunE(cmd, args[1:])
case ".":
return lastContextCmd.RunE(cmd, args[1:])
default:
return setContextCmd.RunE(cmd, args)
}
}

// Common path: initialize once, call Switcher with args[0] or ""
stores, config, err := initialize()
if err != nil {
return err
}

// config file setting overwrites the command line default (--showPreview true)
if showPreview && config.ShowPreview != nil && !*config.ShowPreview {
showPreview = false
}

kubeconfigPath, contextName, err := pkg.Switcher(stores, config, stateDirectory, noIndex, showPreview)
reportNewContext(kubeconfigPath, contextName)
desiredContext := ""
if len(args) > 0 {
desiredContext = args[0]
}

kubeconfigPath, contextName, storeName, sourcePath, err := pkg.Switcher(stores, config, stateDirectory, noIndex, showPreview, desiredContext)
reportNewContext(kubeconfigPath, contextName, storeName, sourcePath)
return err
},
SilenceUsage: true,
Expand Down
149 changes: 139 additions & 10 deletions pkg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sync"
"time"

storeutil "github.com/danielfoehrkn/kubeswitch/pkg/store"
historyutil "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/history/util"
"github.com/hashicorp/go-multierror"
"github.com/ktr0731/go-fuzzyfinder"
Expand Down Expand Up @@ -64,10 +65,26 @@ var (
logger = logrus.New()
)

func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDir string, noIndex, showPreview bool) (*string, *string, error) {
// waitForSearchResults waits for at least one search result or timeout
func waitForSearchResults(timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
allKubeconfigContextNamesLock.RLock()
count := len(allKubeconfigContextNames)
allKubeconfigContextNamesLock.RUnlock()
if count > 0 {
// Give more time for additional results
time.Sleep(300 * time.Millisecond)
return
}
time.Sleep(50 * time.Millisecond)
}
}

func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDir string, noIndex, showPreview bool, desiredContext string) (*string, *string, *string, *string, error) {
c, err := DoSearch(stores, config, stateDir, noIndex)
if err != nil {
return nil, nil, err
return nil, nil, nil, nil, err
}

// here we asynchronously read from the result channel until the wait group is done (call wg.Done for all stores)
Expand Down Expand Up @@ -115,13 +132,124 @@ func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDi

defer logSearchErrors()

// If a desired context was provided, handle exact/partial matching
if desiredContext != "" {
// Wait for search results to populate
waitForSearchResults(10 * time.Second)

// Take a snapshot of current contexts and aliases
allKubeconfigContextNamesLock.RLock()
contextsCopy := make([]string, len(allKubeconfigContextNames))
copy(contextsCopy, allKubeconfigContextNames)
allKubeconfigContextNamesLock.RUnlock()

aliasToContextLock.RLock()
aliasesCopy := make(map[string]string, len(aliasToContext))
for k, v := range aliasToContext {
aliasesCopy[k] = v
}
aliasToContextLock.RUnlock()

// Check for exact match (case-sensitive) in contexts or aliases
exactMatch := ""
for _, name := range contextsCopy {
if name == desiredContext {
exactMatch = name
break
}
}
if exactMatch == "" {
for alias := range aliasesCopy {
if alias == desiredContext {
exactMatch = alias
break
}
}
}

// If exact match, switch immediately without showing picker
if exactMatch != "" {
kubeconfigPath := readFromContextToPathMapping(exactMatch)
storeID := readFromPathToStoreID(kubeconfigPath)
store := kindToStore[storeID]
tags := readFromPathToTagsMapping(kubeconfigPath)

kubeconfigData, err := store.GetKubeconfigForPath(kubeconfigPath, tags)
if err != nil {
return nil, nil, nil, nil, err
}

kubeconfig, err := kubeconfigutil.NewKubeconfig(kubeconfigData)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to parse selected kubeconfig: %v", err)
}

selectedContext := exactMatch
contextForHistory := selectedContext

if len(store.GetContextPrefix(kubeconfigPath)) > 0 && strings.HasPrefix(selectedContext, store.GetContextPrefix(kubeconfigPath)) {
selectedContext = strings.TrimPrefix(selectedContext, fmt.Sprintf("%s/", store.GetContextPrefix(kubeconfigPath)))
}

if err := kubeconfig.SetContext(selectedContext, aliasutil.GetContextForAlias(selectedContext, aliasToContext), store.GetContextPrefix(kubeconfigPath)); err != nil {
return nil, nil, nil, nil, err
}

if err := kubeconfig.SetKubeswitchContext(contextForHistory); err != nil {
return nil, nil, nil, nil, err
}

tempKubeconfigPath, err := kubeconfig.WriteKubeconfigFile()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to write temporary kubeconfig file: %v", err)
}

ns, err := kubeconfig.NamespaceOfContext(kubeconfig.GetCurrentContext())
if err != nil {
logger.Warnf("failed to get namespace: %v", err)
} else if err := historyutil.AppendToHistory(contextForHistory, ns); err != nil {
logger.Warnf("failed to append to history: %v", err)
}

storeName := storeutil.DeriveStoreName(store, kubeconfigPath)
return &tempKubeconfigPath, &selectedContext, &storeName, &kubeconfigPath, nil
}

// No exact match - filter by substring (case-insensitive)
lowerDesired := strings.ToLower(desiredContext)
var partialMatches []string
seen := make(map[string]bool)

for _, name := range contextsCopy {
if strings.Contains(strings.ToLower(name), lowerDesired) {
partialMatches = append(partialMatches, name)
seen[name] = true
}
}

for alias := range aliasesCopy {
if strings.Contains(strings.ToLower(alias), lowerDesired) && !seen[alias] {
partialMatches = append(partialMatches, alias)
}
}

if len(partialMatches) == 0 {
return nil, nil, nil, nil, fmt.Errorf("no contexts matching %q", desiredContext)
}

// Replace global list with filtered matches for picker
allKubeconfigContextNamesLock.Lock()
allKubeconfigContextNames = partialMatches
allKubeconfigContextNamesLock.Unlock()
}

kubeconfigPath, selectedContext, err := showFuzzySearch(kindToStore, showPreview)
if err != nil {
return nil, nil, err
return nil, nil, nil, nil, err
}

if len(kubeconfigPath) == 0 {
return nil, nil, nil
return nil, nil, nil, nil, nil
}

// map back kubeconfig path to the store kind
Expand All @@ -136,12 +264,12 @@ func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDi
// use the store to get the kubeconfig for the selected kubeconfig path
kubeconfigData, err := store.GetKubeconfigForPath(kubeconfigPath, tags)
if err != nil {
return nil, nil, err
return nil, nil, nil, nil, err
}

kubeconfig, err := kubeconfigutil.NewKubeconfig(kubeconfigData)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse selected kubeconfig. Please check if this file is a valid kubeconfig: %v", err)
return nil, nil, nil, nil, fmt.Errorf("failed to parse selected kubeconfig. Please check if this file is a valid kubeconfig: %v", err)
}

// save the original selected context for the history
Expand All @@ -154,17 +282,17 @@ func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDi
}

if err := kubeconfig.SetContext(selectedContext, aliasutil.GetContextForAlias(selectedContext, aliasToContext), store.GetContextPrefix(kubeconfigPath)); err != nil {
return nil, nil, err
return nil, nil, nil, nil, err
}

if err := kubeconfig.SetKubeswitchContext(contextForHistory); err != nil {
return nil, nil, err
return nil, nil, nil, nil, err
}

// write a temporary kubeconfig file and return the path
tempKubeconfigPath, err := kubeconfig.WriteKubeconfigFile()
if err != nil {
return nil, nil, fmt.Errorf("failed to write temporary kubeconfig file: %v", err)
return nil, nil, nil, nil, fmt.Errorf("failed to write temporary kubeconfig file: %v", err)
}

// get namespace for current context
Expand All @@ -175,7 +303,8 @@ func Switcher(stores []storetypes.KubeconfigStore, config *types.Config, stateDi
logger.Warnf("failed to append context to history file: %v", err)
}

return &tempKubeconfigPath, &selectedContext, nil
storeName := storeutil.DeriveStoreName(store, kubeconfigPath)
return &tempKubeconfigPath, &selectedContext, &storeName, &kubeconfigPath, nil
}

// writeIndex tries to write the Index file for the kubeconfig store
Expand Down
Loading