Files
sendico/api/pkg/vault/kv/service.go
2026-02-28 10:10:26 +01:00

152 lines
4.4 KiB
Go

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)