This commit is contained in:
2026-03-01 22:52:08 +07:00
commit 1244f3e2d5
63 changed files with 6075 additions and 0 deletions

204
pkg/config/resolver.go Normal file
View File

@@ -0,0 +1,204 @@
package config
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/spf13/afero"
)
// Resolver handles resolution of configuration values with special prefixes.
type Resolver struct {
gcpProject string
awsRegion string
gsmClient *secretmanager.Client
asmClient *secretsmanager.SecretsManager
awsSession *session.Session
specialValue *regexp.Regexp
fs afero.Fs
}
// NewResolver creates a new value resolver.
func NewResolver(gcpProject, awsRegion string, fs afero.Fs) *Resolver {
return &Resolver{
gcpProject: gcpProject,
awsRegion: awsRegion,
specialValue: regexp.MustCompile(`^\$([A-Z]+):(.+)$`),
fs: fs,
}
}
// Resolve resolves a configuration value that may contain special prefixes.
func (r *Resolver) Resolve(value interface{}) interface{} {
switch v := value.(type) {
case string:
return r.resolveString(v)
case map[string]interface{}:
// Recursively resolve map values
result := make(map[string]interface{})
for k, val := range v {
result[k] = r.Resolve(val)
}
return result
case []interface{}:
// Recursively resolve slice items
result := make([]interface{}, len(v))
for i, val := range v {
result[i] = r.Resolve(val)
}
return result
default:
// Return non-string values as-is
return value
}
}
// resolveString resolves a string value that may contain a special prefix.
func (r *Resolver) resolveString(value string) interface{} {
matches := r.specialValue.FindStringSubmatch(value)
if matches == nil {
return value
}
resolverType := matches[1]
resolverValue := matches[2]
switch resolverType {
case "ENV":
return r.resolveEnv(resolverValue)
case "GSM":
return r.resolveGSM(resolverValue)
case "ASM":
return r.resolveASM(resolverValue)
case "FILE":
return r.resolveFile(resolverValue)
default:
log.Printf("Unknown resolver type: %s", resolverType)
return value
}
}
// resolveEnv resolves an environment variable.
func (r *Resolver) resolveEnv(envVar string) interface{} {
value := os.Getenv(envVar)
if value == "" {
return nil
}
return value
}
// resolveGSM resolves a Google Secret Manager secret.
func (r *Resolver) resolveGSM(secretName string) interface{} {
if r.gcpProject == "" {
log.Printf("GCP project not configured for GSM resolution")
return nil
}
// Initialize GSM client if needed
if r.gsmClient == nil {
ctx := context.Background()
client, err := secretmanager.NewClient(ctx)
if err != nil {
log.Printf("Failed to create GSM client: %v", err)
return nil
}
r.gsmClient = client
}
// Build the resource name
name := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", r.gcpProject, secretName)
// Access the secret
ctx := context.Background()
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: name,
}
result, err := r.gsmClient.AccessSecretVersion(ctx, req)
if err != nil {
log.Printf("Failed to access GSM secret %s: %v", secretName, err)
return nil
}
return string(result.Payload.Data)
}
// resolveASM resolves an AWS Secrets Manager secret.
func (r *Resolver) resolveASM(secretName string) interface{} {
// Initialize AWS session if needed
if r.awsSession == nil {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(r.awsRegion),
})
if err != nil {
log.Printf("Failed to create AWS session: %v", err)
return nil
}
r.awsSession = sess
}
// Initialize ASM client if needed
if r.asmClient == nil {
r.asmClient = secretsmanager.New(r.awsSession)
}
// Get the secret value
input := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretName),
}
result, err := r.asmClient.GetSecretValue(input)
if err != nil {
log.Printf("Failed to access ASM secret %s: %v", secretName, err)
return nil
}
// Return the secret string
if result.SecretString != nil {
return *result.SecretString
}
// If it's binary data, we can't handle it as a string config value
log.Printf("ASM secret %s contains binary data, which is not supported", secretName)
return nil
}
// resolveFile resolves a file's contents.
func (r *Resolver) resolveFile(filePath string) interface{} {
// Expand user home directory if present
if strings.HasPrefix(filePath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
log.Printf("Failed to get user home directory: %v", err)
return nil
}
filePath = filepath.Join(home, filePath[2:])
}
data, err := afero.ReadFile(r.fs, filePath)
if err != nil {
log.Printf("Failed to read file %s: %v", filePath, err)
return nil
}
// Strip whitespace/newlines from file contents
return strings.TrimSpace(string(data))
}
// Close closes any open clients.
func (r *Resolver) Close() error {
if r.gsmClient != nil {
return r.gsmClient.Close()
}
return nil
}