mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-08-31 08:12:57 +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