initial
This commit is contained in:
204
pkg/config/resolver.go
Normal file
204
pkg/config/resolver.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user