First commit of golang CLI.
This commit is contained in:
378
wild-cli/internal/apps/catalog.go
Normal file
378
wild-cli/internal/apps/catalog.go
Normal 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
|
||||
}
|
298
wild-cli/internal/config/manager.go
Normal file
298
wild-cli/internal/config/manager.go
Normal 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
|
||||
}
|
139
wild-cli/internal/config/template.go
Normal file
139
wild-cli/internal/config/template.go
Normal 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)
|
||||
}
|
215
wild-cli/internal/environment/environment.go
Normal file
215
wild-cli/internal/environment/environment.go
Normal 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
130
wild-cli/internal/external/base.go
vendored
Normal 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
226
wild-cli/internal/external/kubectl.go
vendored
Normal 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
93
wild-cli/internal/external/manager.go
vendored
Normal 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
288
wild-cli/internal/external/restic.go
vendored
Normal 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
285
wild-cli/internal/external/talosctl.go
vendored
Normal 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
|
||||
}
|
185
wild-cli/internal/output/logger.go
Normal file
185
wild-cli/internal/output/logger.go
Normal 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...)
|
||||
}
|
Reference in New Issue
Block a user