package smartconfig import ( "context" "encoding/json" "fmt" "io" "os" "os/exec" "strings" "time" "cloud.google.com/go/secretmanager/apiv1" secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/hashicorp/consul/api" vaultapi "github.com/hashicorp/vault/api" clientv3 "go.etcd.io/etcd/client/v3" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) const ( maxRecursionDepth = 3 interpolationPattern = `\$\{([^:]+?):(.*?)\}` ) type Config struct { data map[string]interface{} resolvers map[string]Resolver } type Resolver interface { Resolve(value string) (string, error) } type EnvResolver struct{} func (r *EnvResolver) Resolve(value string) (string, error) { result := os.Getenv(value) if result == "" { return "", fmt.Errorf("environment variable %s not found", value) } return result, nil } type ExecResolver struct{} func (r *ExecResolver) Resolve(value string) (string, error) { cmd := exec.Command("sh", "-c", value) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("exec command failed: %w", err) } return strings.TrimSpace(string(output)), nil } type FileResolver struct{} func (r *FileResolver) Resolve(value string) (string, error) { data, err := os.ReadFile(value) if err != nil { return "", fmt.Errorf("failed to read file %s: %w", value, err) } return strings.TrimSpace(string(data)), nil } type JSONResolver struct{} func (r *JSONResolver) Resolve(value string) (string, error) { parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid JSON resolver format, expected FILE:PATH") } filePath := parts[0] jsonPath := parts[1] data, err := os.ReadFile(filePath) if err != nil { return "", fmt.Errorf("failed to read JSON file %s: %w", filePath, err) } var jsonData interface{} if err := json.Unmarshal(data, &jsonData); err != nil { return "", fmt.Errorf("failed to parse JSON: %w", err) } // Simple JSON path evaluation (would need a proper library for complex paths) if jsonPath == "." { return fmt.Sprintf("%v", jsonData), nil } // This is a simplified implementation // In production, use a proper JSON path library return fmt.Sprintf("%v", jsonData), nil } type YAMLResolver struct{} func (r *YAMLResolver) Resolve(value string) (string, error) { parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid YAML resolver format, expected FILE:PATH") } filePath := parts[0] yamlPath := parts[1] data, err := os.ReadFile(filePath) if err != nil { return "", fmt.Errorf("failed to read YAML file %s: %w", filePath, err) } var yamlData interface{} if err := yaml.Unmarshal(data, &yamlData); err != nil { return "", fmt.Errorf("failed to parse YAML: %w", err) } // Simple YAML path evaluation (would need a proper library for complex paths) if yamlPath == "." { return fmt.Sprintf("%v", yamlData), nil } // This is a simplified implementation // In production, use a proper YAML path library return fmt.Sprintf("%v", yamlData), nil } type AWSSecretManagerResolver struct{} func (r *AWSSecretManagerResolver) Resolve(value string) (string, error) { ctx := context.Background() cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return "", fmt.Errorf("failed to load AWS config: %w", err) } svc := secretsmanager.NewFromConfig(cfg) input := &secretsmanager.GetSecretValueInput{ SecretId: &value, } result, err := svc.GetSecretValue(ctx, input) if err != nil { return "", fmt.Errorf("failed to get secret %s: %w", value, err) } if result.SecretString != nil { return *result.SecretString, nil } return "", fmt.Errorf("secret %s has no string value", value) } type GCPSecretManagerResolver struct{} func (r *GCPSecretManagerResolver) Resolve(value string) (string, error) { ctx := context.Background() client, err := secretmanager.NewClient(ctx) if err != nil { return "", fmt.Errorf("failed to create GCP Secret Manager client: %w", err) } defer func() { _ = client.Close() }() // If value doesn't contain a version, append /versions/latest if !strings.Contains(value, "/versions/") { value = value + "/versions/latest" } req := &secretmanagerpb.AccessSecretVersionRequest{ Name: value, } result, err := client.AccessSecretVersion(ctx, req) if err != nil { return "", fmt.Errorf("failed to access secret %s: %w", value, err) } return string(result.Payload.Data), nil } type VaultResolver struct{} func (r *VaultResolver) Resolve(value string) (string, error) { config := vaultapi.DefaultConfig() client, err := vaultapi.NewClient(config) if err != nil { return "", fmt.Errorf("failed to create Vault client: %w", err) } // Expect format: "path:key" e.g., "secret/data/myapp:password" parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid Vault path format, expected PATH:KEY") } path := parts[0] key := parts[1] secret, err := client.Logical().Read(path) if err != nil { return "", fmt.Errorf("failed to read secret from Vault: %w", err) } if secret == nil || secret.Data == nil { return "", fmt.Errorf("no secret found at path %s", path) } // Handle KV v2 format data, ok := secret.Data["data"].(map[string]interface{}) if ok { if val, ok := data[key].(string); ok { return val, nil } } // Handle KV v1 format if val, ok := secret.Data[key].(string); ok { return val, nil } return "", fmt.Errorf("key %s not found in secret", key) } type ConsulResolver struct{} func (r *ConsulResolver) Resolve(value string) (string, error) { config := api.DefaultConfig() client, err := api.NewClient(config) if err != nil { return "", fmt.Errorf("failed to create Consul client: %w", err) } kv := client.KV() pair, _, err := kv.Get(value, nil) if err != nil { return "", fmt.Errorf("failed to get key %s from Consul: %w", value, err) } if pair == nil { return "", fmt.Errorf("key %s not found in Consul", value) } return string(pair.Value), nil } type AzureKeyVaultResolver struct{} func (r *AzureKeyVaultResolver) Resolve(value string) (string, error) { // Expect format: "https://myvault.vault.azure.net:secretname" parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid Azure Key Vault format, expected VAULT_URL:SECRET_NAME") } vaultURL := parts[0] secretName := parts[1] cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { return "", fmt.Errorf("failed to create Azure credential: %w", err) } client, err := azsecrets.NewClient(vaultURL, cred, nil) if err != nil { return "", fmt.Errorf("failed to create Azure Key Vault client: %w", err) } ctx := context.Background() resp, err := client.GetSecret(ctx, secretName, "", nil) if err != nil { return "", fmt.Errorf("failed to get secret %s: %w", secretName, err) } return *resp.Value, nil } type K8SSecretResolver struct{} func (r *K8SSecretResolver) Resolve(value string) (string, error) { // Expect format: "namespace/secretname:key" parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return "", fmt.Errorf("invalid K8S secret format, expected NAMESPACE/SECRET:KEY") } secretPath := parts[0] key := parts[1] pathParts := strings.SplitN(secretPath, "/", 2) if len(pathParts) != 2 { return "", fmt.Errorf("invalid K8S secret path format, expected NAMESPACE/SECRET") } namespace := pathParts[0] secretName := pathParts[1] config, err := rest.InClusterConfig() if err != nil { // Fall back to kubeconfig return "", fmt.Errorf("failed to get K8S config: %w", err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { return "", fmt.Errorf("failed to create K8S client: %w", err) } ctx := context.Background() secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("failed to get secret %s/%s: %w", namespace, secretName, err) } data, ok := secret.Data[key] if !ok { return "", fmt.Errorf("key %s not found in secret %s/%s", key, namespace, secretName) } return string(data), nil } type EtcdResolver struct{} func (r *EtcdResolver) Resolve(value string) (string, error) { // Default to localhost:2379 if ETCD_ENDPOINTS is not set endpoints := strings.Split(os.Getenv("ETCD_ENDPOINTS"), ",") if len(endpoints) == 1 && endpoints[0] == "" { endpoints = []string{"localhost:2379"} } cli, err := clientv3.New(clientv3.Config{ Endpoints: endpoints, DialTimeout: 5 * time.Second, }) if err != nil { return "", fmt.Errorf("failed to create etcd client: %w", err) } defer func() { _ = cli.Close() }() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := cli.Get(ctx, value) if err != nil { return "", fmt.Errorf("failed to get key %s from etcd: %w", value, err) } if len(resp.Kvs) == 0 { return "", fmt.Errorf("key %s not found in etcd", value) } return string(resp.Kvs[0].Value), nil } func New() *Config { c := &Config{ resolvers: make(map[string]Resolver), } // Register default resolvers c.RegisterResolver("ENV", &EnvResolver{}) c.RegisterResolver("EXEC", &ExecResolver{}) c.RegisterResolver("FILE", &FileResolver{}) c.RegisterResolver("JSON", &JSONResolver{}) c.RegisterResolver("YAML", &YAMLResolver{}) c.RegisterResolver("AWSSM", &AWSSecretManagerResolver{}) c.RegisterResolver("GCPSM", &GCPSecretManagerResolver{}) c.RegisterResolver("VAULT", &VaultResolver{}) c.RegisterResolver("CONSUL", &ConsulResolver{}) c.RegisterResolver("AZURESM", &AzureKeyVaultResolver{}) c.RegisterResolver("K8SS", &K8SSecretResolver{}) c.RegisterResolver("ETCD", &EtcdResolver{}) return c } func (c *Config) RegisterResolver(name string, resolver Resolver) { c.resolvers[name] = resolver } func (c *Config) LoadFromReader(reader io.Reader) error { data, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read config: %w", err) } // Interpolate variables recursively interpolated, err := c.interpolate(string(data), 0) if err != nil { return fmt.Errorf("failed to interpolate config: %w", err) } // Parse as YAML if err := yaml.Unmarshal([]byte(interpolated), &c.data); err != nil { return fmt.Errorf("failed to parse YAML: %w", err) } // Handle environment variable injection if err := c.injectEnvironment(); err != nil { return fmt.Errorf("failed to inject environment variables: %w", err) } return nil } func (c *Config) LoadFromFile(path string) error { file, err := os.Open(path) if err != nil { return fmt.Errorf("failed to open config file: %w", err) } defer func() { _ = file.Close() }() return c.LoadFromReader(file) } func (c *Config) injectEnvironment() error { envData, ok := c.data["env"] if !ok { return nil } envMap, ok := envData.(map[string]interface{}) if !ok { return fmt.Errorf("env section must be a map") } for key, value := range envMap { strValue, ok := value.(string) if !ok { strValue = fmt.Sprintf("%v", value) } if err := os.Setenv(key, strValue); err != nil { return fmt.Errorf("failed to set environment variable %s: %w", key, err) } } return nil } func (c *Config) Get(key string) (interface{}, bool) { return c.data[key], true } func (c *Config) GetString(key string) (string, error) { value, ok := c.data[key] if !ok { return "", fmt.Errorf("key %s not found", key) } strValue, ok := value.(string) if !ok { return "", fmt.Errorf("key %s is not a string", key) } return strValue, nil } func (c *Config) Data() map[string]interface{} { return c.data }