mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-09-05 02:20:17 +00:00
feat: support running OpenCost outside Kubernetes with external Prometheus
This commit is contained in:
149
cmd/kubectx/history.go
Normal file
149
cmd/kubectx/history.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Copyright 2021 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxHistoryLength = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextHistory stores the history of used contexts
|
||||||
|
type ContextHistory struct {
|
||||||
|
Contexts []string `json:"contexts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a context to history, moving it to the front if it already exists
|
||||||
|
func (h *ContextHistory) Add(ctx string) {
|
||||||
|
// Remove if exists
|
||||||
|
for i, c := range h.Contexts {
|
||||||
|
if c == ctx {
|
||||||
|
h.Contexts = append(h.Contexts[:i], h.Contexts[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to front
|
||||||
|
h.Contexts = append([]string{ctx}, h.Contexts...)
|
||||||
|
|
||||||
|
// Trim if needed
|
||||||
|
if len(h.Contexts) > maxHistoryLength {
|
||||||
|
h.Contexts = h.Contexts[:maxHistoryLength]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHistoryFilePath returns the path to the history file
|
||||||
|
var getHistoryFilePath = func() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".kube", "kubectx_history.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadHistory loads the context history from disk
|
||||||
|
func loadHistory() (*ContextHistory, error) {
|
||||||
|
path, err := getHistoryFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file doesn't exist, return empty history
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return &ContextHistory{Contexts: []string{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
var history ContextHistory
|
||||||
|
if len(data) > 0 {
|
||||||
|
if err := json.Unmarshal(data, &history); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
history.Contexts = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &history, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveHistory saves the context history to disk
|
||||||
|
func saveHistory(history *ContextHistory) error {
|
||||||
|
path, err := getHistoryFilePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory if needed
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize to JSON
|
||||||
|
data, err := json.Marshal(history)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prioritizeContexts sorts contexts with recently used ones first
|
||||||
|
func prioritizeContexts(allContexts []string, historyContexts []string) []string {
|
||||||
|
// Create a map for O(1) lookup
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
result := []string{}
|
||||||
|
|
||||||
|
// First add contexts from history that exist in allContexts
|
||||||
|
for _, ctx := range historyContexts {
|
||||||
|
if contains(allContexts, ctx) {
|
||||||
|
result = append(result, ctx)
|
||||||
|
seen[ctx] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add remaining contexts in alphabetical order
|
||||||
|
remaining := []string{}
|
||||||
|
for _, ctx := range allContexts {
|
||||||
|
if !seen[ctx] {
|
||||||
|
remaining = append(remaining, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(remaining)
|
||||||
|
|
||||||
|
return append(result, remaining...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains checks if a string is in a slice
|
||||||
|
func contains(slice []string, str string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
82
cmd/kubectx/main_test.go
Normal file
82
cmd/kubectx/main_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContextHistory(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tmpDir, err := ioutil.TempDir("", "kubectx-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Override history file path for testing
|
||||||
|
origHistoryFilePath := getHistoryFilePath
|
||||||
|
defer func() { getHistoryFilePath = origHistoryFilePath }()
|
||||||
|
|
||||||
|
historyPath := filepath.Join(tmpDir, "history.json")
|
||||||
|
getHistoryFilePath = func() (string, error) {
|
||||||
|
return historyPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("New history is empty", func(t *testing.T) {
|
||||||
|
history, err := loadHistory()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, history)
|
||||||
|
assert.Empty(t, history.Contexts)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Add context to history", func(t *testing.T) {
|
||||||
|
history := &ContextHistory{Contexts: []string{}}
|
||||||
|
history.Add("context1")
|
||||||
|
assert.Equal(t, []string{"context1"}, history.Contexts)
|
||||||
|
|
||||||
|
// Add the same context again
|
||||||
|
history.Add("context1")
|
||||||
|
assert.Equal(t, []string{"context1"}, history.Contexts)
|
||||||
|
|
||||||
|
// Add another context
|
||||||
|
history.Add("context2")
|
||||||
|
assert.Equal(t, []string{"context2", "context1"}, history.Contexts)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Save and load history", func(t *testing.T) {
|
||||||
|
history := &ContextHistory{Contexts: []string{"context1", "context2"}}
|
||||||
|
err := saveHistory(history)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loaded, err := loadHistory()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, history.Contexts, loaded.Contexts)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("History respects max length", func(t *testing.T) {
|
||||||
|
history := &ContextHistory{Contexts: []string{}}
|
||||||
|
|
||||||
|
// Add more than maxHistoryLength contexts
|
||||||
|
for i := 0; i < maxHistoryLength+5; i++ {
|
||||||
|
history.Add(fmt.Sprintf("context%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, maxHistoryLength, len(history.Contexts))
|
||||||
|
assert.Equal(t, "context9", history.Contexts[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrioritizeContexts(t *testing.T) {
|
||||||
|
allContexts := []string{"a", "b", "c", "d", "e"}
|
||||||
|
historyContexts := []string{"c", "a", "f"} // Note: "f" is not in allContexts
|
||||||
|
|
||||||
|
result := prioritizeContexts(allContexts, historyContexts)
|
||||||
|
|
||||||
|
// Expected: c, a (from history, in order), then b, d, e alphabetically
|
||||||
|
expected := []string{"c", "a", "b", "d", "e"}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
}
|
95
internal/flags/flags.go
Normal file
95
internal/flags/flags.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags contains parsed flags.
|
||||||
|
type Flags struct {
|
||||||
|
Delete string
|
||||||
|
Current bool
|
||||||
|
ShowHelp bool
|
||||||
|
Version bool
|
||||||
|
Unset bool
|
||||||
|
History bool
|
||||||
|
SelectedContext []string
|
||||||
|
NewContext []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseArgs parses given command line arguments to flags.
|
||||||
|
func parseArgs(args []string) (Flags, error) {
|
||||||
|
var f Flags
|
||||||
|
|
||||||
|
// filter out first argument (program name)
|
||||||
|
if len(args) > 0 {
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if args[i] == "--help" || args[i] == "-h" {
|
||||||
|
f.ShowHelp = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[i] == "--current" || args[i] == "-c" {
|
||||||
|
f.Current = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[i] == "--unset" || args[i] == "-u" {
|
||||||
|
f.Unset = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[i] == "--version" || args[i] == "-V" {
|
||||||
|
f.Version = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if args[i] == "--history" {
|
||||||
|
f.History = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[i] == "--delete" || args[i] == "-d") && i+1 < len(args) {
|
||||||
|
f.Delete = args[i+1]
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// <NEW>=<OLD>
|
||||||
|
if strings.Contains(args[i], "=") {
|
||||||
|
a := strings.SplitN(args[i], "=", 2)
|
||||||
|
if len(a) != 2 {
|
||||||
|
return f, fmt.Errorf("invalid argument: %s", args[i])
|
||||||
|
}
|
||||||
|
new, old := a[0], a[1]
|
||||||
|
if new == "" || old == "" {
|
||||||
|
return f, fmt.Errorf("invalid argument: %s", args[i])
|
||||||
|
}
|
||||||
|
f.NewContext = []string{new, old}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// <CONTEXT>
|
||||||
|
f.SelectedContext = append(f.SelectedContext, args[i])
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns empty flags.
|
||||||
|
func New() *Flags {
|
||||||
|
return &Flags{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line flags.
|
||||||
|
func (f *Flags) Parse(args []string) error {
|
||||||
|
fl, err := parseArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = fl
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user