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,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)
}