407 lines
11 KiB
Markdown
407 lines
11 KiB
Markdown
# 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
|
|
1. **kubectl** - Kubernetes cluster management
|
|
2. **yq** - YAML processing and manipulation
|
|
3. **gomplate** - Template processing
|
|
4. **kustomize** - Kubernetes manifest processing
|
|
5. **talosctl** - Talos Linux node management
|
|
6. **restic** - Backup and restore operations
|
|
7. **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**
|
|
|
|
```go
|
|
// 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)
|
|
```go
|
|
// 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)
|
|
```go
|
|
// 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)
|
|
```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:
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
1. **YAML processing** - Replace yq with native Go YAML libraries
|
|
2. **Template processing** - Replace gomplate with text/template + sprig
|
|
3. **Configuration management** - Native config/secrets handling
|
|
4. **Kubernetes client** - Use client-go instead of kubectl where possible
|
|
|
|
### Phase 2: External Tool Wrappers
|
|
1. **kubectl wrapper** - For operations not covered by client-go
|
|
2. **talosctl wrapper** - For Talos-specific operations
|
|
3. **restic wrapper** - For backup/restore operations
|
|
4. **kustomize wrapper** - For manifest processing
|
|
|
|
### Phase 3: Enhanced Features
|
|
1. **Automatic tool installation** - Download missing tools automatically
|
|
2. **Version management** - Handle multiple tool versions
|
|
3. **Container fallbacks** - Use containerized tools when local ones unavailable
|
|
4. **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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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. |