smartconfig/main.go
2025-07-20 12:12:14 +02:00

475 lines
12 KiB
Go

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
}