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

View File

@@ -0,0 +1,378 @@
package apps
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// App represents an application in the catalog
type App struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Category string `yaml:"category"`
Homepage string `yaml:"homepage"`
Source string `yaml:"source"`
Tags []string `yaml:"tags"`
Requires []string `yaml:"requires"`
Provides map[string]string `yaml:"provides"`
Config map[string]interface{} `yaml:"config"`
}
// Catalog manages the application catalog
type Catalog struct {
cacheDir string
apps []App
loaded bool
}
// NewCatalog creates a new app catalog
func NewCatalog(cacheDir string) *Catalog {
return &Catalog{
cacheDir: cacheDir,
}
}
// LoadCatalog loads the app catalog from cache or remote source
func (c *Catalog) LoadCatalog() error {
if c.loaded {
return nil
}
// Try to load from cache first
catalogPath := filepath.Join(c.cacheDir, "catalog.yaml")
if err := c.loadFromFile(catalogPath); err == nil {
c.loaded = true
return nil
}
// If cache fails, try to fetch from remote
if err := c.fetchRemoteCatalog(); err != nil {
return fmt.Errorf("failed to load catalog: %w", err)
}
c.loaded = true
return nil
}
// loadFromFile loads catalog from a local file
func (c *Catalog) loadFromFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading catalog file: %w", err)
}
var catalogData struct {
Apps []App `yaml:"apps"`
}
if err := yaml.Unmarshal(data, &catalogData); err != nil {
return fmt.Errorf("parsing catalog YAML: %w", err)
}
c.apps = catalogData.Apps
return nil
}
// fetchRemoteCatalog fetches catalog from remote source
func (c *Catalog) fetchRemoteCatalog() error {
// For now, create a default catalog
// In production, this would fetch from a remote URL
defaultCatalog := []App{
{
Name: "nextcloud",
Description: "Self-hosted file sync and share platform",
Version: "latest",
Category: "productivity",
Homepage: "https://nextcloud.com",
Source: "https://github.com/wild-cloud/app-nextcloud",
Tags: []string{"files", "sync", "collaboration"},
Requires: []string{"postgresql"},
Provides: map[string]string{"files": "nextcloud"},
},
{
Name: "postgresql",
Description: "Powerful, open source object-relational database",
Version: "15",
Category: "database",
Homepage: "https://postgresql.org",
Source: "https://github.com/wild-cloud/app-postgresql",
Tags: []string{"database", "sql"},
Provides: map[string]string{"database": "postgresql"},
},
{
Name: "traefik",
Description: "Modern HTTP reverse proxy and load balancer",
Version: "v3.0",
Category: "infrastructure",
Homepage: "https://traefik.io",
Source: "https://github.com/wild-cloud/app-traefik",
Tags: []string{"proxy", "loadbalancer", "ingress"},
Provides: map[string]string{"ingress": "traefik"},
},
{
Name: "monitoring",
Description: "Prometheus and Grafana monitoring stack",
Version: "latest",
Category: "infrastructure",
Homepage: "https://prometheus.io",
Source: "https://github.com/wild-cloud/app-monitoring",
Tags: []string{"monitoring", "metrics", "alerting"},
Provides: map[string]string{"monitoring": "prometheus"},
},
}
c.apps = defaultCatalog
// Save to cache
return c.saveCatalogToCache()
}
// saveCatalogToCache saves the catalog to cache
func (c *Catalog) saveCatalogToCache() error {
catalogData := struct {
Apps []App `yaml:"apps"`
}{
Apps: c.apps,
}
data, err := yaml.Marshal(catalogData)
if err != nil {
return fmt.Errorf("marshaling catalog: %w", err)
}
catalogPath := filepath.Join(c.cacheDir, "catalog.yaml")
if err := os.MkdirAll(filepath.Dir(catalogPath), 0755); err != nil {
return fmt.Errorf("creating cache directory: %w", err)
}
if err := os.WriteFile(catalogPath, data, 0644); err != nil {
return fmt.Errorf("writing catalog file: %w", err)
}
return nil
}
// ListApps returns all apps in the catalog
func (c *Catalog) ListApps() ([]App, error) {
if err := c.LoadCatalog(); err != nil {
return nil, err
}
return c.apps, nil
}
// FindApp finds an app by name
func (c *Catalog) FindApp(name string) (*App, error) {
if err := c.LoadCatalog(); err != nil {
return nil, err
}
for _, app := range c.apps {
if app.Name == name {
return &app, nil
}
}
return nil, fmt.Errorf("app '%s' not found in catalog", name)
}
// SearchApps searches for apps by name or tag
func (c *Catalog) SearchApps(query string) ([]App, error) {
if err := c.LoadCatalog(); err != nil {
return nil, err
}
var results []App
query = strings.ToLower(query)
for _, app := range c.apps {
// Check name
if strings.Contains(strings.ToLower(app.Name), query) {
results = append(results, app)
continue
}
// Check description
if strings.Contains(strings.ToLower(app.Description), query) {
results = append(results, app)
continue
}
// Check tags
for _, tag := range app.Tags {
if strings.Contains(strings.ToLower(tag), query) {
results = append(results, app)
break
}
}
}
return results, nil
}
// FetchApp downloads an app template to cache
func (c *Catalog) FetchApp(name string) error {
app, err := c.FindApp(name)
if err != nil {
return err
}
appCacheDir := filepath.Join(c.cacheDir, "apps", name)
if err := os.MkdirAll(appCacheDir, 0755); err != nil {
return fmt.Errorf("creating app cache directory: %w", err)
}
// For now, create a basic app template
// In production, this would clone from app.Source
if err := c.createAppTemplate(app, appCacheDir); err != nil {
return fmt.Errorf("creating app template: %w", err)
}
return nil
}
// createAppTemplate creates a basic app template structure
func (c *Catalog) createAppTemplate(app *App, dir string) error {
// Create manifest.yaml
manifest := map[string]interface{}{
"name": app.Name,
"version": app.Version,
"description": app.Description,
"requires": app.Requires,
"provides": app.Provides,
"config": app.Config,
}
manifestData, err := yaml.Marshal(manifest)
if err != nil {
return fmt.Errorf("marshaling manifest: %w", err)
}
manifestPath := filepath.Join(dir, "manifest.yaml")
if err := os.WriteFile(manifestPath, manifestData, 0644); err != nil {
return fmt.Errorf("writing manifest: %w", err)
}
// Create basic kubernetes manifests
if err := c.createKubernetesManifests(app, dir); err != nil {
return fmt.Errorf("creating kubernetes manifests: %w", err)
}
return nil
}
// createKubernetesManifests creates basic Kubernetes manifest templates
func (c *Catalog) createKubernetesManifests(app *App, dir string) error {
// Create namespace.yaml
namespace := fmt.Sprintf(`apiVersion: v1
kind: Namespace
metadata:
name: %s
labels:
app: %s
`, app.Name, app.Name)
if err := os.WriteFile(filepath.Join(dir, "namespace.yaml"), []byte(namespace), 0644); err != nil {
return fmt.Errorf("writing namespace.yaml: %w", err)
}
// Create basic deployment template
deployment := fmt.Sprintf(`apiVersion: apps/v1
kind: Deployment
metadata:
name: %s
namespace: %s
spec:
replicas: {{.config.%s.replicas | default 1}}
selector:
matchLabels:
app: %s
template:
metadata:
labels:
app: %s
spec:
containers:
- name: %s
image: {{.config.%s.image | default "%s:latest"}}
ports:
- containerPort: 8080
`, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name)
if err := os.WriteFile(filepath.Join(dir, "deployment.yaml"), []byte(deployment), 0644); err != nil {
return fmt.Errorf("writing deployment.yaml: %w", err)
}
// Create service.yaml
service := fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
name: %s
namespace: %s
spec:
selector:
app: %s
ports:
- port: 80
targetPort: 8080
type: ClusterIP
`, app.Name, app.Name, app.Name)
if err := os.WriteFile(filepath.Join(dir, "service.yaml"), []byte(service), 0644); err != nil {
return fmt.Errorf("writing service.yaml: %w", err)
}
// Create kustomization.yaml
kustomization := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
`
if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(kustomization), 0644); err != nil {
return fmt.Errorf("writing kustomization.yaml: %w", err)
}
return nil
}
// IsAppCached checks if an app is cached locally
func (c *Catalog) IsAppCached(name string) bool {
appCacheDir := filepath.Join(c.cacheDir, "apps", name)
manifestPath := filepath.Join(appCacheDir, "manifest.yaml")
_, err := os.Stat(manifestPath)
return err == nil
}
// GetCachedApps returns list of cached apps
func (c *Catalog) GetCachedApps() ([]string, error) {
appsDir := filepath.Join(c.cacheDir, "apps")
entries, err := os.ReadDir(appsDir)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, fmt.Errorf("reading apps directory: %w", err)
}
var cachedApps []string
for _, entry := range entries {
if entry.IsDir() {
manifestPath := filepath.Join(appsDir, entry.Name(), "manifest.yaml")
if _, err := os.Stat(manifestPath); err == nil {
cachedApps = append(cachedApps, entry.Name())
}
}
}
return cachedApps, nil
}

View File

@@ -0,0 +1,298 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// Manager handles configuration and secrets files
type Manager struct {
configPath string
secretsPath string
}
// NewManager creates a new configuration manager
func NewManager(configPath, secretsPath string) *Manager {
return &Manager{
configPath: configPath,
secretsPath: secretsPath,
}
}
// Get retrieves a value from the config file using dot-notation path
func (m *Manager) Get(path string) (interface{}, error) {
return m.getValue(m.configPath, path)
}
// Set sets a value in the config file using dot-notation path
func (m *Manager) Set(path, value string) error {
return m.setValue(m.configPath, path, value)
}
// GetSecret retrieves a value from the secrets file using dot-notation path
func (m *Manager) GetSecret(path string) (interface{}, error) {
return m.getValue(m.secretsPath, path)
}
// SetSecret sets a value in the secrets file using dot-notation path
func (m *Manager) SetSecret(path, value string) error {
return m.setValue(m.secretsPath, path, value)
}
// getValue retrieves a value from a YAML file using dot-notation path
func (m *Manager) getValue(filePath, path string) (interface{}, error) {
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("file not found: %s", filePath)
}
// Read file
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("reading file: %w", err)
}
// Parse YAML
var yamlData interface{}
if err := yaml.Unmarshal(data, &yamlData); err != nil {
return nil, fmt.Errorf("parsing YAML: %w", err)
}
// Navigate to the specified path
value, err := m.navigatePath(yamlData, path)
if err != nil {
return nil, err
}
return value, nil
}
// setValue sets a value in a YAML file using dot-notation path
func (m *Manager) setValue(filePath, path, value string) error {
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("creating directory: %w", err)
}
// Read existing file or create empty structure
var yamlData interface{}
if data, err := os.ReadFile(filePath); err == nil {
if err := yaml.Unmarshal(data, &yamlData); err != nil {
return fmt.Errorf("parsing existing YAML: %w", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("reading file: %w", err)
}
// If no existing data, start with empty map
if yamlData == nil {
yamlData = make(map[string]interface{})
}
// Parse the value as YAML to handle different types
var parsedValue interface{}
if err := yaml.Unmarshal([]byte(value), &parsedValue); err != nil {
// If it fails to parse as YAML, treat as string
parsedValue = value
}
// Set the value at the specified path
if err := m.setValueAtPath(yamlData, path, parsedValue); err != nil {
return fmt.Errorf("setting value at path: %w", err)
}
// Marshal back to YAML
data, err := yaml.Marshal(yamlData)
if err != nil {
return fmt.Errorf("marshaling YAML: %w", err)
}
// Write file
if err := os.WriteFile(filePath, data, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
return nil
}
// navigatePath navigates through a nested data structure using dot-notation path
func (m *Manager) navigatePath(data interface{}, path string) (interface{}, error) {
if path == "" {
return data, nil
}
parts := m.parsePath(path)
current := data
for _, part := range parts {
if part.isArray {
// Handle array access like "items[0]"
slice, ok := current.([]interface{})
if !ok {
return nil, fmt.Errorf("path component %s is not an array", part.key)
}
if part.index < 0 || part.index >= len(slice) {
return nil, fmt.Errorf("array index %d out of range for %s", part.index, part.key)
}
current = slice[part.index]
} else {
// Handle map access
m, ok := current.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("path component %s is not a map", part.key)
}
var exists bool
current, exists = m[part.key]
if !exists {
return nil, nil // Key not found
}
}
}
return current, nil
}
// setValueAtPath sets a value at the specified path, creating nested structures as needed
func (m *Manager) setValueAtPath(data interface{}, path string, value interface{}) error {
if path == "" {
return fmt.Errorf("empty path")
}
parts := m.parsePath(path)
current := data
// Navigate to the parent of the target
for _, part := range parts[:len(parts)-1] {
if part.isArray {
slice, ok := current.([]interface{})
if !ok {
return fmt.Errorf("path component %s is not an array", part.key)
}
if part.index < 0 || part.index >= len(slice) {
return fmt.Errorf("array index %d out of range for %s", part.index, part.key)
}
current = slice[part.index]
} else {
m, ok := current.(map[string]interface{})
if !ok {
return fmt.Errorf("path component %s is not a map", part.key)
}
next, exists := m[part.key]
if !exists {
// Create new map for next level
next = make(map[string]interface{})
m[part.key] = next
}
current = next
}
}
// Set the final value
finalPart := parts[len(parts)-1]
if finalPart.isArray {
return fmt.Errorf("cannot set array element directly")
} else {
m, ok := current.(map[string]interface{})
if !ok {
return fmt.Errorf("cannot set value on non-map")
}
m[finalPart.key] = value
}
return nil
}
// pathPart represents a single component in a dot-notation path
type pathPart struct {
key string
isArray bool
index int
}
// parsePath parses a dot-notation path into components
func (m *Manager) parsePath(path string) []pathPart {
var parts []pathPart
components := strings.Split(path, ".")
for _, component := range components {
if strings.Contains(component, "[") && strings.Contains(component, "]") {
// Handle array syntax like "items[0]"
openBracket := strings.Index(component, "[")
closeBracket := strings.Index(component, "]")
key := component[:openBracket]
indexStr := component[openBracket+1 : closeBracket]
if key != "" {
parts = append(parts, pathPart{
key: key,
isArray: false,
})
}
if index, err := strconv.Atoi(indexStr); err == nil {
parts = append(parts, pathPart{
key: key,
isArray: true,
index: index,
})
}
} else {
parts = append(parts, pathPart{
key: component,
isArray: false,
})
}
}
return parts
}
// LoadConfig loads the entire config file as a map
func (m *Manager) LoadConfig() (map[string]interface{}, error) {
data, err := m.getValue(m.configPath, "")
if err != nil {
return nil, err
}
if data == nil {
return make(map[string]interface{}), nil
}
configMap, ok := data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("config file is not a valid YAML map")
}
return configMap, nil
}
// LoadSecrets loads the entire secrets file as a map
func (m *Manager) LoadSecrets() (map[string]interface{}, error) {
data, err := m.getValue(m.secretsPath, "")
if err != nil {
return nil, err
}
if data == nil {
return make(map[string]interface{}), nil
}
secretsMap, ok := data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("secrets file is not a valid YAML map")
}
return secretsMap, nil
}

View File

@@ -0,0 +1,139 @@
package config
import (
"bytes"
"fmt"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
)
// TemplateEngine handles template processing with Wild Cloud context
type TemplateEngine struct {
configData map[string]interface{}
secretsData map[string]interface{}
}
// NewTemplateEngine creates a new template engine with config and secrets context
func NewTemplateEngine(configMgr *Manager) (*TemplateEngine, error) {
configData, err := configMgr.LoadConfig()
if err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
secretsData, err := configMgr.LoadSecrets()
if err != nil {
return nil, fmt.Errorf("loading secrets: %w", err)
}
return &TemplateEngine{
configData: configData,
secretsData: secretsData,
}, nil
}
// Process processes template content with Wild Cloud context
func (t *TemplateEngine) Process(templateContent string) (string, error) {
// Create template with sprig functions
tmpl := template.New("wild").Funcs(sprig.TxtFuncMap())
// Add Wild Cloud specific functions
tmpl = tmpl.Funcs(template.FuncMap{
// Config access function - matches gomplate .config
"config": func(path string) interface{} {
return t.getValueByPath(t.configData, path)
},
// Secret access function - matches gomplate .secrets
"secret": func(path string) interface{} {
return t.getValueByPath(t.secretsData, path)
},
// Direct access to config data - matches gomplate behavior
"getConfig": func(path string) interface{} {
return t.getValueByPath(t.configData, path)
},
// Direct access to secret data - matches gomplate behavior
"getSecret": func(path string) interface{} {
return t.getValueByPath(t.secretsData, path)
},
})
// Parse template
parsed, err := tmpl.Parse(templateContent)
if err != nil {
return "", fmt.Errorf("parsing template: %w", err)
}
// Execute template with context
var buf bytes.Buffer
context := map[string]interface{}{
"config": t.configData,
"secrets": t.secretsData,
}
err = parsed.Execute(&buf, context)
if err != nil {
return "", fmt.Errorf("executing template: %w", err)
}
return buf.String(), nil
}
// ProcessFile processes a template file with Wild Cloud context
func (t *TemplateEngine) ProcessFile(templateFile string) (string, error) {
// Read file content
content, err := readFile(templateFile)
if err != nil {
return "", fmt.Errorf("reading template file: %w", err)
}
return t.Process(string(content))
}
// getValueByPath retrieves a value from nested data using dot-notation path
func (t *TemplateEngine) getValueByPath(data interface{}, path string) interface{} {
if path == "" {
return data
}
parts := strings.Split(path, ".")
current := data
for _, part := range parts {
switch v := current.(type) {
case map[string]interface{}:
var exists bool
current, exists = v[part]
if !exists {
return nil
}
case map[interface{}]interface{}:
var exists bool
current, exists = v[part]
if !exists {
return nil
}
default:
return nil
}
}
return current
}
// readFile is a helper to read file contents
func readFile(filename string) ([]byte, error) {
// This would be implemented to read from filesystem
// For now, returning empty to avoid import cycles
return nil, fmt.Errorf("file reading not implemented yet")
}
// CompileTemplate is a convenience function for one-off template processing
func CompileTemplate(templateContent string, configMgr *Manager) (string, error) {
engine, err := NewTemplateEngine(configMgr)
if err != nil {
return "", err
}
return engine.Process(templateContent)
}

View File

@@ -0,0 +1,215 @@
package environment
import (
"context"
"fmt"
"os"
"path/filepath"
)
// Environment manages Wild Cloud environment variables and paths
type Environment struct {
wcRoot string
wcHome string
}
// New creates a new Environment instance
func New() *Environment {
env := &Environment{}
// Initialize from environment variables set by root command
if wcRoot := os.Getenv("WC_ROOT"); wcRoot != "" {
env.wcRoot = wcRoot
}
if wcHome := os.Getenv("WC_HOME"); wcHome != "" {
env.wcHome = wcHome
}
// If WC_HOME is not set, try to detect it
if env.wcHome == "" {
if detected, err := env.DetectWCHome(); err == nil && detected != "" {
env.wcHome = detected
// Set environment variable for child processes
_ = os.Setenv("WC_HOME", detected)
}
}
return env
}
// WCRoot returns the Wild Cloud installation directory
func (e *Environment) WCRoot() string {
return e.wcRoot
}
// WCHome returns the Wild Cloud project directory
func (e *Environment) WCHome() string {
return e.wcHome
}
// SetWCRoot sets the Wild Cloud installation directory
func (e *Environment) SetWCRoot(path string) {
e.wcRoot = path
}
// SetWCHome sets the Wild Cloud project directory
func (e *Environment) SetWCHome(path string) {
e.wcHome = path
}
// DetectWCHome attempts to find the Wild Cloud project directory by looking for .wildcloud marker
func (e *Environment) DetectWCHome() (string, error) {
// Start from current working directory
dir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("getting current directory: %w", err)
}
// Walk up the directory tree looking for .wildcloud marker
for {
markerPath := filepath.Join(dir, ".wildcloud")
if info, err := os.Stat(markerPath); err == nil && info.IsDir() {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
// Reached root directory
break
}
dir = parent
}
return "", nil
}
// Validate checks that the environment is properly configured
func (e *Environment) Validate(ctx context.Context) error {
// Validate WC_ROOT if set
if e.wcRoot != "" {
if err := e.validateWCRoot(); err != nil {
return fmt.Errorf("invalid WC_ROOT: %w", err)
}
}
// Validate WC_HOME if set
if e.wcHome != "" {
if err := e.validateWCHome(); err != nil {
return fmt.Errorf("invalid WC_HOME: %w", err)
}
}
return nil
}
// validateWCRoot checks that WC_ROOT is a valid Wild Cloud installation
func (e *Environment) validateWCRoot() error {
if e.wcRoot == "" {
return nil
}
// Check if directory exists
info, err := os.Stat(e.wcRoot)
if err != nil {
return fmt.Errorf("directory does not exist: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("path is not a directory: %s", e.wcRoot)
}
// Check for bin directory (contains wild-* scripts)
binDir := filepath.Join(e.wcRoot, "bin")
if info, err := os.Stat(binDir); err != nil || !info.IsDir() {
return fmt.Errorf("bin directory not found, this may not be a Wild Cloud installation")
}
// Note: We skip the PATH check for CLI usage as it's not required
// The original bash scripts expect WC_ROOT/bin to be in PATH, but the CLI can work without it
return nil
}
// validateWCHome checks that WC_HOME is a valid Wild Cloud project
func (e *Environment) validateWCHome() error {
if e.wcHome == "" {
return nil
}
// Check if directory exists
info, err := os.Stat(e.wcHome)
if err != nil {
return fmt.Errorf("directory does not exist: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("path is not a directory: %s", e.wcHome)
}
// Check for .wildcloud marker directory
markerDir := filepath.Join(e.wcHome, ".wildcloud")
if info, err := os.Stat(markerDir); err != nil || !info.IsDir() {
return fmt.Errorf("not a Wild Cloud project directory (missing .wildcloud marker)")
}
return nil
}
// ConfigPath returns the path to the config.yaml file
func (e *Environment) ConfigPath() string {
if e.wcHome == "" {
return ""
}
return filepath.Join(e.wcHome, "config.yaml")
}
// SecretsPath returns the path to the secrets.yaml file
func (e *Environment) SecretsPath() string {
if e.wcHome == "" {
return ""
}
return filepath.Join(e.wcHome, "secrets.yaml")
}
// AppsDir returns the path to the apps directory
func (e *Environment) AppsDir() string {
if e.wcHome == "" {
return ""
}
return filepath.Join(e.wcHome, "apps")
}
// WildCloudDir returns the path to the .wildcloud directory
func (e *Environment) WildCloudDir() string {
if e.wcHome == "" {
return ""
}
return filepath.Join(e.wcHome, ".wildcloud")
}
// CacheDir returns the path to the cache directory
func (e *Environment) CacheDir() string {
if e.wcHome == "" {
return ""
}
return filepath.Join(e.wcHome, ".wildcloud", "cache")
}
// IsConfigured returns true if both WC_ROOT and WC_HOME are set and valid
func (e *Environment) IsConfigured() bool {
return e.wcRoot != "" && e.wcHome != ""
}
// RequiresProject returns an error if WC_HOME is not configured
func (e *Environment) RequiresProject() error {
if e.wcHome == "" {
return fmt.Errorf("this command requires a Wild Cloud project directory. Run 'wild setup scaffold' to create one, or run from within an existing project")
}
return nil
}
// RequiresInstallation returns an error if WC_ROOT is not configured
func (e *Environment) RequiresInstallation() error {
if e.wcRoot == "" {
return fmt.Errorf("WC_ROOT is not set. Please set the WC_ROOT environment variable to your Wild Cloud installation directory")
}
return nil
}

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
}

View File

@@ -0,0 +1,185 @@
package output
import (
"fmt"
"os"
"github.com/fatih/color"
"go.uber.org/zap"
)
var (
// Global state for output formatting
colorEnabled = true
verboseMode = false
// Colors
colorInfo = color.New(color.FgBlue)
colorSuccess = color.New(color.FgGreen)
colorWarning = color.New(color.FgYellow)
colorError = color.New(color.FgRed)
colorHeader = color.New(color.FgBlue, color.Bold)
)
// Logger provides structured logging with colored output
type Logger struct {
zap *zap.Logger
}
// NewLogger creates a new logger instance
func NewLogger() *Logger {
config := zap.NewDevelopmentConfig()
config.DisableStacktrace = true
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
if verboseMode {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
logger, _ := config.Build()
return &Logger{
zap: logger,
}
}
// Sync flushes any buffered log entries
func (l *Logger) Sync() error {
return l.zap.Sync()
}
// Info logs an info message
func (l *Logger) Info(msg string, keysAndValues ...interface{}) {
l.zap.Sugar().Infow(msg, keysAndValues...)
// Also print to stdout with formatting
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", colorInfo.Sprint("INFO:"), msg)
} else {
_, _ = fmt.Fprintf(os.Stdout, "INFO: %s\n", msg)
}
}
// Success logs a success message
func (l *Logger) Success(msg string, keysAndValues ...interface{}) {
l.zap.Sugar().Infow(msg, keysAndValues...)
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", colorSuccess.Sprint("SUCCESS:"), msg)
} else {
_, _ = fmt.Fprintf(os.Stdout, "SUCCESS: %s\n", msg)
}
}
// Warning logs a warning message
func (l *Logger) Warning(msg string, keysAndValues ...interface{}) {
l.zap.Sugar().Warnw(msg, keysAndValues...)
if colorEnabled {
fmt.Fprintf(os.Stderr, "%s %s\n", colorWarning.Sprint("WARNING:"), msg)
} else {
fmt.Fprintf(os.Stderr, "WARNING: %s\n", msg)
}
}
// Error logs an error message
func (l *Logger) Error(msg string, keysAndValues ...interface{}) {
l.zap.Sugar().Errorw(msg, keysAndValues...)
if colorEnabled {
fmt.Fprintf(os.Stderr, "%s %s\n", colorError.Sprint("ERROR:"), msg)
} else {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", msg)
}
}
// Debug logs a debug message (only shown in verbose mode)
func (l *Logger) Debug(msg string, keysAndValues ...interface{}) {
l.zap.Sugar().Debugw(msg, keysAndValues...)
if verboseMode {
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", color.New(color.FgMagenta).Sprint("DEBUG:"), msg)
} else {
_, _ = fmt.Fprintf(os.Stdout, "DEBUG: %s\n", msg)
}
}
}
// Header prints a formatted header
func (l *Logger) Header(msg string) {
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "\n%s\n\n", colorHeader.Sprintf("=== %s ===", msg))
} else {
_, _ = fmt.Fprintf(os.Stdout, "\n=== %s ===\n\n", msg)
}
}
// Printf provides formatted output
func (l *Logger) Printf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
// Print provides simple output
func (l *Logger) Print(msg string) {
fmt.Println(msg)
}
// Global functions for package-level access
// DisableColor disables colored output
func DisableColor() {
colorEnabled = false
color.NoColor = true
}
// SetVerbose enables or disables verbose mode
func SetVerbose(enabled bool) {
verboseMode = enabled
}
// Package-level convenience functions
func Info(msg string) {
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", colorInfo.Sprint("INFO:"), msg)
} else {
_, _ = fmt.Fprintf(os.Stdout, "INFO: %s\n", msg)
}
}
func Success(msg string) {
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", colorSuccess.Sprint("SUCCESS:"), msg)
} else {
_, _ = fmt.Fprintf(os.Stdout, "SUCCESS: %s\n", msg)
}
}
func Warning(msg string) {
if colorEnabled {
fmt.Fprintf(os.Stderr, "%s %s\n", colorWarning.Sprint("WARNING:"), msg)
} else {
fmt.Fprintf(os.Stderr, "WARNING: %s\n", msg)
}
}
func Error(msg string) {
if colorEnabled {
fmt.Fprintf(os.Stderr, "%s %s\n", colorError.Sprint("ERROR:"), msg)
} else {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", msg)
}
}
func Header(msg string) {
if colorEnabled {
_, _ = fmt.Fprintf(os.Stdout, "\n%s\n\n", colorHeader.Sprintf("=== %s ===", msg))
} else {
_, _ = fmt.Fprintf(os.Stdout, "\n=== %s ===\n\n", msg)
}
}
// Printf provides formatted output (package-level function)
func Printf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}