Add terraform-provider-litellmvaultsecret implementation
Populate the repo with the Terraform/OpenTofu provider that manages the LiteLLM dynamic secrets engine on Vault/OpenBao via the Vault API. - Provider (VAULT_ADDR/VAULT_TOKEN) with resources litellmvaultsecret_secret_backend (mount + config) and litellmvaultsecret_secret_backend_role (models, max_budget, ttl/max_ttl in seconds, metadata) - Unit tests against a mock Vault API - End-to-end test: builds the sibling plugin, boots Vault + LiteLLM + Postgres, and runs a real terraform apply/destroy asserting key generation works - Makefile, woodpecker CI (build/test/pre-commit), examples, README
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
// vaultClient wraps the Vault/OpenBao API client with the operations this
|
||||
// provider needs to manage the LiteLLM secrets engine.
|
||||
type vaultClient struct {
|
||||
api *vault.Client
|
||||
}
|
||||
|
||||
func newVaultClient(address, token string) (*vaultClient, error) {
|
||||
cfg := vault.DefaultConfig()
|
||||
if cfg.Error != nil {
|
||||
return nil, cfg.Error
|
||||
}
|
||||
if address != "" {
|
||||
cfg.Address = address
|
||||
}
|
||||
c, err := vault.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token != "" {
|
||||
c.SetToken(token)
|
||||
}
|
||||
return &vaultClient{api: c}, nil
|
||||
}
|
||||
|
||||
// mountConfig holds the tunable options applied when enabling the engine.
|
||||
type mountConfig struct {
|
||||
DefaultLeaseTTL string
|
||||
MaxLeaseTTL string
|
||||
}
|
||||
|
||||
// enableMount mounts the secrets engine of the given plugin type at path.
|
||||
func (c *vaultClient) enableMount(ctx context.Context, path, pluginType, description string, cfg mountConfig) error {
|
||||
input := &vault.MountInput{
|
||||
Type: pluginType,
|
||||
Description: description,
|
||||
Config: vault.MountConfigInput{
|
||||
DefaultLeaseTTL: cfg.DefaultLeaseTTL,
|
||||
MaxLeaseTTL: cfg.MaxLeaseTTL,
|
||||
},
|
||||
}
|
||||
return c.api.Sys().MountWithContext(ctx, path, input)
|
||||
}
|
||||
|
||||
// tuneMount updates tunable options of an existing mount (e.g. description).
|
||||
func (c *vaultClient) tuneMount(ctx context.Context, path, description string, cfg mountConfig) error {
|
||||
input := vault.MountConfigInput{
|
||||
Description: &description,
|
||||
DefaultLeaseTTL: cfg.DefaultLeaseTTL,
|
||||
MaxLeaseTTL: cfg.MaxLeaseTTL,
|
||||
}
|
||||
return c.api.Sys().TuneMountWithContext(ctx, path, input)
|
||||
}
|
||||
|
||||
// mountInfo returns the mount at the given path, or nil if it does not exist.
|
||||
func (c *vaultClient) mountInfo(ctx context.Context, path string) (*vault.MountOutput, error) {
|
||||
mounts, err := c.api.Sys().ListMountsWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := strings.TrimRight(path, "/") + "/"
|
||||
if m, ok := mounts[key]; ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// disableMount unmounts the secrets engine at path.
|
||||
func (c *vaultClient) disableMount(ctx context.Context, path string) error {
|
||||
return c.api.Sys().UnmountWithContext(ctx, path)
|
||||
}
|
||||
|
||||
// writeConfig writes the LiteLLM connection config for the given backend mount.
|
||||
func (c *vaultClient) writeConfig(ctx context.Context, backend string, data map[string]interface{}) error {
|
||||
_, err := c.api.Logical().WriteWithContext(ctx, backend+"/config", data)
|
||||
return err
|
||||
}
|
||||
|
||||
// readConfig reads the LiteLLM connection config, returning nil if absent.
|
||||
func (c *vaultClient) readConfig(ctx context.Context, backend string) (map[string]interface{}, error) {
|
||||
secret, err := c.api.Logical().ReadWithContext(ctx, backend+"/config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secret == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return secret.Data, nil
|
||||
}
|
||||
|
||||
// writeRole creates or updates a role on the given backend mount.
|
||||
func (c *vaultClient) writeRole(ctx context.Context, backend, name string, data map[string]interface{}) error {
|
||||
_, err := c.api.Logical().WriteWithContext(ctx, rolePath(backend, name), data)
|
||||
return err
|
||||
}
|
||||
|
||||
// readRole reads a role, returning nil if it does not exist.
|
||||
func (c *vaultClient) readRole(ctx context.Context, backend, name string) (map[string]interface{}, error) {
|
||||
secret, err := c.api.Logical().ReadWithContext(ctx, rolePath(backend, name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secret == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return secret.Data, nil
|
||||
}
|
||||
|
||||
// deleteRole removes a role from the backend mount.
|
||||
func (c *vaultClient) deleteRole(ctx context.Context, backend, name string) error {
|
||||
_, err := c.api.Logical().DeleteWithContext(ctx, rolePath(backend, name))
|
||||
return err
|
||||
}
|
||||
|
||||
func rolePath(backend, name string) string {
|
||||
return fmt.Sprintf("%s/roles/%s", strings.TrimRight(backend, "/"), name)
|
||||
}
|
||||
|
||||
// isMountAlreadyExists reports whether the error is Vault's "path is already in
|
||||
// use" response, so callers can surface a friendlier message.
|
||||
func isMountAlreadyExists(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var respErr *vault.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
for _, e := range respErr.Errors {
|
||||
if strings.Contains(e, "path is already in use") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user