Custom Providers¶
Create your own secret provider by implementing the vault.Vault interface.
Interface¶
package vault
type Vault interface {
// Get retrieves a secret by path
Get(ctx context.Context, path string) (*Secret, error)
// Set stores a secret at the given path
Set(ctx context.Context, path string, secret *Secret) error
// Delete removes a secret
Delete(ctx context.Context, path string) error
// Exists checks if a secret exists
Exists(ctx context.Context, path string) (bool, error)
// List returns paths matching the prefix
List(ctx context.Context, prefix string) ([]string, error)
// Name returns the provider name
Name() string
// Capabilities returns supported operations
Capabilities() Capabilities
// Close releases resources
Close() error
}
Creating a Provider Module¶
1. Create a new Go module¶
mkdir omnivault-myprovider
cd omnivault-myprovider
go mod init github.com/yourorg/omnivault-myprovider
2. Import only the vault package¶
// Only import the interface package - no dependency on full omnivault
go get github.com/agentplexus/omnivault/vault
3. Implement the interface¶
package myprovider
import (
"context"
"github.com/agentplexus/omnivault/vault"
)
type Provider struct {
client *MyBackendClient
}
type Config struct {
APIKey string
Endpoint string
}
func New(cfg Config) (vault.Vault, error) {
client, err := newBackendClient(cfg.APIKey, cfg.Endpoint)
if err != nil {
return nil, err
}
return &Provider{client: client}, nil
}
func (p *Provider) Get(ctx context.Context, path string) (*vault.Secret, error) {
data, err := p.client.Fetch(ctx, path)
if err != nil {
if isNotFound(err) {
return nil, vault.ErrSecretNotFound
}
return nil, err
}
return &vault.Secret{
Value: data.Value,
Fields: data.Fields,
Metadata: vault.Metadata{
CreatedAt: data.CreatedAt,
},
}, nil
}
func (p *Provider) Set(ctx context.Context, path string, secret *vault.Secret) error {
return p.client.Store(ctx, path, secret.Value, secret.Fields)
}
func (p *Provider) Delete(ctx context.Context, path string) error {
return p.client.Remove(ctx, path)
}
func (p *Provider) Exists(ctx context.Context, path string) (bool, error) {
_, err := p.client.Fetch(ctx, path)
if err != nil {
if isNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
func (p *Provider) List(ctx context.Context, prefix string) ([]string, error) {
return p.client.ListKeys(ctx, prefix)
}
func (p *Provider) Name() string {
return "myprovider"
}
func (p *Provider) Capabilities() vault.Capabilities {
return vault.Capabilities{
Read: true,
Write: true,
Delete: true,
List: true,
Binary: false,
MultiField: true,
}
}
func (p *Provider) Close() error {
return p.client.Close()
}
Using Your Provider¶
With Client¶
import (
"github.com/agentplexus/omnivault"
"github.com/yourorg/omnivault-myprovider"
)
provider, _ := myprovider.New(myprovider.Config{
APIKey: "...",
})
client, _ := omnivault.NewClient(omnivault.Config{
CustomVault: provider,
})
secret, _ := client.Get(ctx, "my/secret")
With Resolver¶
resolver := omnivault.NewResolver()
resolver.Register("myprovider", provider)
value, _ := resolver.Resolve(ctx, "myprovider://my/secret")
Best Practices¶
Error Handling¶
Return standard errors when applicable:
import "github.com/agentplexus/omnivault/vault"
// Use standard errors
if notFound {
return nil, vault.ErrSecretNotFound
}
if accessDenied {
return nil, vault.ErrAccessDenied
}
// Wrap backend errors
return nil, fmt.Errorf("backend error: %w", err)
Context Handling¶
Respect context cancellation:
func (p *Provider) Get(ctx context.Context, path string) (*vault.Secret, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Proceed with operation...
}
Resource Management¶
Implement proper cleanup:
Capability Declaration¶
Be accurate about what your provider supports:
func (p *Provider) Capabilities() vault.Capabilities {
return vault.Capabilities{
Read: true, // Always true for a useful provider
Write: false, // Set false if read-only
Delete: false, // Set false if deletion not supported
List: true, // Set false if listing not supported
Binary: true, // Set true if ValueBytes is supported
MultiField: true, // Set true if Fields map is supported
}
}
Module Architecture¶
omnivault-myprovider/
├── go.mod
├── go.sum
├── provider.go # Main implementation
├── provider_test.go # Tests
├── config.go # Configuration types
├── client.go # Backend client (internal)
└── README.md # Usage documentation
This architecture ensures:
- External providers don't bloat the core library with dependencies
- Providers can be versioned independently
- Users only install the providers they need