package kv import ( "context" "os" "strings" "github.com/hashicorp/vault/api" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) const defaultComponent = "vault kv" type service struct { logger mlogger.Logger component string store *api.KVv2 } func newService(opts Options) (Client, error) { logger := opts.Logger if logger == nil { logger = zap.NewNop() } component := strings.TrimSpace(opts.Component) if component == "" { component = defaultComponent } address := strings.TrimSpace(opts.Config.Address) if address == "" { logger.Error("Vault address missing") return nil, merrors.InvalidArgument(component + ": address is required") } tokenEnv := strings.TrimSpace(opts.Config.TokenEnv) if tokenEnv == "" { logger.Error("Vault token env missing") return nil, merrors.InvalidArgument(component + ": token_env is required") } token := strings.TrimSpace(os.Getenv(tokenEnv)) if token == "" { logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv)) return nil, merrors.InvalidArgument(component + ": token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)") } mountPath := strings.Trim(strings.TrimSpace(opts.Config.MountPath), "/") if mountPath == "" { logger.Error("Vault mount path missing") return nil, merrors.InvalidArgument(component + ": mount_path is required") } clientCfg := api.DefaultConfig() clientCfg.Address = address client, err := api.NewClient(clientCfg) if err != nil { logger.Error("Failed to create vault client", zap.Error(err)) return nil, merrors.Internal(component + ": failed to create client: " + err.Error()) } client.SetToken(token) if ns := strings.TrimSpace(opts.Config.Namespace); ns != "" { client.SetNamespace(ns) } return &service{ logger: logger.Named("vault"), component: component, store: client.KVv2(mountPath), }, nil } func (s *service) Put(ctx context.Context, secretPath string, payload map[string]interface{}) error { if payload == nil { return merrors.InvalidArgument(s.component+": payload is required", "payload") } normalizedPath, err := normalizePath(secretPath) if err != nil { return merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path") } if _, err := s.store.Put(ctx, normalizedPath, payload); err != nil { s.logger.Warn("Failed to write secret", zap.String("path", normalizedPath), zap.Error(err)) return merrors.Internal(s.component + ": failed to write secret at " + normalizedPath + ": " + err.Error()) } return nil } func (s *service) Get(ctx context.Context, secretPath string) (map[string]interface{}, error) { normalizedPath, err := normalizePath(secretPath) if err != nil { return nil, merrors.InvalidArgumentWrap(err, s.component+": secret path is invalid", "secret_path") } secret, err := s.store.Get(ctx, normalizedPath) if err != nil { s.logger.Warn("Failed to read secret", zap.String("path", normalizedPath), zap.Error(err)) return nil, merrors.Internal(s.component + ": failed to read secret at " + normalizedPath + ": " + err.Error()) } if secret == nil || secret.Data == nil { return nil, merrors.NoData(s.component + ": secret " + normalizedPath + " not found") } data := make(map[string]interface{}, len(secret.Data)) for k, v := range secret.Data { data[k] = v } return data, nil } func (s *service) GetString(ctx context.Context, secretPath, field string) (string, error) { field = strings.TrimSpace(field) if field == "" { return "", merrors.InvalidArgument(s.component+": field is required", "field") } data, err := s.Get(ctx, secretPath) if err != nil { return "", err } val, ok := data[field] if !ok { return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " missing " + field) } str, ok := val.(string) if !ok || strings.TrimSpace(str) == "" { return "", merrors.Internal(s.component + ": secret " + strings.Trim(strings.TrimPrefix(secretPath, "/"), "/") + " invalid " + field) } return str, nil } func normalizePath(secretPath string) (string, error) { normalizedPath := strings.Trim(strings.TrimPrefix(strings.TrimSpace(secretPath), "/"), "/") if normalizedPath == "" { return "", merrors.InvalidArgument("secret path is required", "secret_path") } return normalizedPath, nil } var _ Client = (*service)(nil)