mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
193 lines
6.4 KiB
Go
193 lines
6.4 KiB
Go
// Package kubeexec wraps kubectl/aws-cli invocations used by kctl-tui.
|
|
// All functions here have side effects (they run external processes) and
|
|
// are therefore not covered by unit tests; the pure logic they depend on
|
|
// lives in the kctl package instead.
|
|
//
|
|
// Every kubectl-related function takes an explicit context argument
|
|
// (passed as --context) instead of relying on/mutating the globally
|
|
// active kubectl context. This lets the panel act on multiple resolved
|
|
// contexts (e.g. beta and prod) without switching global state back and
|
|
// forth.
|
|
package kubeexec
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
func runOutput(name string, args ...string) (string, error) {
|
|
logCmd(name, args...)
|
|
cmd := exec.Command(name, args...)
|
|
var stdout, stderr strings.Builder
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
logErr(name, err)
|
|
prefix := fmt.Sprintf("%s %s failed: %v", name, strings.Join(args, " "), err)
|
|
msg := prefix
|
|
if s := strings.TrimSpace(stderr.String()); s != "" {
|
|
msg += "\n" + s
|
|
}
|
|
if s := strings.TrimSpace(stdout.String()); s != "" {
|
|
msg += "\n" + s
|
|
}
|
|
return "", fmt.Errorf("%s", msg)
|
|
}
|
|
out := strings.TrimSpace(stdout.String())
|
|
logOutput(name, out)
|
|
return out, nil
|
|
}
|
|
|
|
// kubectlArgs prepends a --context flag when context is non-empty.
|
|
func kubectlArgs(context string, args ...string) []string {
|
|
if context == "" {
|
|
return args
|
|
}
|
|
return append([]string{"--context", context}, args...)
|
|
}
|
|
|
|
type nsItem struct {
|
|
Metadata struct {
|
|
Name string `json:"name"`
|
|
Labels map[string]string `json:"labels"`
|
|
} `json:"metadata"`
|
|
}
|
|
|
|
type nsList struct {
|
|
Items []nsItem `json:"items"`
|
|
}
|
|
|
|
// GetNamespacesWithLabels returns a map of namespace name -> labels for all
|
|
// namespaces visible in the given context.
|
|
func GetNamespacesWithLabels(context string) (map[string]map[string]string, error) {
|
|
args := kubectlArgs(context, "get", "ns", "-o", "json")
|
|
out, err := runOutput("kubectl", args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var list nsList
|
|
if err := json.Unmarshal([]byte(out), &list); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string]map[string]string, len(list.Items))
|
|
for _, item := range list.Items {
|
|
result[item.Metadata.Name] = item.Metadata.Labels
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetDeployments lists deployment names in the given context/namespace.
|
|
func GetDeployments(context, namespace string) ([]string, error) {
|
|
args := kubectlArgs(context, "-n", namespace, "get", "deploy",
|
|
"-o", `jsonpath={range .items[*]}{.metadata.name}{"\n"}{end}`)
|
|
out, err := runOutput("kubectl", args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if out == "" {
|
|
return []string{}, nil
|
|
}
|
|
return strings.Split(out, "\n"), nil
|
|
}
|
|
|
|
// RolloutRestart triggers a rolling restart of a deployment.
|
|
func RolloutRestart(context, namespace, deployment string) (string, error) {
|
|
args := kubectlArgs(context, "-n", namespace, "rollout", "restart", "deploy/"+deployment)
|
|
return runOutput("kubectl", args...)
|
|
}
|
|
|
|
// RolloutStatus waits for and returns the rollout status of a deployment.
|
|
func RolloutStatus(context, namespace, deployment string) (string, error) {
|
|
args := kubectlArgs(context, "-n", namespace, "rollout", "status", "deploy/"+deployment)
|
|
return runOutput("kubectl", args...)
|
|
}
|
|
|
|
// GetSecretAllFields returns all fields of a Kubernetes secret, already
|
|
// base64-decoded into plain values.
|
|
func GetSecretAllFields(context, namespace, secretName string) (map[string]string, error) {
|
|
args := kubectlArgs(context, "-n", namespace, "get", "secret", secretName, "-o", "json")
|
|
out, err := runOutput("kubectl", args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var parsed struct {
|
|
Data map[string]string `json:"data"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[string]string, len(parsed.Data))
|
|
for k, v := range parsed.Data {
|
|
decoded, err := DecodeBase64(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode field %q: %w", k, err)
|
|
}
|
|
result[k] = decoded
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// DecodeBase64 decodes a base64-encoded Kubernetes secret value.
|
|
func DecodeBase64(value string) (string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(decoded), nil
|
|
}
|
|
|
|
// AnnotateForceSync sets the force-sync annotation on an ExternalSecret
|
|
// object to trigger an immediate re-sync from the upstream secret store.
|
|
func AnnotateForceSync(context, namespace, externalSecretName string, unixTimestamp int64) (string, error) {
|
|
annotation := fmt.Sprintf("force-sync=%d", unixTimestamp)
|
|
args := kubectlArgs(context, "-n", namespace, "annotate", "externalsecret",
|
|
externalSecretName, annotation, "--overwrite")
|
|
return runOutput("kubectl", args...)
|
|
}
|
|
|
|
// GetAWSSecretString fetches the SecretString of an AWS Secrets Manager
|
|
// secret via the aws-cli. The secret ID is computed from config templates
|
|
// (see internal/config), not looked up interactively.
|
|
func GetAWSSecretString(secretID, region string) (string, error) {
|
|
return runOutput("aws", "secretsmanager", "get-secret-value",
|
|
"--secret-id", secretID, "--region", region,
|
|
"--query", "SecretString", "--output", "text")
|
|
}
|
|
|
|
// CheckAWSAuth performs a cheap, fast call to verify the current AWS
|
|
// credentials/SSO session are valid. Returns nil if authenticated, or the
|
|
// underlying error (e.g. an expired SSO session) otherwise.
|
|
func CheckAWSAuth() error {
|
|
_, err := runOutput("aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text")
|
|
return err
|
|
}
|
|
|
|
// RunAWSLogin returns an *exec.Cmd for the given login command (e.g.
|
|
// "aws sso login"), split on whitespace. The caller is responsible for
|
|
// running it interactively (e.g. via tea.ExecProcess) since SSO login
|
|
// typically requires opening a browser and confirming a device code.
|
|
func RunAWSLogin(loginCommand string) *exec.Cmd {
|
|
parts := strings.Fields(loginCommand)
|
|
if len(parts) == 0 {
|
|
parts = []string{"aws", "sso", "login"}
|
|
}
|
|
return exec.Command(parts[0], parts[1:]...)
|
|
}
|
|
|
|
// CheckTool verifies that a named executable is available in PATH.
|
|
// Returns nil if found, or a descriptive error if not.
|
|
func CheckTool(name string) error {
|
|
_, err := exec.LookPath(name)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if name == "tmux" {
|
|
return fmt.Errorf("%q not found in PATH — install tmux (Linux/macOS) or psmux (Windows: scoop install psmux or cargo install psmux)", name)
|
|
}
|
|
return fmt.Errorf("%q not found in PATH — please install it first", name)
|
|
}
|