11 KiB
11 KiB
External Dependencies Strategy for Wild CLI
Overview
The Wild CLI needs to interface with multiple external tools that the current bash scripts depend on. This document outlines the strategy for managing these dependencies in the Go implementation.
Current External Dependencies
Primary Tools
- kubectl - Kubernetes cluster management
- yq - YAML processing and manipulation
- gomplate - Template processing
- kustomize - Kubernetes manifest processing
- talosctl - Talos Linux node management
- restic - Backup and restore operations
- helm - Helm chart operations (limited use)
System Tools
- openssl - Random string generation (fallback to /dev/urandom)
Go Integration Strategies
1. Tool Abstraction Layer
Create interface-based abstractions for all external tools to enable:
- Testing with mocks
- Fallback strategies
- Version compatibility handling
- Platform-specific executable resolution
// pkg/external/interfaces.go
type ExternalTool interface {
Name() string
IsInstalled() bool
Version() (string, error)
Execute(ctx context.Context, args ...string) ([]byte, error)
}
type KubectlClient interface {
ExternalTool
Apply(ctx context.Context, manifests []string, namespace string, dryRun bool) error
Delete(ctx context.Context, resource, name, namespace string) error
CreateSecret(ctx context.Context, name, namespace string, data map[string]string) error
GetResource(ctx context.Context, resource, name, namespace string) ([]byte, error)
}
type YqClient interface {
ExternalTool
Query(ctx context.Context, path, file string) (string, error)
Set(ctx context.Context, path, value, file string) error
Exists(ctx context.Context, path, file string) bool
}
type GomplateClient interface {
ExternalTool
Process(ctx context.Context, template string, contexts map[string]string) (string, error)
ProcessFile(ctx context.Context, templateFile string, contexts map[string]string) (string, error)
}
2. Native Go Implementations (Preferred)
Where possible, replace external tools with native Go implementations:
YAML Processing (Replace yq)
// internal/config/yaml.go
import (
"gopkg.in/yaml.v3"
"github.com/mikefarah/yq/v4/pkg/yqlib"
)
type YAMLManager struct {
configPath string
secretsPath string
}
func (y *YAMLManager) Get(path string) (interface{}, error) {
// Use yq Go library directly instead of external binary
return yqlib.NewYamlDecoder().Process(y.configPath, path)
}
func (y *YAMLManager) Set(path string, value interface{}) error {
// Direct YAML manipulation using Go libraries
return y.updateYAMLFile(path, value)
}
Template Processing (Replace gomplate)
// internal/config/template.go
import (
"text/template"
"github.com/Masterminds/sprig/v3"
)
type TemplateEngine struct {
configData map[string]interface{}
secretsData map[string]interface{}
}
func (t *TemplateEngine) Process(templateContent string) (string, error) {
tmpl := template.New("wild").Funcs(sprig.TxtFuncMap())
// Add custom functions like gomplate
tmpl = tmpl.Funcs(template.FuncMap{
"config": func(path string) interface{} {
return t.getValueByPath(t.configData, path)
},
"secret": func(path string) interface{} {
return t.getValueByPath(t.secretsData, path)
},
})
parsed, err := tmpl.Parse(templateContent)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = parsed.Execute(&buf, map[string]interface{}{
"config": t.configData,
"secrets": t.secretsData,
})
return buf.String(), err
}
Kubernetes Client (Native Go)
// internal/kubernetes/client.go
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
)
type Client struct {
clientset kubernetes.Interface
config *rest.Config
}
func (c *Client) ApplyManifest(ctx context.Context, manifest string, namespace string) error {
// Parse YAML into unstructured objects
decoder := yaml.NewYAMLToJSONDecoder(strings.NewReader(manifest))
for {
var obj unstructured.Unstructured
if err := decoder.Decode(&obj); err != nil {
if err == io.EOF {
break
}
return err
}
// Apply using dynamic client
err := c.applyUnstructured(ctx, &obj, namespace)
if err != nil {
return err
}
}
return nil
}
3. External Tool Wrappers (When Native Not Available)
For tools where native Go implementations aren't practical:
// internal/external/base.go
type ToolExecutor struct {
name string
binaryPath string
timeout time.Duration
}
func (t *ToolExecutor) Execute(ctx context.Context, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, t.binaryPath, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("executing %s: %w\nstderr: %s", t.name, err, stderr.String())
}
return stdout.Bytes(), nil
}
// internal/external/kubectl.go
type KubectlWrapper struct {
*ToolExecutor
kubeconfig string
}
func (k *KubectlWrapper) Apply(ctx context.Context, manifest string, namespace string, dryRun bool) error {
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...)
}
cmd := exec.CommandContext(ctx, k.binaryPath, args...)
cmd.Stdin = strings.NewReader(manifest)
return cmd.Run()
}
4. Tool Discovery and Installation
// internal/external/discovery.go
type ToolManager struct {
tools map[string]*ToolInfo
}
type ToolInfo struct {
Name string
BinaryName string
MinVersion string
InstallURL string
CheckCommand []string
IsRequired bool
NativeAvailable bool
}
func (tm *ToolManager) DiscoverTools() error {
for name, tool := range tm.tools {
path, err := exec.LookPath(tool.BinaryName)
if err != nil {
if tool.IsRequired && !tool.NativeAvailable {
return fmt.Errorf("required tool %s not found: %w", name, err)
}
continue
}
tool.BinaryPath = path
// Check version compatibility
version, err := tm.getVersion(tool)
if err != nil {
return fmt.Errorf("checking version of %s: %w", name, err)
}
if !tm.isVersionCompatible(version, tool.MinVersion) {
return fmt.Errorf("tool %s version %s is not compatible (minimum: %s)",
name, version, tool.MinVersion)
}
}
return nil
}
func (tm *ToolManager) PreferNative(toolName string) bool {
tool, exists := tm.tools[toolName]
return exists && tool.NativeAvailable
}
Implementation Priority
Phase 1: Core Native Implementations
- YAML processing - Replace yq with native Go YAML libraries
- Template processing - Replace gomplate with text/template + sprig
- Configuration management - Native config/secrets handling
- Kubernetes client - Use client-go instead of kubectl where possible
Phase 2: External Tool Wrappers
- kubectl wrapper - For operations not covered by client-go
- talosctl wrapper - For Talos-specific operations
- restic wrapper - For backup/restore operations
- kustomize wrapper - For manifest processing
Phase 3: Enhanced Features
- Automatic tool installation - Download missing tools automatically
- Version management - Handle multiple tool versions
- Container fallbacks - Use containerized tools when local ones unavailable
- Parallel execution - Run independent tool operations concurrently
Tool-Specific Strategies
kubectl
- Primary: Native client-go for most operations
- Fallback: kubectl binary for edge cases (port-forward, proxy, etc.)
- Kustomize integration: Use sigs.k8s.io/kustomize/api
yq
- Primary: Native YAML processing with gopkg.in/yaml.v3
- Advanced queries: Use github.com/mikefarah/yq/v4 Go library
- No external binary needed
gomplate
- Primary: Native template processing with text/template
- Functions: Use github.com/Masterminds/sprig for template functions
- Custom functions: Implement config/secret accessors natively
talosctl
- Only option: External binary wrapper
- Strategy: Embed in releases or auto-download
- Platform handling: talosctl-linux-amd64, talosctl-darwin-amd64, etc.
restic
- Only option: External binary wrapper
- Strategy: Auto-download appropriate version
- Configuration: Handle repository initialization and config
Error Handling and Recovery
// internal/external/manager.go
func (tm *ToolManager) ExecuteWithFallback(ctx context.Context, operation Operation) error {
// Try native implementation first
if tm.hasNativeImplementation(operation.Tool) {
err := tm.executeNative(ctx, operation)
if err == nil {
return nil
}
// Log native failure, try external tool
log.Warnf("Native %s implementation failed: %v, trying external tool",
operation.Tool, err)
}
// Use external tool wrapper
return tm.executeExternal(ctx, operation)
}
Testing Strategy
// internal/external/mock.go
type MockKubectl struct {
ApplyCalls []ApplyCall
ApplyError error
}
func (m *MockKubectl) Apply(ctx context.Context, manifest, namespace string, dryRun bool) error {
m.ApplyCalls = append(m.ApplyCalls, ApplyCall{
Manifest: manifest,
Namespace: namespace,
DryRun: dryRun,
})
return m.ApplyError
}
// Test usage
func TestAppDeploy(t *testing.T) {
mockKubectl := &MockKubectl{}
deployer := &AppDeployer{
kubectl: mockKubectl,
}
err := deployer.Deploy(context.Background(), "test-app", false)
assert.NoError(t, err)
assert.Len(t, mockKubectl.ApplyCalls, 1)
assert.Equal(t, "test-app", mockKubectl.ApplyCalls[0].Namespace)
}
Platform Compatibility
// internal/external/platform.go
func getPlatformBinary(toolName string) string {
base := toolName
if runtime.GOOS == "windows" {
base += ".exe"
}
return base
}
func getToolDownloadURL(toolName, version string) string {
switch toolName {
case "talosctl":
return fmt.Sprintf("https://github.com/siderolabs/talos/releases/download/%s/talosctl-%s-%s",
version, runtime.GOOS, runtime.GOARCH)
case "restic":
return fmt.Sprintf("https://github.com/restic/restic/releases/download/%s/restic_%s_%s_%s.bz2",
version, strings.TrimPrefix(version, "v"), runtime.GOOS, runtime.GOARCH)
}
return ""
}
This strategy provides a robust foundation for managing external dependencies while maximizing the use of native Go implementations for better performance, testing, and cross-platform compatibility.