Initial commit.

This commit is contained in:
2025-10-11 17:19:11 +00:00
commit 24245e46e8
33 changed files with 4206 additions and 0 deletions

17
internal/config/config.go Normal file
View File

@@ -0,0 +1,17 @@
// Package config handles CLI configuration
package config
import (
"os"
)
// GetDaemonURL returns the daemon URL from environment or default
func GetDaemonURL() string {
// Check environment variable first
if url := os.Getenv("WILD_DAEMON_URL"); url != "" {
return url
}
// Use default matching daemon's port
return "http://localhost:5055"
}

View File

@@ -0,0 +1,44 @@
package config_test
import (
"fmt"
"github.com/wild-cloud/wild-central/wild/internal/config"
)
func ExampleGetValue() {
cfg := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
},
}
value := config.GetValue(cfg, "cloud.domain")
fmt.Println(value)
// Output: example.com
}
func ExampleValidatePaths() {
cfg := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
},
}
required := []string{"cloud.domain", "cloud.name", "network.subnet"}
missing := config.ValidatePaths(cfg, required)
fmt.Printf("Missing paths: %v\n", missing)
// Output: Missing paths: [cloud.name network.subnet]
}
func ExampleExpandTemplate() {
cfg := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
},
}
result, _ := config.ExpandTemplate("registry.{{ .cloud.domain }}", cfg)
fmt.Println(result)
// Output: registry.example.com
}

View File

@@ -0,0 +1,83 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// GetWildCLIDataDir returns the Wild CLI data directory
func GetWildCLIDataDir() string {
if dir := os.Getenv("WILD_CLI_DATA"); dir != "" {
return dir
}
home, err := os.UserHomeDir()
if err != nil {
return ".wildcloud"
}
return filepath.Join(home, ".wildcloud")
}
// GetCurrentInstance resolves the current instance using the priority cascade:
// 1. --instance flag (passed as parameter)
// 2. $WILD_CLI_DATA/current_instance file
// 3. Auto-select first instance from API
func GetCurrentInstance(flagInstance string, apiClient InstanceLister) (string, string, error) {
// Priority 1: --instance flag
if flagInstance != "" {
return flagInstance, "flag", nil
}
// Priority 2: current_instance file
dataDir := GetWildCLIDataDir()
currentFile := filepath.Join(dataDir, "current_instance")
if data, err := os.ReadFile(currentFile); err == nil {
instance := strings.TrimSpace(string(data))
if instance != "" {
return instance, "file", nil
}
}
// Priority 3: Auto-select first instance from API
if apiClient != nil {
instances, err := apiClient.ListInstances()
if err != nil {
return "", "", fmt.Errorf("no instance configured and failed to list instances: %w", err)
}
if len(instances) == 0 {
return "", "", fmt.Errorf("no instance configured and no instances available (create one with: wild instance create <name>)")
}
// Auto-select first instance
return instances[0], "auto", nil
}
return "", "", fmt.Errorf("no instance configured (use --instance flag or run: wild instance use <name>)")
}
// SetCurrentInstance persists the instance selection to file
func SetCurrentInstance(instance string) error {
dataDir := GetWildCLIDataDir()
// Create directory if it doesn't exist
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
currentFile := filepath.Join(dataDir, "current_instance")
// Write instance name to file
if err := os.WriteFile(currentFile, []byte(instance), 0644); err != nil {
return fmt.Errorf("failed to write current instance file: %w", err)
}
return nil
}
// InstanceLister is an interface for listing instances (allows for testing and dependency injection)
type InstanceLister interface {
ListInstances() ([]string, error)
}

View File

@@ -0,0 +1,210 @@
package config
import (
"os"
"path/filepath"
"testing"
)
// mockInstanceLister is a mock implementation of InstanceLister for testing
type mockInstanceLister struct {
instances []string
err error
}
func (m *mockInstanceLister) ListInstances() ([]string, error) {
if m.err != nil {
return nil, m.err
}
return m.instances, nil
}
func TestGetCurrentInstance(t *testing.T) {
// Save and restore env var
oldWildCLIData := os.Getenv("WILD_CLI_DATA")
defer os.Setenv("WILD_CLI_DATA", oldWildCLIData)
// Create temp directory for testing
tmpDir := t.TempDir()
os.Setenv("WILD_CLI_DATA", tmpDir)
tests := []struct {
name string
flagInstance string
fileInstance string
apiInstances []string
wantInstance string
wantSource string
wantErr bool
}{
{
name: "flag takes priority",
flagInstance: "flag-instance",
fileInstance: "file-instance",
apiInstances: []string{"api-instance"},
wantInstance: "flag-instance",
wantSource: "flag",
wantErr: false,
},
{
name: "file takes priority over api",
flagInstance: "",
fileInstance: "file-instance",
apiInstances: []string{"api-instance"},
wantInstance: "file-instance",
wantSource: "file",
wantErr: false,
},
{
name: "auto-select first from api",
flagInstance: "",
fileInstance: "",
apiInstances: []string{"first-instance", "second-instance"},
wantInstance: "first-instance",
wantSource: "auto",
wantErr: false,
},
{
name: "no instance available",
flagInstance: "",
fileInstance: "",
apiInstances: []string{},
wantInstance: "",
wantSource: "",
wantErr: true,
},
{
name: "no api client and no config",
flagInstance: "",
fileInstance: "",
apiInstances: nil,
wantInstance: "",
wantSource: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up file
currentFile := filepath.Join(tmpDir, "current_instance")
if tt.fileInstance != "" {
if err := os.WriteFile(currentFile, []byte(tt.fileInstance), 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
} else {
os.Remove(currentFile)
}
// Set up API client mock
var lister InstanceLister
if tt.apiInstances != nil {
lister = &mockInstanceLister{instances: tt.apiInstances}
}
// Test
gotInstance, gotSource, err := GetCurrentInstance(tt.flagInstance, lister)
if (err != nil) != tt.wantErr {
t.Errorf("GetCurrentInstance() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotInstance != tt.wantInstance {
t.Errorf("GetCurrentInstance() instance = %v, want %v", gotInstance, tt.wantInstance)
}
if gotSource != tt.wantSource {
t.Errorf("GetCurrentInstance() source = %v, want %v", gotSource, tt.wantSource)
}
})
}
}
func TestSetCurrentInstance(t *testing.T) {
// Save and restore env var
oldWildCLIData := os.Getenv("WILD_CLI_DATA")
defer os.Setenv("WILD_CLI_DATA", oldWildCLIData)
// Create temp directory for testing
tmpDir := t.TempDir()
os.Setenv("WILD_CLI_DATA", tmpDir)
// Test setting instance
testInstance := "test-instance"
err := SetCurrentInstance(testInstance)
if err != nil {
t.Fatalf("SetCurrentInstance() error = %v", err)
}
// Verify file was written
currentFile := filepath.Join(tmpDir, "current_instance")
data, err := os.ReadFile(currentFile)
if err != nil {
t.Fatalf("Failed to read current_instance file: %v", err)
}
if string(data) != testInstance {
t.Errorf("File content = %v, want %v", string(data), testInstance)
}
// Test updating instance
newInstance := "new-instance"
err = SetCurrentInstance(newInstance)
if err != nil {
t.Fatalf("SetCurrentInstance() error = %v", err)
}
data, err = os.ReadFile(currentFile)
if err != nil {
t.Fatalf("Failed to read current_instance file: %v", err)
}
if string(data) != newInstance {
t.Errorf("File content = %v, want %v", string(data), newInstance)
}
}
func TestGetWildCLIDataDir(t *testing.T) {
// Save and restore env var
oldWildCLIData := os.Getenv("WILD_CLI_DATA")
defer os.Setenv("WILD_CLI_DATA", oldWildCLIData)
tests := []struct {
name string
envVar string
wantDir string
}{
{
name: "custom directory from env",
envVar: "/custom/path",
wantDir: "/custom/path",
},
{
name: "default directory when env not set",
envVar: "",
// We can't predict the exact home directory, so we'll just check it's not empty
wantDir: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envVar != "" {
os.Setenv("WILD_CLI_DATA", tt.envVar)
} else {
os.Unsetenv("WILD_CLI_DATA")
}
gotDir := GetWildCLIDataDir()
if tt.wantDir != "" && gotDir != tt.wantDir {
t.Errorf("GetWildCLIDataDir() = %v, want %v", gotDir, tt.wantDir)
}
if tt.wantDir == "" && gotDir == "" {
t.Errorf("GetWildCLIDataDir() should not be empty when using default")
}
})
}
}

View File

@@ -0,0 +1,61 @@
package config
import (
"bytes"
"fmt"
"strings"
"text/template"
)
// ValidatePaths checks if all required paths exist in the config
// Returns a list of missing paths
func ValidatePaths(config map[string]interface{}, paths []string) []string {
var missing []string
for _, path := range paths {
if GetValue(config, path) == nil {
missing = append(missing, path)
}
}
return missing
}
// GetValue retrieves a nested value from config using dot notation
// Returns nil if the path doesn't exist
func GetValue(config map[string]interface{}, path string) interface{} {
parts := strings.Split(path, ".")
var current interface{} = config
for _, part := range parts {
m, ok := current.(map[string]interface{})
if !ok {
return nil
}
current = m[part]
if current == nil {
return nil
}
}
return current
}
// ExpandTemplate expands {{ .path.to.value }} templates in the string
// Returns the original string if no templates are present
func ExpandTemplate(tmpl string, config map[string]interface{}) (string, error) {
// Return original if no template markers
if !strings.Contains(tmpl, "{{") {
return tmpl, nil
}
t, err := template.New("config").Parse(tmpl)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, config); err != nil {
return "", fmt.Errorf("execute template: %w", err)
}
return buf.String(), nil
}

View File

@@ -0,0 +1,153 @@
package config
import (
"reflect"
"testing"
)
func TestGetValue(t *testing.T) {
config := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
"name": "test-cloud",
},
"simple": "value",
}
tests := []struct {
name string
path string
expected interface{}
}{
{"simple path", "simple", "value"},
{"nested path", "cloud.domain", "example.com"},
{"nested path 2", "cloud.name", "test-cloud"},
{"missing path", "missing", nil},
{"missing nested", "cloud.missing", nil},
{"invalid nested", "simple.nested", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetValue(config, tt.path)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("GetValue(%q) = %v, want %v", tt.path, result, tt.expected)
}
})
}
}
func TestValidatePaths(t *testing.T) {
config := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
"name": "test-cloud",
},
"network": map[string]interface{}{
"subnet": "10.0.0.0/24",
},
}
tests := []struct {
name string
paths []string
expected []string
}{
{
name: "all paths exist",
paths: []string{"cloud.domain", "cloud.name", "network.subnet"},
expected: nil,
},
{
name: "some paths missing",
paths: []string{"cloud.domain", "missing.path", "network.missing"},
expected: []string{"missing.path", "network.missing"},
},
{
name: "all paths missing",
paths: []string{"foo.bar", "baz.qux"},
expected: []string{"foo.bar", "baz.qux"},
},
{
name: "empty paths",
paths: []string{},
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidatePaths(config, tt.paths)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("ValidatePaths() = %v, want %v", result, tt.expected)
}
})
}
}
func TestExpandTemplate(t *testing.T) {
config := map[string]interface{}{
"cloud": map[string]interface{}{
"domain": "example.com",
"name": "test-cloud",
},
"port": 8080,
}
tests := []struct {
name string
template string
expected string
shouldErr bool
}{
{
name: "simple template",
template: "registry.{{ .cloud.domain }}",
expected: "registry.example.com",
},
{
name: "multiple templates",
template: "{{ .cloud.name }}.{{ .cloud.domain }}",
expected: "test-cloud.example.com",
},
{
name: "no template",
template: "plain-string",
expected: "plain-string",
},
{
name: "template with text",
template: "http://{{ .cloud.domain }}:{{ .port }}/api",
expected: "http://example.com:8080/api",
},
{
name: "invalid template syntax",
template: "{{ .cloud.domain",
shouldErr: true,
},
{
name: "missing value renders as no value",
template: "{{ .missing.path }}",
expected: "<no value>",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ExpandTemplate(tt.template, config)
if tt.shouldErr {
if err == nil {
t.Errorf("ExpandTemplate() expected error, got nil")
}
return
}
if err != nil {
t.Errorf("ExpandTemplate() unexpected error: %v", err)
return
}
if result != tt.expected {
t.Errorf("ExpandTemplate() = %q, want %q", result, tt.expected)
}
})
}
}