First commit of golang CLI.

This commit is contained in:
2025-08-31 11:51:11 -07:00
parent 4ca06aecb6
commit f0a2098f11
51 changed files with 8840 additions and 0 deletions

130
wild-cli/internal/external/base.go vendored Normal file
View File

@@ -0,0 +1,130 @@
package external
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"time"
)
// Tool represents an external command-line tool
type Tool interface {
Name() string
BinaryName() string
IsInstalled() bool
Version() (string, error)
Execute(ctx context.Context, args ...string) ([]byte, error)
ExecuteWithInput(ctx context.Context, input string, args ...string) ([]byte, error)
}
// BaseTool provides common functionality for external tools
type BaseTool struct {
name string
binaryName string
binaryPath string
timeout time.Duration
}
// NewBaseTool creates a new base tool
func NewBaseTool(name, binaryName string) *BaseTool {
return &BaseTool{
name: name,
binaryName: binaryName,
timeout: 5 * time.Minute, // Default timeout
}
}
// Name returns the tool name
func (t *BaseTool) Name() string {
return t.name
}
// BinaryName returns the binary name
func (t *BaseTool) BinaryName() string {
return t.binaryName
}
// IsInstalled checks if the tool is available in PATH
func (t *BaseTool) IsInstalled() bool {
if t.binaryPath == "" {
path, err := exec.LookPath(t.binaryName)
if err != nil {
return false
}
t.binaryPath = path
}
return true
}
// Version returns the tool version
func (t *BaseTool) Version() (string, error) {
if !t.IsInstalled() {
return "", fmt.Errorf("tool %s not installed", t.name)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
output, err := t.Execute(ctx, "--version")
if err != nil {
// Try alternative version flags
output, err = t.Execute(ctx, "version")
if err != nil {
output, err = t.Execute(ctx, "-v")
if err != nil {
return "", fmt.Errorf("getting version: %w", err)
}
}
}
return string(output), nil
}
// Execute runs the tool with given arguments
func (t *BaseTool) Execute(ctx context.Context, args ...string) ([]byte, error) {
return t.ExecuteWithInput(ctx, "", args...)
}
// ExecuteWithInput runs the tool with stdin input
func (t *BaseTool) ExecuteWithInput(ctx context.Context, input string, args ...string) ([]byte, error) {
if !t.IsInstalled() {
return nil, fmt.Errorf("tool %s not installed", t.name)
}
// Create command with timeout
ctx, cancel := context.WithTimeout(ctx, t.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, t.binaryPath, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if input != "" {
cmd.Stdin = bytes.NewBufferString(input)
}
// Set environment
cmd.Env = os.Environ()
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("executing %s %v: %w\nstderr: %s",
t.name, args, err, stderr.String())
}
return stdout.Bytes(), nil
}
// SetTimeout sets the execution timeout
func (t *BaseTool) SetTimeout(timeout time.Duration) {
t.timeout = timeout
}
// SetBinaryPath explicitly sets the binary path (useful for testing)
func (t *BaseTool) SetBinaryPath(path string) {
t.binaryPath = path
}

226
wild-cli/internal/external/kubectl.go vendored Normal file
View File

@@ -0,0 +1,226 @@
package external
import (
"context"
"fmt"
"strings"
)
// KubectlTool wraps kubectl operations
type KubectlTool struct {
*BaseTool
kubeconfig string
}
// NewKubectlTool creates a new kubectl tool wrapper
func NewKubectlTool() *KubectlTool {
return &KubectlTool{
BaseTool: NewBaseTool("kubectl", "kubectl"),
}
}
// SetKubeconfig sets the kubeconfig file path
func (k *KubectlTool) SetKubeconfig(path string) {
k.kubeconfig = path
}
// Apply applies Kubernetes manifests
func (k *KubectlTool) Apply(ctx context.Context, manifests []string, namespace string, dryRun bool) error {
for _, manifest := range manifests {
args := []string{"apply", "-f", "-"}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if dryRun {
args = append(args, "--dry-run=client")
}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
_, err := k.ExecuteWithInput(ctx, manifest, args...)
if err != nil {
return fmt.Errorf("applying manifest: %w", err)
}
}
return nil
}
// ApplyKustomize applies using kustomize
func (k *KubectlTool) ApplyKustomize(ctx context.Context, path string, namespace string, dryRun bool) error {
args := []string{"apply", "-k", path}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if dryRun {
args = append(args, "--dry-run=client")
}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
_, err := k.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("applying kustomize: %w", err)
}
return nil
}
// Delete deletes Kubernetes resources
func (k *KubectlTool) Delete(ctx context.Context, resource, name, namespace string, ignoreNotFound bool) error {
args := []string{"delete", resource, name}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if ignoreNotFound {
args = append(args, "--ignore-not-found=true")
}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
_, err := k.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("deleting resource: %w", err)
}
return nil
}
// CreateSecret creates a Kubernetes secret
func (k *KubectlTool) CreateSecret(ctx context.Context, name, namespace string, data map[string]string) error {
// First try to delete existing secret
_ = k.Delete(ctx, "secret", name, namespace, true)
args := []string{"create", "secret", "generic", name}
for key, value := range data {
args = append(args, "--from-literal="+key+"="+value)
}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
_, err := k.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("creating secret: %w", err)
}
return nil
}
// GetResource gets a Kubernetes resource
func (k *KubectlTool) GetResource(ctx context.Context, resource, name, namespace string) ([]byte, error) {
args := []string{"get", resource}
if name != "" {
args = append(args, name)
}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
args = append(args, "-o", "yaml")
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
output, err := k.Execute(ctx, args...)
if err != nil {
return nil, fmt.Errorf("getting resource: %w", err)
}
return output, nil
}
// WaitForDeletion waits for a resource to be deleted
func (k *KubectlTool) WaitForDeletion(ctx context.Context, resource, name, namespace string, timeout string) error {
args := []string{"wait", "--for=delete", resource + "/" + name}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if timeout != "" {
args = append(args, "--timeout="+timeout)
}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
_, err := k.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("waiting for deletion: %w", err)
}
return nil
}
// GetNodes returns information about cluster nodes
func (k *KubectlTool) GetNodes(ctx context.Context) ([]byte, error) {
args := []string{"get", "nodes", "-o", "wide"}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
output, err := k.Execute(ctx, args...)
if err != nil {
return nil, fmt.Errorf("getting nodes: %w", err)
}
return output, nil
}
// GetServiceAccount gets a service account token
func (k *KubectlTool) GetServiceAccountToken(ctx context.Context, serviceAccount, namespace string) (string, error) {
// Get the service account
args := []string{"get", "serviceaccount", serviceAccount, "--namespace", namespace, "-o", "jsonpath={.secrets[0].name}"}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
secretName, err := k.Execute(ctx, args...)
if err != nil {
return "", fmt.Errorf("getting service account secret: %w", err)
}
secretNameStr := strings.TrimSpace(string(secretName))
if secretNameStr == "" {
return "", fmt.Errorf("no secret found for service account %s", serviceAccount)
}
// Get the token from the secret
args = []string{"get", "secret", secretNameStr, "--namespace", namespace, "-o", "jsonpath={.data.token}"}
if k.kubeconfig != "" {
args = append([]string{"--kubeconfig", k.kubeconfig}, args...)
}
tokenBytes, err := k.Execute(ctx, args...)
if err != nil {
return "", fmt.Errorf("getting token from secret: %w", err)
}
return string(tokenBytes), nil
}

93
wild-cli/internal/external/manager.go vendored Normal file
View File

@@ -0,0 +1,93 @@
package external
import (
"context"
"fmt"
)
// Manager coordinates external tools
type Manager struct {
kubectl *KubectlTool
talosctl *TalosctlTool
restic *ResticTool
tools map[string]Tool
}
// NewManager creates a new tool manager
func NewManager() *Manager {
kubectl := NewKubectlTool()
talosctl := NewTalosctlTool()
restic := NewResticTool()
tools := map[string]Tool{
"kubectl": kubectl,
"talosctl": talosctl,
"restic": restic,
}
return &Manager{
kubectl: kubectl,
talosctl: talosctl,
restic: restic,
tools: tools,
}
}
// Kubectl returns the kubectl tool
func (m *Manager) Kubectl() *KubectlTool {
return m.kubectl
}
// Talosctl returns the talosctl tool
func (m *Manager) Talosctl() *TalosctlTool {
return m.talosctl
}
// Restic returns the restic tool
func (m *Manager) Restic() *ResticTool {
return m.restic
}
// CheckTools verifies that required tools are available
func (m *Manager) CheckTools(ctx context.Context, required []string) error {
missing := make([]string, 0)
for _, toolName := range required {
tool, exists := m.tools[toolName]
if !exists {
missing = append(missing, toolName)
continue
}
if !tool.IsInstalled() {
missing = append(missing, toolName)
}
}
if len(missing) > 0 {
return fmt.Errorf("missing required tools: %v", missing)
}
return nil
}
// GetToolVersion returns the version of a tool
func (m *Manager) GetToolVersion(toolName string) (string, error) {
tool, exists := m.tools[toolName]
if !exists {
return "", fmt.Errorf("tool %s not found", toolName)
}
return tool.Version()
}
// ListTools returns information about all tools
func (m *Manager) ListTools() map[string]bool {
status := make(map[string]bool)
for name, tool := range m.tools {
status[name] = tool.IsInstalled()
}
return status
}

288
wild-cli/internal/external/restic.go vendored Normal file
View File

@@ -0,0 +1,288 @@
package external
import (
"context"
"fmt"
"os"
"strings"
)
// ResticTool wraps restic backup operations
type ResticTool struct {
*BaseTool
repository string
password string
}
// NewResticTool creates a new restic tool wrapper
func NewResticTool() *ResticTool {
return &ResticTool{
BaseTool: NewBaseTool("restic", "restic"),
}
}
// SetRepository sets the restic repository
func (r *ResticTool) SetRepository(repo string) {
r.repository = repo
}
// SetPassword sets the restic password
func (r *ResticTool) SetPassword(password string) {
r.password = password
}
// InitRepository initializes a new restic repository
func (r *ResticTool) InitRepository(ctx context.Context) error {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"init"}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
_, err := cmd.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("initializing repository: %w", err)
}
return nil
}
// Backup creates a backup
func (r *ResticTool) Backup(ctx context.Context, paths []string, excludes []string, tags []string) error {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"backup"}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
// Add paths
args = append(args, paths...)
// Add excludes
for _, exclude := range excludes {
args = append(args, "--exclude", exclude)
}
// Add tags
for _, tag := range tags {
args = append(args, "--tag", tag)
}
_, err := cmd.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("creating backup: %w", err)
}
return nil
}
// Restore restores from backup
func (r *ResticTool) Restore(ctx context.Context, snapshotID, target string) error {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"restore", snapshotID, "--target", target}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
_, err := cmd.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("restoring backup: %w", err)
}
return nil
}
// ListSnapshots lists available snapshots
func (r *ResticTool) ListSnapshots(ctx context.Context, tags []string) ([]byte, error) {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"snapshots", "--json"}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
// Add tag filters
for _, tag := range tags {
args = append(args, "--tag", tag)
}
output, err := cmd.Execute(ctx, args...)
if err != nil {
return nil, fmt.Errorf("listing snapshots: %w", err)
}
return output, nil
}
// Check verifies repository integrity
func (r *ResticTool) Check(ctx context.Context) error {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"check"}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
_, err := cmd.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("checking repository: %w", err)
}
return nil
}
// Forget removes snapshots
func (r *ResticTool) Forget(ctx context.Context, keepLast int, keepDaily int, keepWeekly int, keepMonthly int, prune bool) error {
env := r.getEnvironment()
cmd := r.BaseTool
originalEnv := os.Environ()
defer func() {
os.Clearenv()
for _, kv := range originalEnv {
if k, v, found := strings.Cut(kv, "="); found {
_ = os.Setenv(k, v)
}
}
}()
// Set environment variables
for k, v := range env {
_ = os.Setenv(k, v)
}
args := []string{"forget"}
if r.repository != "" {
args = append(args, "--repo", r.repository)
}
if keepLast > 0 {
args = append(args, "--keep-last", fmt.Sprintf("%d", keepLast))
}
if keepDaily > 0 {
args = append(args, "--keep-daily", fmt.Sprintf("%d", keepDaily))
}
if keepWeekly > 0 {
args = append(args, "--keep-weekly", fmt.Sprintf("%d", keepWeekly))
}
if keepMonthly > 0 {
args = append(args, "--keep-monthly", fmt.Sprintf("%d", keepMonthly))
}
if prune {
args = append(args, "--prune")
}
_, err := cmd.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("forgetting snapshots: %w", err)
}
return nil
}
// getEnvironment returns environment variables for restic
func (r *ResticTool) getEnvironment() map[string]string {
env := make(map[string]string)
if r.repository != "" {
env["RESTIC_REPOSITORY"] = r.repository
}
if r.password != "" {
env["RESTIC_PASSWORD"] = r.password
}
return env
}

285
wild-cli/internal/external/talosctl.go vendored Normal file
View File

@@ -0,0 +1,285 @@
package external
import (
"context"
"fmt"
)
// TalosctlTool wraps talosctl operations
type TalosctlTool struct {
*BaseTool
endpoints []string
nodes []string
talosconfig string
}
// NewTalosctlTool creates a new talosctl tool wrapper
func NewTalosctlTool() *TalosctlTool {
return &TalosctlTool{
BaseTool: NewBaseTool("talosctl", "talosctl"),
}
}
// SetEndpoints sets the Talos API endpoints
func (t *TalosctlTool) SetEndpoints(endpoints []string) {
t.endpoints = endpoints
}
// SetNodes sets the target nodes
func (t *TalosctlTool) SetNodes(nodes []string) {
t.nodes = nodes
}
// SetTalosconfig sets the talosconfig file path
func (t *TalosctlTool) SetTalosconfig(path string) {
t.talosconfig = path
}
// GenerateConfig generates Talos configuration files
func (t *TalosctlTool) GenerateConfig(ctx context.Context, clusterName, clusterEndpoint, outputDir string) error {
args := []string{
"gen", "config",
clusterName,
clusterEndpoint,
"--output-dir", outputDir,
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("generating config: %w", err)
}
return nil
}
// ApplyConfig applies configuration to nodes
func (t *TalosctlTool) ApplyConfig(ctx context.Context, configFile string, insecure bool) error {
args := []string{"apply-config"}
if insecure {
args = append(args, "--insecure")
}
args = append(args, "--file", configFile)
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("applying config: %w", err)
}
return nil
}
// Bootstrap bootstraps the cluster
func (t *TalosctlTool) Bootstrap(ctx context.Context) error {
args := []string{"bootstrap"}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("bootstrapping cluster: %w", err)
}
return nil
}
// Kubeconfig retrieves the kubeconfig
func (t *TalosctlTool) Kubeconfig(ctx context.Context, outputFile string, force bool) error {
args := []string{"kubeconfig"}
if outputFile != "" {
args = append(args, outputFile)
}
if force {
args = append(args, "--force")
}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("getting kubeconfig: %w", err)
}
return nil
}
// Health checks the health of nodes
func (t *TalosctlTool) Health(ctx context.Context) ([]byte, error) {
args := []string{"health"}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
output, err := t.Execute(ctx, args...)
if err != nil {
return nil, fmt.Errorf("checking health: %w", err)
}
return output, nil
}
// List lists Talos resources
func (t *TalosctlTool) List(ctx context.Context, resource string) ([]byte, error) {
args := []string{"list", resource}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
output, err := t.Execute(ctx, args...)
if err != nil {
return nil, fmt.Errorf("listing %s: %w", resource, err)
}
return output, nil
}
// Patch applies patches to node configuration
func (t *TalosctlTool) Patch(ctx context.Context, patchFile string, configType string) error {
args := []string{"patch", configType, "--patch-file", patchFile}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("patching config: %w", err)
}
return nil
}
// Reboot reboots nodes
func (t *TalosctlTool) Reboot(ctx context.Context) error {
args := []string{"reboot"}
if len(t.endpoints) > 0 {
args = append(args, "--endpoints")
args = append(args, t.endpoints...)
}
if len(t.nodes) > 0 {
args = append(args, "--nodes")
args = append(args, t.nodes...)
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("rebooting nodes: %w", err)
}
return nil
}
// GenerateSecrets generates cluster secrets
func (t *TalosctlTool) GenerateSecrets(ctx context.Context) error {
args := []string{"gen", "secrets"}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("generating secrets: %w", err)
}
return nil
}
// GenerateConfigWithSecrets generates configuration with existing secrets
func (t *TalosctlTool) GenerateConfigWithSecrets(ctx context.Context, clusterName, clusterEndpoint, secretsFile string) error {
args := []string{
"gen", "config",
"--with-secrets", secretsFile,
clusterName,
clusterEndpoint,
}
if t.talosconfig != "" {
args = append([]string{"--talosconfig", t.talosconfig}, args...)
}
_, err := t.Execute(ctx, args...)
if err != nil {
return fmt.Errorf("generating config with secrets: %w", err)
}
return nil
}