Add internal/kctl and internal/config packages with unit tests

This commit is contained in:
Stefan Koelle
2026-08-09 11:21:47 +02:00
parent 2dae206ec4
commit dc8bcc555f
6 changed files with 253 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
package kctl
import "sort"
// DistinctLabelValues returns the sorted, unique, non-empty values of the
// given label key across a set of namespaces (name -> labels map).
func DistinctLabelValues(namespaces map[string]map[string]string, labelKey string) []string {
seen := map[string]bool{}
for _, labels := range namespaces {
if v, ok := labels[labelKey]; ok && v != "" {
seen[v] = true
}
}
result := make([]string, 0, len(seen))
for v := range seen {
result = append(result, v)
}
sort.Strings(result)
return result
}
// NamespacesForLabelValue returns the sorted namespace names whose labelKey
// matches the given value exactly.
func NamespacesForLabelValue(namespaces map[string]map[string]string, labelKey, value string) []string {
result := make([]string, 0)
for ns, labels := range namespaces {
if labels[labelKey] == value {
result = append(result, ns)
}
}
sort.Strings(result)
return result
}
+45
View File
@@ -0,0 +1,45 @@
package kctl
import (
"reflect"
"testing"
)
func testNamespaces() map[string]map[string]string {
return map[string]map[string]string{
"ns-one": {"team-label": "team-a"},
"ns-two": {"team-label": "team-b"},
"ns-three": {"team-label": "team-a"},
"ns-four": {}, // no label at all
}
}
func TestDistinctLabelValues(t *testing.T) {
got := DistinctLabelValues(testNamespaces(), "team-label")
want := []string{"team-a", "team-b"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestDistinctLabelValues_UnknownKey(t *testing.T) {
got := DistinctLabelValues(testNamespaces(), "does-not-exist")
if len(got) != 0 {
t.Fatalf("expected empty result, got %v", got)
}
}
func TestNamespacesForLabelValue(t *testing.T) {
got := NamespacesForLabelValue(testNamespaces(), "team-label", "team-a")
want := []string{"ns-one", "ns-three"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestNamespacesForLabelValue_NoMatch(t *testing.T) {
got := NamespacesForLabelValue(testNamespaces(), "team-label", "team-z")
if len(got) != 0 {
t.Fatalf("expected empty result, got %v", got)
}
}
+35
View File
@@ -0,0 +1,35 @@
// Package kctl contains the core, non-interactive logic of kctl-tui.
// Functions here are pure (no kubectl/tmux side effects) so they can be
// unit tested without a live cluster.
package kctl
// ContextPair groups a set of related kubectl contexts that should be
// switchable via TAB while keeping the same namespace (e.g. staging/prod).
type ContextPair struct {
Name string `yaml:"name"`
Contexts []string `yaml:"contexts"`
}
// FindNextContext returns the next context in the same pair/group as
// current, cycling through the group. Returns "", false if current is not
// part of any configured pair.
func FindNextContext(current string, pairs []ContextPair) (string, bool) {
for _, pair := range pairs {
idx := indexOf(pair.Contexts, current)
if idx == -1 {
continue
}
next := pair.Contexts[(idx+1)%len(pair.Contexts)]
return next, true
}
return "", false
}
func indexOf(items []string, target string) int {
for i, v := range items {
if v == target {
return i
}
}
return -1
}
+48
View File
@@ -0,0 +1,48 @@
package kctl
import "testing"
func TestFindNextContext_TwoWayToggle(t *testing.T) {
pairs := []ContextPair{
{Name: "env-pair-1", Contexts: []string{"ctx-a", "ctx-b"}},
}
next, ok := FindNextContext("ctx-a", pairs)
if !ok || next != "ctx-b" {
t.Fatalf("expected ctx-b, got %q (ok=%v)", next, ok)
}
next, ok = FindNextContext("ctx-b", pairs)
if !ok || next != "ctx-a" {
t.Fatalf("expected ctx-a, got %q (ok=%v)", next, ok)
}
}
func TestFindNextContext_Rotation(t *testing.T) {
pairs := []ContextPair{
{Name: "rotation", Contexts: []string{"a", "b", "c"}},
}
next, ok := FindNextContext("c", pairs)
if !ok || next != "a" {
t.Fatalf("expected wraparound to a, got %q (ok=%v)", next, ok)
}
}
func TestFindNextContext_NotConfigured(t *testing.T) {
pairs := []ContextPair{
{Name: "env-pair-1", Contexts: []string{"ctx-a", "ctx-b"}},
}
_, ok := FindNextContext("unrelated-context", pairs)
if ok {
t.Fatalf("expected ok=false for a context with no configured pair")
}
}
func TestFindNextContext_NoPairsConfigured(t *testing.T) {
_, ok := FindNextContext("ctx-a", nil)
if ok {
t.Fatalf("expected ok=false when no pairs are configured")
}
}