182 lines
5.4 KiB
Go
182 lines
5.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")
|
|
}
|
|
|
|
token, tokenSource, err := resolveToken(opts.Config)
|
|
if err != nil {
|
|
logger.Error("Vault token configuration is invalid", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
if token == "" {
|
|
logger.Error("Vault token missing", zap.String("source", tokenSource))
|
|
return nil, merrors.InvalidArgument(component + ": vault token is empty")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func resolveToken(config Config) (string, string, error) {
|
|
tokenEnv := strings.TrimSpace(config.TokenEnv)
|
|
if tokenEnv != "" {
|
|
if token := strings.TrimSpace(os.Getenv(tokenEnv)); token != "" {
|
|
return token, "token_env:" + tokenEnv, nil
|
|
}
|
|
}
|
|
|
|
tokenFilePath := strings.TrimSpace(config.TokenFile)
|
|
if tokenFileEnv := strings.TrimSpace(config.TokenFileEnv); tokenFileEnv != "" {
|
|
if resolved := strings.TrimSpace(os.Getenv(tokenFileEnv)); resolved != "" {
|
|
tokenFilePath = resolved
|
|
}
|
|
}
|
|
if tokenFilePath != "" {
|
|
raw, err := os.ReadFile(tokenFilePath)
|
|
if err != nil {
|
|
return "", "", merrors.Internal("vault kv: failed to read token file " + tokenFilePath + ": " + err.Error())
|
|
}
|
|
return strings.TrimSpace(string(raw)), "token_file:" + tokenFilePath, nil
|
|
}
|
|
|
|
if tokenEnv != "" {
|
|
return "", "token_env:" + tokenEnv, merrors.InvalidArgument("vault kv: token env " + tokenEnv + " is empty")
|
|
}
|
|
if strings.TrimSpace(config.TokenFileEnv) != "" {
|
|
return "", "token_file_env:" + strings.TrimSpace(config.TokenFileEnv), merrors.InvalidArgument("vault kv: token file env is empty")
|
|
}
|
|
|
|
return "", "", merrors.InvalidArgument("vault kv: either token_env or token_file/token_file_env must be configured")
|
|
}
|
|
|
|
var _ Client = (*service)(nil)
|