diff --git a/.github/workflows/unit-test-base.yaml b/.github/workflows/unit-test-base.yaml index 809b403..21682b7 100644 --- a/.github/workflows/unit-test-base.yaml +++ b/.github/workflows/unit-test-base.yaml @@ -23,5 +23,3 @@ jobs: uses: actions/checkout@v3 - name: Test run: make test - - name: Build Examples - run: make build-examples diff --git a/.github/workflows/unit-test-pr.yaml b/.github/workflows/unit-test-pr.yaml index d92b417..4c65dd5 100644 --- a/.github/workflows/unit-test-pr.yaml +++ b/.github/workflows/unit-test-pr.yaml @@ -19,5 +19,3 @@ jobs: uses: actions/checkout@v3 - name: Test run: make test - - name: Build Examples - run: make build-examples diff --git a/README.md b/README.md index 64ca290..1b771ea 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,33 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/jack-mcveigh/secretly)](https://goreportcard.com/report/github.com/jack-mcveigh/secretly) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -___Secretly___ was created to allow Go applications to easily interface with popular secret management services and reduce secret ingestion boiler-plate. In-memory secret caching is included to reduce the number of operations against the secret management service, when dealing with secrets that store map data in the form of JSON and YAML. +___Secretly___ was created to allow Go applications to easily interface with secret management services and reduce secret ingestion boiler-plate. In-memory secret caching is included to reduce the number of operations against the secret management service, when dealing with secrets that store map data in the form of JSON and YAML. -Below is a list of the currently supported secret management services: +## Usage -* [Google Cloud Platform's (GCP) Secret Manager](https://cloud.google.com/secret-manager) -* [Amazon Web Services' (AWS) Secrets Manager](https://aws.amazon.com/secrets-manager/) -* [Vault KV Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/kv) - * [KV Secrets Engine - Version 1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1) - * [KV Secrets Engine - Version 2](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2) -* [Microsoft Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault/) +```go +type Secrets struct { + DatabaseUsername string `type:"yaml" name:"My-DB-Credentials" key:"username" split_words:"true"` -If there's a secret management service missing that you'd like to see included, create a [Feature Request](https://github.com/jack-mcveigh/secretly/issues/new)! + DatabasePassword string `type:"yaml" name:"My-DB-Credentials" key:"password"` +} -## Usage +func getSecret (ctx context.Context, name, version string) ([]byte, error) { + // Use your secret manager of choice's client library here to retrieve secrets. + return []byte(""), nil +} -See the brief overview below or check out our [examples](examples). +func example(ctx context.Context) Secrets { + var s Secrets + + err := secretly.Process(ctx, &s, getSecret) + if err != nil { + log.Fatal(err) + } + + return s +} +``` ## Overview @@ -137,26 +148,27 @@ Secretly provides two options for specifying secret versions other than the __ve * example.go ```go - ... - type Secrets struct { DatabaseUsername string `type:"yaml" name:"My-DB-Credentials" key:"username" split_words:"true"` DatabasePassword string `type:"yaml" name:"My-DB-Credentials" key:"password"` } - func example(client secretly.Client) Secrets { + func getSecret (ctx context.Context, name, version string) ([]byte, error) { + // Use your secret manager of choice's client library here to retrieve secrets. + return []byte(""), nil + } + + func example(ctx context.Context) Secrets { var s Secrets - err := client.Process(&s, secretly.ApplyPatch("versions.json")) + err := secretly.Process(ctx, &s, getSecret, secretly.WithPatchFile("versions.json")) if err != nil { log.Fatal(err) } return s } - - ... ``` 2. Read secret versions from environment variables: @@ -173,26 +185,27 @@ Secretly provides two options for specifying secret versions other than the __ve * example.go ```go - ... - type Secrets struct { DatabaseUsername string `type:"yaml" name:"My-DB-Credentials" key:"username" split_words:"true"` DatabasePassword string `type:"yaml" name:"My-DB-Credentials" key:"password"` } - func example(client secretly.Client) Secrets { + func getSecret (ctx context.Context, name, version string) ([]byte, error) { + // Use your secret manager of choice's client library here to retrieve secrets. + return []byte(""), nil + } + + func example(ctx context.Context) Secrets { var s Secrets - err := client.Process(&s, secretly.WithVersionsFromEnv("EXAMPLE")) + err := secretly.Process(ctx, &s, getSecret, secretly.WithVersionsFromEnv("EXAMPLE")) if err != nil { log.Fatal(err) } return s } - - ... ``` ## References diff --git a/aws/client.go b/aws/client.go deleted file mode 100644 index 685d2b7..0000000 --- a/aws/client.go +++ /dev/null @@ -1,156 +0,0 @@ -package aws - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/jack-mcveigh/secretly" -) - -// AWS Staging Labels are used to access secret versions by an alias, -// retrieving versions such as the latest version. -const ( - AWSCURRENT = "AWSCURRENT" // latest - AWSPREVIOUS = "AWSPREVIOUS" // latest - 1 - AWSPENDING = "AWSPENDING" // Temporary while secret is being rotated -) - -// awssmc describes required AWS Secrets Manager client methods -type awssmc interface { - GetSecretValueWithContext(ctx aws.Context, input *secretsmanager.GetSecretValueInput, opts ...request.Option) (*secretsmanager.GetSecretValueOutput, error) -} - -// Config provides both AWS Secrets Manager client and secretly wrapper configurations. -type Config struct { - // ConfigProvider provides service clients with a client.Config. - ConfigProvider client.ConfigProvider - - SecretlyConfig secretly.Config -} - -// Client is the AWS Secrets Manager Client wrapper. -// Implements secretly.Client -type Client struct { - // client is the AWS Secrets Manager client. - client awssmc - - // secretCache is the cache that stores secrets => versions => content - // to reduce secret manager accesses. - secretCache secretly.SecretCache -} - -// Compile time check to assert that client implements secretly.Client -var _ secretly.Client = (*Client)(nil) - -// NewClient returns an AWS Secrets Manager client wrapper -// with the configs applied. -// Will error if authentication with the secrets manager fails. -func NewClient(cfg Config, cfgs ...*aws.Config) *Client { - smc := secretsmanager.New(cfg.ConfigProvider, cfgs...) - - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: smc, - secretCache: sc, - } - return c -} - -// Wrap wraps the AWS Secrets Manager client. -func Wrap(client *secretsmanager.SecretsManager, cfg Config) *Client { - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: client, - secretCache: sc, - } - return c -} - -// Process resolves the provided specification -// using AWS Secrets Manager. -// ProcessOptions can be provided -// to add additional processing for the fields, -// like reading version info from the env or a file. -// -// (*Client).Process is a convenience -// for calling secretly.Process with the Client. -func (c *Client) Process(spec any, opts ...secretly.ProcessOption) error { - return secretly.Process(c, spec, opts...) -} - -// GetSecret retrieves the secret labeled AWSCURRENT for name -// from AWS Secrets Manager. -func (c *Client) GetSecret(ctx context.Context, name string) ([]byte, error) { - return c.getSecretWithStagingLabel(ctx, name, AWSCURRENT) -} - -// getSecretWithStagingLabel retrieves the secret labeled, label, for name -// from AWS Secrets Manager. -func (c *Client) getSecretWithStagingLabel(ctx context.Context, name, label string) ([]byte, error) { - if b, hit := c.secretCache.Get(name, label); hit { - return b, nil - } - b, err := c.getSecretVersion(ctx, &secretsmanager.GetSecretValueInput{ - SecretId: &name, - VersionStage: aws.String(label), - }) - c.secretCache.Add(name, label, b) - return b, err -} - -// GetSecretWithVersion retrieves the specific secret version for name -// from AWS Secrets Manager. -// -// Note: The version provided can be either a version id or -// one of the default version staging labels, -// [AWSCURRENT], [AWSPREVIOUS], or [AWSPENDING]. -func (c *Client) GetSecretWithVersion(ctx context.Context, name, versionOrVersionStage string) ([]byte, error) { - switch versionStage := versionOrVersionStage; versionStage { - case secretly.DefaultVersion, AWSCURRENT: - return c.GetSecret(ctx, name) - case AWSPENDING, AWSPREVIOUS: - return c.getSecretWithStagingLabel(ctx, name, versionStage) - } - - version := versionOrVersionStage - - if b, hit := c.secretCache.Get(name, version); hit { - return b, nil - } - - b, err := c.getSecretVersion(ctx, &secretsmanager.GetSecretValueInput{ - SecretId: &name, - VersionId: &version, - }) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, version, b) - return b, nil -} - -// getSecret retrieves the a specific version of the secret from the AWS Secrets Manager. -func (c *Client) getSecretVersion(ctx context.Context, input *secretsmanager.GetSecretValueInput) ([]byte, error) { - output, err := c.client.GetSecretValueWithContext(ctx, input) - if err != nil { - return nil, err - } - - return []byte(*output.SecretString), nil -} diff --git a/aws/client_test.go b/aws/client_test.go deleted file mode 100644 index 1c59ef1..0000000 --- a/aws/client_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package aws - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/jack-mcveigh/secretly" -) - -const secretKeyFormat = "%s_%s" - -var ( - errSecretNotFound = errors.New("secret not found") - errSecretAccessedMoreThanOnce = errors.New("secret accessed more than once") - - testSecretContent = []byte("secret content") -) - -type secretInfo struct { - name string - version string -} - -type stubClient struct { - secrets map[string][]byte - - accessed bool - failIfAccessedMoreThanOnce bool -} - -func newStubClientWithSecrets() *stubClient { - c := &stubClient{ - secrets: make(map[string][]byte), - } - - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", AWSCURRENT)] = testSecretContent - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", "1")] = testSecretContent - - return c -} - -func (c *stubClient) GetSecretValueWithContext(ctx context.Context, input *secretsmanager.GetSecretValueInput, opts ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - if c.failIfAccessedMoreThanOnce && c.accessed { - return nil, errSecretAccessedMoreThanOnce - } - c.accessed = true - - var version string - if input.VersionStage != nil { - version = *input.VersionStage - } else { - version = *input.VersionId - } - - key := fmt.Sprintf(secretKeyFormat, *input.SecretId, version) - - if b, ok := c.secrets[key]; ok { - v := string(b) - resp := &secretsmanager.GetSecretValueOutput{ - SecretString: &v, - } - return resp, nil - } - return nil, errSecretNotFound -} - -func TestGetSecretVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfo secretInfo - want []byte - wantErr error - }{ - { - name: "Success With Default Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Numbered Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "1", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Latest Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: AWSCURRENT, - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Secret Does Not Exist", - secretInfo: secretInfo{ - name: "fake-secret-that-does-not-exist", - version: "0", - }, - want: nil, - wantErr: errSecretNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubClientWithSecrets() - c := Client{client: smc, secretCache: secretly.NewSecretCache()} - - got, err := c.GetSecretWithVersion(context.Background(), tt.secretInfo.name, tt.secretInfo.version) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Incorrect secret content. Want %v, got %v", tt.want, got) - } - }) - } -} - -func TestGetSecretVersionCaching(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfos []secretInfo - wantErr error - }{ - { - name: "Cache Hit", - secretInfos: []secretInfo{ - { - name: "fake-secret", - version: AWSCURRENT, - }, - { - name: "fake-secret", - version: AWSCURRENT, - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubClientWithSecrets() - smc.failIfAccessedMoreThanOnce = true - - c := Client{ - client: smc, - secretCache: secretly.NewSecretCache(), - } - - var err error - for _, secretInfo := range tt.secretInfos { - _, err = c.GetSecretWithVersion(context.Background(), secretInfo.name, secretInfo.version) - } - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - }) - } -} diff --git a/azure/client.go b/azure/client.go deleted file mode 100644 index 058f726..0000000 --- a/azure/client.go +++ /dev/null @@ -1,134 +0,0 @@ -package azure - -import ( - "context" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" - "github.com/jack-mcveigh/secretly" -) - -// azuresc describes required Azure Key Vault Secrets client methods -type azuresc interface { - GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) -} - -// Config provides both Azure Key Vault Secrets client and secretly wrapper configurations. -type Config struct { - // VaultURI is the uri of the Azure Key Vault instance. - VaultURI string - - // Credential is the credential for the Azure Key Vault instance. - Credential azcore.TokenCredential - - // Options are optional settings for the Azure Key Vault Secrets client. - Options *azsecrets.ClientOptions - - SecretlyConfig secretly.Config -} - -// Client is the Azure Key Vault Secrets client wrapper. -// Implements secretly.Client -type Client struct { - // client is the Azure Key Vault Secrets client. - client azuresc - - // secretCache is the cache that stores secrets => versions => content - // to reduce secret client accesses. - secretCache secretly.SecretCache -} - -// Compile time check to assert that client implements secretly.Client -var _ secretly.Client = (*Client)(nil) - -// NewClient returns a Azure Key Vault Secrets client wrapper -// with the options applied. -// Will error if authentication with the secret manager fails. -func NewClient(ctx context.Context, cfg Config) (*Client, error) { - azsc, err := azsecrets.NewClient(cfg.VaultURI, cfg.Credential, cfg.Options) - if err != nil { - return nil, err - } - - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: azsc, - secretCache: sc, - } - return c, nil -} - -// Wrap wraps the Azure Key Vault Secrets client. -func Wrap(client *azsecrets.Client, cfg Config) *Client { - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: client, - secretCache: sc, - } - return c -} - -// Process resolves the provided specification -// using Azure Key Vault Secrets. -// ProcessOptions can be provided -// to add additional processing for the fields, -// like reading version info from the env or a file. -// -// (*Client).Process is a convenience -// for calling secretly.Process with the Client. -func (c *Client) Process(spec any, opts ...secretly.ProcessOption) error { - return secretly.Process(c, spec, opts...) -} - -// GetSecret retrieves the latest secret version for name -// from Azure Key Vault Secrets. -func (c *Client) GetSecret(ctx context.Context, name string) ([]byte, error) { - if b, hit := c.secretCache.Get(name, ""); hit { - return b, nil - } - b, err := c.getSecretVersion(ctx, name, "") - c.secretCache.Add(name, "", b) - return b, err -} - -// GetSecretWithVersion retrieves the specific secret version for name -// from Azure Key Vault Secrets. -func (c *Client) GetSecretWithVersion(ctx context.Context, name, version string) ([]byte, error) { - switch version { - case secretly.DefaultVersion, "": - return c.GetSecret(ctx, name) - } - - if b, hit := c.secretCache.Get(name, version); hit { - return b, nil - } - - b, err := c.getSecretVersion(ctx, name, version) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, version, b) - return b, nil -} - -// getSecret retrieves the a specific version of the secret from the Azure Key Vault Secrets. -func (c *Client) getSecretVersion(ctx context.Context, name, version string) ([]byte, error) { - resp, err := c.client.GetSecret(ctx, name, version, nil) - if err != nil { - return nil, err - } - return []byte(*resp.Value), nil -} diff --git a/azure/client_test.go b/azure/client_test.go deleted file mode 100644 index 59a4d9f..0000000 --- a/azure/client_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package azure - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" - "github.com/jack-mcveigh/secretly" -) - -const secretKeyFormat = "%s_%s" - -var ( - errSecretNotFound = errors.New("secret not found") - errSecretAccessedMoreThanOnce = errors.New("secret accessed more than once") - - testSecretContent = "secret content" -) - -type secretInfo struct { - name string - version string -} - -type stubClient struct { - secrets map[string]string - - accessed bool - failIfAccessedMoreThanOnce bool -} - -func newStubClientWithSecrets() *stubClient { - c := &stubClient{ - secrets: make(map[string]string), - } - - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", "")] = testSecretContent - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", "1")] = testSecretContent - - return c -} - -func (c *stubClient) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - resp := azsecrets.GetSecretResponse{} - if c.failIfAccessedMoreThanOnce && c.accessed { - return resp, errSecretAccessedMoreThanOnce - } - c.accessed = true - - key := fmt.Sprintf(secretKeyFormat, name, version) - - if s, ok := c.secrets[key]; ok { - resp.Value = &s - return resp, nil - } - return resp, errSecretNotFound -} - -func (c *stubClient) Close() error { return nil } - -func TestGetSecretVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfo secretInfo - want []byte - wantErr error - }{ - { - name: "Success With Default Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "", - }, - want: []byte(testSecretContent), - wantErr: nil, - }, - { - name: "Success With Numbered Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "1", - }, - want: []byte(testSecretContent), - wantErr: nil, - }, - { - name: "Success With Latest Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "", - }, - want: []byte(testSecretContent), - wantErr: nil, - }, - { - name: "Secret Does Not Exist", - secretInfo: secretInfo{ - name: "fake-secret-that-does-not-exist", - version: "0", - }, - want: nil, - wantErr: errSecretNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - azsc := newStubClientWithSecrets() - c := Client{client: azsc, secretCache: secretly.NewSecretCache()} - - got, err := c.GetSecretWithVersion(context.Background(), tt.secretInfo.name, tt.secretInfo.version) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Incorrect secret content. Want %v, got %v", tt.want, got) - } - }) - } -} - -func TestGetSecretVersionCaching(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfos []secretInfo - wantErr error - }{ - { - name: "Cache Hit", - secretInfos: []secretInfo{ - { - name: "fake-secret", - version: "", - }, - { - name: "fake-secret", - version: "", - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - azsc := newStubClientWithSecrets() - azsc.failIfAccessedMoreThanOnce = true - - c := Client{ - client: azsc, - secretCache: secretly.NewSecretCache(), - } - - var err error - for _, secretInfo := range tt.secretInfos { - _, err = c.GetSecretWithVersion(context.Background(), secretInfo.name, secretInfo.version) - } - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - }) - } -} diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..8477c37 --- /dev/null +++ b/cache.go @@ -0,0 +1,31 @@ +package secretly + +// secretCacheEntry is a map of versions to the secret content. +type secretCacheEntry map[string][]byte + +// cache contains the cache, mapping secrets to a [secretCacheEntry]. +type cache struct { + cache map[string]secretCacheEntry +} + +// newCache constructs a secretCache. +func newCache() *cache { + return &cache{cache: make(map[string]secretCacheEntry)} +} + +func (sc cache) Add(name, version string, content []byte) { + if sc.cache[name] == nil { + sc.cache[name] = make(secretCacheEntry) + } + sc.cache[name][version] = content +} + +func (sc cache) Get(name, version string) ([]byte, bool) { + if _, ok := sc.cache[name]; !ok { + return nil, false + } + if b, ok := sc.cache[name][version]; ok { + return b, true + } + return nil, false +} diff --git a/secret_cache_test.go b/cache_test.go similarity index 97% rename from secret_cache_test.go rename to cache_test.go index ebd9c59..5c615cc 100644 --- a/secret_cache_test.go +++ b/cache_test.go @@ -11,13 +11,14 @@ type secretInfo struct { content []byte } -func newSecretCacheWithEntries() secretCache { - sc := NewSecretCache() +func newSecretCacheWithEntries() *cache { + sc := newCache() sc.cache["key1"] = secretCacheEntry{ "1": []byte("key1: 1: secret content"), "latest": []byte("key1: latest: secret content"), } + return sc } diff --git a/errors.go b/errors.go index 11945ef..ed1526d 100644 --- a/errors.go +++ b/errors.go @@ -6,7 +6,7 @@ import ( ) var ( - ErrInvalidSpecification = errors.New("specification must be a struct pointer") + ErrInvalidSpecification = errors.New("invalid specification") ErrInvalidSecretType = errors.New("invalid secret type") ErrInvalidSecretVersion = errors.New("invalid secret version") ErrSecretTypeDoesNotSupportKey = errors.New("secret type does not support \"key\"") @@ -19,6 +19,8 @@ type StructTagError struct { Err error } -func (e StructTagError) Error() string { return fmt.Sprintf("%s: %s: %s", e.Name, e.Key, e.Err) } +func (e StructTagError) Error() string { + return fmt.Sprintf("field %q: key %q: %s", e.Name, e.Key, e.Err) +} func (e StructTagError) Unwrap() error { return e.Err } diff --git a/examples/aws/example.go b/examples/aws/example.go deleted file mode 100644 index 17a09f1..0000000 --- a/examples/aws/example.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "log" - - "github.com/aws/aws-sdk-go/aws/session" - "github.com/jack-mcveigh/secretly" - secretlyaws "github.com/jack-mcveigh/secretly/aws" -) - -const secretVersionsFilePath = "versions.json" - -type SecretConfig struct { - // The secret stores text data and is named "Service_Integration_Token" - // in AWS Secrets Manager. Since "split_words" is enabled, version info can be loaded - // from a config file by including the field name, converted to PascalCase to - // Snake_Case, as a key: "Service_Integration_Token". - ServiceIntegrationToken string `split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in AWS Secrets Manager. The field to extract from the json secret is named - // "Username". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Username". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Username". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Username", since split_words is set to true. - DatabaseUsername string `type:"json" name:"My-Database-Credentials" key:"Username" split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in AWS Secrets Manager. The field to extract from the json secret is named - // "Password". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Password". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Password". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Password", since split_words is set to true. - DatabasePassword string `type:"json" name:"My-Database-Credentials" key:"Password" split_words:"true"` -} - -func main() { - s := session.Must(session.NewSession()) - - client := secretlyaws.NewClient(secretlyaws.Config{ - ConfigProvider: s, - }) - - // Or initialize by wrapping your own AWS Secrets Manager client. - // - // client := secretlyaws.Wrap(secretsmanager.New(s), secretlyaws.Config{}) - - var sc SecretConfig - err := client.Process(&sc, secretly.ApplyPatch(secretVersionsFilePath)) - if err != nil { - log.Fatalf("Failed to process SecretConfig: %v", err) - } - - log.Printf("Username: %s", sc.DatabaseUsername) - log.Printf("Password: %s", sc.DatabasePassword) -} diff --git a/examples/aws/versions.json b/examples/aws/versions.json deleted file mode 100644 index 172b7f0..0000000 --- a/examples/aws/versions.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Service_Integration_Token": { - "version": "EXAMPLE1-90ab-cdef-fedc-ba987SECRET1" - }, - "My-Database-Credentials_Username": { - "version": "AWSPREVIOUS" - }, - "My-Database-Credentials_Password": { - "version": "EXAMPLE3-90ab-cdef-fedc-ba987SECRET3" - } -} \ No newline at end of file diff --git a/examples/azure/example.go b/examples/azure/example.go deleted file mode 100644 index 8033e46..0000000 --- a/examples/azure/example.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/jack-mcveigh/secretly" - secretlyazure "github.com/jack-mcveigh/secretly/azure" -) - -const ( - azureVaultURI = "fake.vault.azure.net" - secretVersionsFilePath = "versions.json" -) - -type SecretConfig struct { - // The secret stores text data and is named "Service_Integration_Token" - // in Azure Key Vault. Since "split_words" is enabled, version info can be loaded - // from a config file by including the field name, converted to PascalCase to - // Snake_Case, as a key: "Service_Integration_Token". - ServiceIntegrationToken string `split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Azure Key Vault. The field to extract from the json secret is named - // "Username". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Username". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Username". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Username", since split_words is set to true. - DatabaseUsername string `type:"json" name:"My-Database-Credentials" key:"Username" split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Azure Key Vault. The field to extract from the json secret is named - // "Password". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Password". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Password". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Password", since split_words is set to true. - DatabasePassword string `type:"json" name:"My-Database-Credentials" key:"Password" split_words:"true"` -} - -func main() { - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - log.Fatalf("Failed to obtain a credential: %v", err) - } - - client, err := secretlyazure.NewClient(context.Background(), secretlyazure.Config{ - VaultURI: azureVaultURI, - Credential: cred, - }) - if err != nil { - log.Fatalf("Failed to initialize azure key vault secrets client: %v", err) - } - - // Or initialize by wrapping your own Azure keyvault secrets client. - // - // azsc, err := azsecrets.NewClient(azureVaultURI, cred, nil) - // if err != nil { - // log.Fatalf("Failed to initialize azure key vault secrets client: %v", err) - // } - // client := secretlyazure.Wrap(azsc, secretlyazure.Config{}) - - var sc SecretConfig - err = client.Process(&sc, secretly.ApplyPatch(secretVersionsFilePath)) - if err != nil { - log.Fatalf("Failed to process SecretConfig: %v", err) - } - - log.Printf("Username: %s", sc.DatabaseUsername) - log.Printf("Password: %s", sc.DatabasePassword) -} diff --git a/examples/azure/versions.json b/examples/azure/versions.json deleted file mode 100644 index c8d0dd0..0000000 --- a/examples/azure/versions.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Service_Integration_Token": { - "version": "3" - }, - "My-Database-Credentials_Username": { - "version": "latest" - }, - "My-Database-Credentials_Password": { - "version": "2" - } -} \ No newline at end of file diff --git a/examples/gcp/example.go b/examples/gcp/example.go deleted file mode 100644 index 0685e0c..0000000 --- a/examples/gcp/example.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "context" - "log" - - "github.com/jack-mcveigh/secretly" - secretlygcp "github.com/jack-mcveigh/secretly/gcp" -) - -const ( - gcpProjectId = "project-id-12345" - secretVersionsFilePath = "versions.json" -) - -type SecretConfig struct { - // The secret stores text data and is named "Service_Integration_Token" - // in GCP Secret Manager. Since "split_words" is enabled, version info can be loaded - // from a config file by including the field name, converted to PascalCase to - // Snake_Case, as a key: "Service_Integration_Token". - ServiceIntegrationToken string `split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in GCP Secret Manager. The field to extract from the json secret is named - // "Username". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Username". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Username". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Username", since split_words is set to true. - DatabaseUsername string `type:"json" name:"My-Database-Credentials" key:"Username" split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in GCP Secret Manager. The field to extract from the json secret is named - // "Password". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Password". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Password". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Password", since split_words is set to true. - DatabasePassword string `type:"json" name:"My-Database-Credentials" key:"Password" split_words:"true"` -} - -func main() { - client, err := secretlygcp.NewClient(context.Background(), secretlygcp.Config{ - ProjectID: gcpProjectId, - }) - if err != nil { - log.Fatalf("Failed to initialize gcp secret manager client: %v", err) - } - - // Or initialize by wrapping your own GCP Secret Manager client. - // - // smc, err := secretmanager.NewClient(context.Background()) - // if err != nil { - // log.Fatalf("Failed to initialize gcp secret manager client: %v", err) - // } - // client := secretlygcp.Wrap(smc, secretlygcp.Config{ - // ProjectId: gcpProjectId, - // }) - - var sc SecretConfig - err = client.Process(&sc, secretly.ApplyPatch(secretVersionsFilePath)) - if err != nil { - log.Fatalf("Failed to process SecretConfig: %v", err) - } - - log.Printf("Username: %s", sc.DatabaseUsername) - log.Printf("Password: %s", sc.DatabasePassword) -} diff --git a/examples/gcp/versions.json b/examples/gcp/versions.json deleted file mode 100644 index c8d0dd0..0000000 --- a/examples/gcp/versions.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Service_Integration_Token": { - "version": "3" - }, - "My-Database-Credentials_Username": { - "version": "latest" - }, - "My-Database-Credentials_Password": { - "version": "2" - } -} \ No newline at end of file diff --git a/examples/vault/kvv1/example.go b/examples/vault/kvv1/example.go deleted file mode 100644 index 0ca3e76..0000000 --- a/examples/vault/kvv1/example.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "log" - - vault "github.com/hashicorp/vault/api" - secretlyvault "github.com/jack-mcveigh/secretly/vault" -) - -const ( - vaultToken = "a-fake-token" - vaultMountPath = "a-fake-mount-path" - vaultAddress = "www.google.com" -) - -type SecretConfig struct { - // The secret stores text data and is named "Service_Integration_Token" - // in Vault. Since "split_words" is enabled, version info can be loaded - // from a config file by including the field name, converted to PascalCase to - // Snake_Case, as a key: "Service_Integration_Token". - ServiceIntegrationToken string `split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Vault. The field to extract from the json secret is named - // "Username". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Username". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Username". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Username", since split_words is set to true. - DatabaseUsername string `type:"json" name:"My-Database-Credentials" key:"Username" split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Vault. The field to extract from the json secret is named - // "Password". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Password". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Password". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Password", since split_words is set to true. - DatabasePassword string `type:"json" name:"My-Database-Credentials" key:"Password" split_words:"true"` -} - -func main() { - cfg := vault.DefaultConfig() - cfg.Address = vaultAddress - - client, err := secretlyvault.NewKVv1Client(secretlyvault.Config{ - Token: vaultToken, - MountPath: vaultMountPath, - VaultConfig: cfg, - }) - if err != nil { - log.Fatalf("Failed to initialize vault KV v1 secret engine client: %v", err) - } - - // Or initialize by wrapping your own Vault KV V1 Secret Engine client. - // - // vc, err := vault.NewClient(cfg) - // if err != nil { - // log.Fatalf("Failed to initialize vault KV V1 secret engine client: %v", err) - // } - // vc.SetToken(vaultToken) - // client = secretlyvault.WrapKVv1(vc.KVv1(vaultMountPath), secretlyvault.Config{}) - - var sc SecretConfig - err = client.Process(&sc) - if err != nil { - log.Fatalf("Failed to process SecretConfig: %v", err) - } - - log.Printf("Username: %s", sc.DatabaseUsername) - log.Printf("Password: %s", sc.DatabasePassword) -} diff --git a/examples/vault/kvv2/example.go b/examples/vault/kvv2/example.go deleted file mode 100644 index 9544955..0000000 --- a/examples/vault/kvv2/example.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "log" - - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" - secretlyvault "github.com/jack-mcveigh/secretly/vault" -) - -const ( - vaultToken = "a-fake-token" - vaultMountPath = "a-fake-mount-path" - vaultAddress = "www.google.com" - secretVersionsFilePath = "versions.json" -) - -type SecretConfig struct { - // The secret stores text data and is named "Service_Integration_Token" - // in Vault. Since "split_words" is enabled, version info can be loaded - // from a config file by including the field name, converted to PascalCase to - // Snake_Case, as a key: "Service_Integration_Token". - ServiceIntegrationToken string `split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Vault. The field to extract from the json secret is named - // "Username". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Username". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Username". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Username", since split_words is set to true. - DatabaseUsername string `type:"json" name:"My-Database-Credentials" key:"Username" split_words:"true"` - - // The secret stores a json map and is named "My-Database-Credentials" - // in Vault. The field to extract from the json secret is named - // "Password". Version info from a config can be loaded by the config including the - // key "My-Database-Credentials_Password". Version info from a config can be loaded - // by exporting the variable "My_Database_Credentials_Password". Note, an underscore - // separates the name, "My_Database_Credentials", and the key, - // "Password", since split_words is set to true. - DatabasePassword string `type:"json" name:"My-Database-Credentials" key:"Password" split_words:"true"` -} - -func main() { - cfg := vault.DefaultConfig() - cfg.Address = vaultAddress - - client, err := secretlyvault.NewKVv2Client(secretlyvault.Config{ - Token: vaultToken, - MountPath: vaultMountPath, - VaultConfig: cfg, - }) - if err != nil { - log.Fatalf("Failed to initialize vault KV v2 secret engine client: %v", err) - } - - // Or initialize by wrapping your own Vault KV V2 Secret Engine client. - // - // vc, err := vault.NewClient(cfg) - // if err != nil { - // log.Fatalf("Failed to initialize vault KV V2 secret engine client: %v", err) - // } - // vc.SetToken(vaultToken) - // client := secretlyvault.WrapKVv2(vc.KVv2(vaultMountPath), secretlyvault.Config{}) - - var sc SecretConfig - err = client.Process(&sc, secretly.ApplyPatch(secretVersionsFilePath)) - if err != nil { - log.Fatalf("Failed to process SecretConfig: %v", err) - } - - log.Printf("Username: %s", sc.DatabaseUsername) - log.Printf("Password: %s", sc.DatabasePassword) -} diff --git a/examples/vault/kvv2/versions.json b/examples/vault/kvv2/versions.json deleted file mode 100644 index d97870a..0000000 --- a/examples/vault/kvv2/versions.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Service_Integration_Token": { - "version": "1" - }, - "My-Database-Credentials_Username": { - "version": "0" - }, - "My-Database-Credentials_Password": { - "version": "1" - } -} \ No newline at end of file diff --git a/field.go b/field.go index c26f09e..a727dc8 100644 --- a/field.go +++ b/field.go @@ -13,13 +13,20 @@ import ( yaml "gopkg.in/yaml.v3" ) +type secretType string + // Default values for optional field tags. const ( + // Supported types of secrets values. + Text secretType = "text" + JSON secretType = "json" + YAML secretType = "yaml" + // Defaults. - DefaultType = "text" + DefaultType = Text DefaultVersion = "0" - // Tags. + // Supported tags for modifying secretly's behavior. tagIgnored = "ignored" tagKey = "key" tagName = "name" @@ -31,40 +38,45 @@ const ( var ( regexGatherWords = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)") regexAcronym = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)") +) +var ( ErrInvalidJSONSecret = errors.New("secret is not valid json") ErrInvalidYAMLSecret = errors.New("secret is not valid yaml") ErrSecretMissingKey = errors.New("secret is missing provided key") ) -// Field represents a field in a struct, +type fields = []field + +// field represents a field in a struct, // exposing its secretly tag values // and reference to the underlying value. -type Field struct { - SecretType string - SecretName string - SecretVersion string - MapKeyName string // NOTE: Only used when secretType is "json" or "yaml" - SplitWords bool - Value reflect.Value +type field struct { + secretType secretType + secretName string + secretVersion string + mapKeyName string // NOTE: Only used for JSONType and YAMLType secret types. + splitWords bool + value reflect.Value + cache *cache } -// NewField constructs a field referencing the provided reflect.Value with the tags from +// newField constructs a field referencing the provided reflect.Value with the tags from // the reflect.StructField applied -func NewField(fValue reflect.Value, fStructField reflect.StructField) (Field, error) { +func newField(fValue reflect.Value, fStructField reflect.StructField) (field, error) { var ( - newField Field + newField field ok bool err error ) // Set the reference to the field's reflection - newField.Value = fValue + newField.value = fValue // Get the split_words value, setting it to false if not explicitly set - newField.SplitWords, ok, err = parseOptionalStructTagKey[bool](fStructField, tagSplitWords) + newField.splitWords, ok, err = parseOptionalStructTagKey[bool](fStructField, tagSplitWords) if err != nil { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagSplitWords, Err: err, @@ -72,14 +84,14 @@ func NewField(fValue reflect.Value, fStructField reflect.StructField) (Field, er } if !ok { - newField.SplitWords = false + newField.splitWords = false } // Get the type value, setting it to the default, "text", if not explicitly set. // Also perform validation to ensure only valid types are provided - newField.SecretType, ok, err = parseOptionalStructTagKey[string](fStructField, tagType) + newField.secretType, ok, err = parseOptionalStructTagKey[secretType](fStructField, tagType) if err != nil { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagType, Err: err, @@ -87,24 +99,24 @@ func NewField(fValue reflect.Value, fStructField reflect.StructField) (Field, er } if !ok { - newField.SecretType = DefaultType + newField.secretType = DefaultType } - switch newField.SecretType { - case "text", "json", "yaml": + switch newField.secretType { + case Text, JSON, YAML: default: - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagType, - Err: ErrInvalidSecretType, + Err: fmt.Errorf("%w: %q", ErrInvalidSecretType, newField.secretType), } } // Get the name value, setting it to the field's name if not explicitly set. // Split the words if the default value was used and split_words was set to true - newField.SecretName, ok, err = parseOptionalStructTagKey[string](fStructField, tagName) + newField.secretName, ok, err = parseOptionalStructTagKey[string](fStructField, tagName) if err != nil { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagName, Err: err, @@ -112,34 +124,28 @@ func NewField(fValue reflect.Value, fStructField reflect.StructField) (Field, er } if !ok { - newField.SecretName = fStructField.Name - if newField.SplitWords { - newField.SecretName = splitWords(newField.SecretName) - } + newField.secretName = fStructField.Name } // Get the key value, if the type is "json" or "yaml", and setting it to the field's name // if not explicitly set. Split the words if the default value was used and // split_words was set to true - switch newField.SecretType { - case "json", "yaml": - newField.MapKeyName, ok, err = parseOptionalStructTagKey[string](fStructField, tagKey) + switch newField.secretType { + case JSON, YAML: + newField.mapKeyName, ok, err = parseOptionalStructTagKey[string](fStructField, tagKey) if err != nil { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagKey, Err: err, } } if !ok { - newField.MapKeyName = fStructField.Name - if newField.SplitWords { - newField.MapKeyName = splitWords(newField.MapKeyName) - } + newField.mapKeyName = fStructField.Name } default: if _, ok = fStructField.Tag.Lookup(tagKey); ok { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagKey, Err: ErrSecretTypeDoesNotSupportKey, @@ -149,66 +155,80 @@ func NewField(fValue reflect.Value, fStructField reflect.StructField) (Field, er // Get the version value, setting it to the default, "default", if not explicitly // set. Split the words if the default value was used and split_words was set to true - newField.SecretVersion, ok, err = parseOptionalStructTagKey[string](fStructField, tagVersion) + newField.secretVersion, ok, err = parseOptionalStructTagKey[string](fStructField, tagVersion) if err != nil { - return Field{}, StructTagError{ + return field{}, StructTagError{ Name: fStructField.Name, Key: tagVersion, Err: err, } } if !ok { - newField.SecretVersion = DefaultVersion + newField.secretVersion = DefaultVersion } return newField, nil } +func (f *field) SecretName() string { + if f.splitWords { + return splitWords(f.secretName) + } + + return f.secretName +} + +func (f *field) MapKeyName() string { + if f.splitWords { + return splitWords(f.mapKeyName) + } + + return f.mapKeyName +} + // Name returns the resolved name of the field. If the secret type is "json" or "yaml", // the secret name and key name are combined. If "split_words" is true, the combination // of secret name and key name are transformed into uppercase, snake case. -func (f *Field) Name() string { - name := f.SecretName - - switch f.SecretType { - case "json", "yaml": +func (f *field) Name() string { + switch f.secretType { + case JSON, YAML: var delimiter string - if f.SplitWords { + if f.splitWords { delimiter = "_" } - name += delimiter + f.MapKeyName + return f.SecretName() + delimiter + f.MapKeyName() } - return name + return f.SecretName() } // Set sets the field's reflect.Value with b. -func (f *Field) Set(b []byte) error { - switch f.SecretType { - case "text": +func (f *field) Set(b []byte) error { + switch f.secretType { + case Text: return f.setText(b) - case "json": + case JSON: return f.setJSON(b) - case "yaml": + case YAML: return f.setYAML(b) default: - return ErrInvalidSecretType + return fmt.Errorf("%w: %v", ErrInvalidSecretType, f.secretType) } } // setText sets the field's underlying value, // handling the input as a "text" secret. -func (f *Field) setText(b []byte) error { - const ErrFailedConvertFormat = "failed to convert secret \"%s's\" key, \"%s\" to %s: %w" +func (f *field) setText(b []byte) error { + const failedConvertErrFormat = "failed to convert secret %q to %s: %w" byteString := string(b) - valueType := f.Value.Type() + valueType := f.value.Type() - switch f.Value.Kind() { + switch f.value.Kind() { case reflect.String: - f.Value.SetString(byteString) + f.value.SetString(byteString) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: var ( @@ -216,7 +236,7 @@ func (f *Field) setText(b []byte) error { err error ) - if f.Value.Kind() == reflect.Int64 && valueType.PkgPath() == "time" && valueType.Name() == "Duration" { + if f.value.Kind() == reflect.Int64 && valueType.PkgPath() == "time" && valueType.Name() == "Duration" { var d time.Duration d, err = time.ParseDuration(byteString) value = int64(d) @@ -226,38 +246,38 @@ func (f *Field) setText(b []byte) error { if err != nil { t := fmt.Sprintf("int%d", valueType.Bits()) - return fmt.Errorf(ErrFailedConvertFormat, f.SecretName, f.MapKeyName, t, err) + return fmt.Errorf(failedConvertErrFormat, f.Name(), t, err) } - f.Value.SetInt(value) + f.value.SetInt(value) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: value, err := strconv.ParseUint(byteString, 0, valueType.Bits()) if err != nil { t := fmt.Sprintf("uint%d", valueType.Bits()) - return fmt.Errorf(ErrFailedConvertFormat, f.SecretName, f.MapKeyName, t, err) + return fmt.Errorf(failedConvertErrFormat, f.Name(), t, err) } - f.Value.SetUint(value) + f.value.SetUint(value) case reflect.Bool: value, err := strconv.ParseBool(byteString) if err != nil { - return fmt.Errorf(ErrFailedConvertFormat, f.SecretName, f.MapKeyName, "bool", err) + return fmt.Errorf(failedConvertErrFormat, f.Name(), "bool", err) } - f.Value.SetBool(value) + f.value.SetBool(value) case reflect.Float32, reflect.Float64: value, err := strconv.ParseFloat(byteString, valueType.Bits()) if err != nil { t := fmt.Sprintf("float%d", valueType.Bits()) - return fmt.Errorf(ErrFailedConvertFormat, f.SecretName, f.MapKeyName, t, err) + return fmt.Errorf(failedConvertErrFormat, f.Name(), t, err) } - f.Value.SetFloat(value) + f.value.SetFloat(value) } return nil @@ -265,7 +285,7 @@ func (f *Field) setText(b []byte) error { // setJSON sets the field's underlying value, // handling the input as a "json" secret. -func (f *Field) setJSON(b []byte) error { +func (f *field) setJSON(b []byte) error { var secretMap map[string]string err := json.Unmarshal(b, &secretMap) @@ -273,16 +293,16 @@ func (f *Field) setJSON(b []byte) error { return ErrInvalidJSONSecret } - if value, ok := secretMap[f.MapKeyName]; ok { + if value, ok := secretMap[f.MapKeyName()]; ok { return f.setText([]byte(value)) } - return fmt.Errorf("%w: secret \"%s\" missing \"%s\"", ErrSecretMissingKey, f.SecretName, f.MapKeyName) + return fmt.Errorf("%w: secret \"%s\" missing \"%s\"", ErrSecretMissingKey, f.SecretName(), f.MapKeyName()) } // setYAML sets the field's underlying value, // handling the input as a "yaml" secret -func (f *Field) setYAML(b []byte) error { +func (f *field) setYAML(b []byte) error { var secretMap map[string]string err := yaml.Unmarshal(b, &secretMap) @@ -290,11 +310,11 @@ func (f *Field) setYAML(b []byte) error { return ErrInvalidYAMLSecret } - if value, ok := secretMap[f.MapKeyName]; ok { + if value, ok := secretMap[f.MapKeyName()]; ok { return f.setText([]byte(value)) } - return fmt.Errorf("%w: secret \"%s\" missing \"%s\"", ErrSecretMissingKey, f.SecretName, f.MapKeyName) + return fmt.Errorf("%w: secret \"%s\" missing \"%s\"", ErrSecretMissingKey, f.SecretName(), f.MapKeyName()) } // parseOptionalStructTagKey parses the provided key's value from the struct field, @@ -305,17 +325,21 @@ func parseOptionalStructTagKey[T any](structField reflect.StructField, key strin if raw, ok = structField.Tag.Lookup(key); ok { // If key present switch any(value).(type) { + case secretType: + value, ok = any(secretType(raw)).(T) case string: value = any(raw).(T) case int: - i, err := strconv.Atoi(raw) + var i int + i, err = strconv.Atoi(raw) if err != nil { break } value = any(i).(T) case bool: - b, err := strconv.ParseBool(raw) + var b bool + b, err = strconv.ParseBool(raw) if err != nil { break } diff --git a/gcp/client.go b/gcp/client.go deleted file mode 100644 index 23cdb7d..0000000 --- a/gcp/client.go +++ /dev/null @@ -1,154 +0,0 @@ -package gcp - -import ( - "context" - "fmt" - "strconv" - - secretmanager "cloud.google.com/go/secretmanager/apiv1" - "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - gax "github.com/googleapis/gax-go/v2" - "github.com/jack-mcveigh/secretly" - "google.golang.org/api/option" -) - -const secretVersionsFormat = "projects/%s/secrets/%s/versions/%s" - -// gcpsmc describes required GCP Secret Manager client methods -type gcpsmc interface { - AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) - Close() error -} - -// Config provides both GCP Secret Manager client and secretly wrapper configurations. -type Config struct { - // ProjectID identifies the GCP project from which to retrieve the secrets. - ProjectID string - - SecretlyConfig secretly.Config -} - -// Client is the GCP Secret Manager Client wrapper. -// Implements secretly.Client -type Client struct { - // client is the GCP Secret Manager client. - client gcpsmc - - // projectId identifies the GCP project from which to retrieve the secrets. - projectId string - - // secretCache is the cache that stores secrets => versions => content - // to reduce secret manager accesses. - secretCache secretly.SecretCache -} - -// Compile time check to assert that client implements secretly.Client -var _ secretly.Client = (*Client)(nil) - -// NewClient returns a GCP client wrapper -// with the options applied. -// Will error if authentication with the secret manager fails. -func NewClient(ctx context.Context, cfg Config, opts ...option.ClientOption) (*Client, error) { - smc, err := secretmanager.NewClient(ctx, opts...) - if err != nil { - return nil, err - } - - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: smc, - projectId: cfg.ProjectID, - secretCache: sc, - } - return c, nil -} - -// Wrap wraps the GCP client. -func Wrap(client *secretmanager.Client, cfg Config) *Client { - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &Client{ - client: client, - projectId: cfg.ProjectID, - secretCache: sc, - } - return c -} - -// Process resolves the provided specification -// using GCP Secret Manager. -// ProcessOptions can be provided -// to add additional processing for the fields, -// like reading version info from the env or a file. -// -// (*Client).Process is a convenience -// for calling secretly.Process with the Client. -func (c *Client) Process(spec any, opts ...secretly.ProcessOption) error { - return secretly.Process(c, spec, opts...) -} - -// GetSecret retrieves the latest secret version for name -// from GCP Secret Manager. -func (c *Client) GetSecret(ctx context.Context, name string) ([]byte, error) { - if b, hit := c.secretCache.Get(name, "latest"); hit { - return b, nil - } - b, err := c.getSecretVersion(ctx, name, "latest") - c.secretCache.Add(name, "latest", b) - return b, err -} - -// GetSecretWithVersion retrieves the specific secret version for name -// from GCP Secret Manager. -func (c *Client) GetSecretWithVersion(ctx context.Context, name, version string) ([]byte, error) { - switch version { - case secretly.DefaultVersion, "latest": - return c.GetSecret(ctx, name) - default: - _, err := strconv.ParseUint(version, 10, 0) - if err != nil { - return nil, secretly.ErrInvalidSecretVersion - } - } - - if b, hit := c.secretCache.Get(name, version); hit { - return b, nil - } - - b, err := c.getSecretVersion(ctx, name, version) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, version, b) - return b, nil -} - -// getSecret retrieves the a specific version of the secret from the GCP Secret Manager. -func (c *Client) getSecretVersion(ctx context.Context, name, version string) ([]byte, error) { - req := &secretmanagerpb.AccessSecretVersionRequest{ - Name: fmt.Sprintf(secretVersionsFormat, c.projectId, name, version), - } - - resp, err := c.client.AccessSecretVersion(ctx, req) - if err != nil { - return nil, err - } - return resp.GetPayload().GetData(), nil -} - -// Close releases resources consumed by the client. -func (c *Client) Close() error { - return c.client.Close() -} diff --git a/gcp/client_test.go b/gcp/client_test.go deleted file mode 100644 index 151aacc..0000000 --- a/gcp/client_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package gcp - -import ( - "context" - "errors" - "fmt" - "reflect" - "testing" - - "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - gax "github.com/googleapis/gax-go/v2" - "github.com/jack-mcveigh/secretly" -) - -const testProjectId = "test-project" - -var ( - errSecretNotFound = errors.New("secret not found") - errSecretAccessedMoreThanOnce = errors.New("secret accessed more than once") - - testSecretContent = []byte("secret content") -) - -type secretInfo struct { - name string - version string -} - -type stubClient struct { - secrets map[string][]byte - - accessed bool - failIfAccessedMoreThanOnce bool -} - -func newStubClientWithSecrets() *stubClient { - c := &stubClient{ - secrets: make(map[string][]byte), - } - - c.secrets[fmt.Sprintf(secretVersionsFormat, testProjectId, "fake-secret", "latest")] = testSecretContent - c.secrets[fmt.Sprintf(secretVersionsFormat, testProjectId, "fake-secret", "1")] = testSecretContent - - return c -} - -func (c *stubClient) AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) { - if c.failIfAccessedMoreThanOnce && c.accessed { - return nil, errSecretAccessedMoreThanOnce - } - c.accessed = true - - if b, ok := c.secrets[req.Name]; ok { - resp := &secretmanagerpb.AccessSecretVersionResponse{ - Payload: &secretmanagerpb.SecretPayload{ - Data: b, - }, - } - return resp, nil - } - return nil, errSecretNotFound -} - -func (c *stubClient) Close() error { return nil } - -func TestGetSecretVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfo secretInfo - want []byte - wantErr error - }{ - { - name: "Success With Default Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Numbered Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "1", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Latest Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "latest", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Secret Does Not Exist", - secretInfo: secretInfo{ - name: "fake-secret-that-does-not-exist", - version: "0", - }, - want: nil, - wantErr: errSecretNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubClientWithSecrets() - c := Client{client: smc, projectId: testProjectId, secretCache: secretly.NewSecretCache()} - - got, err := c.GetSecretWithVersion(context.Background(), tt.secretInfo.name, tt.secretInfo.version) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Incorrect secret content. Want %v, got %v", tt.want, got) - } - }) - } -} - -func TestGetSecretVersionCaching(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfos []secretInfo - wantErr error - }{ - { - name: "Cache Hit", - secretInfos: []secretInfo{ - { - name: "fake-secret", - version: "latest", - }, - { - name: "fake-secret", - version: "latest", - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubClientWithSecrets() - smc.failIfAccessedMoreThanOnce = true - - c := Client{ - client: smc, - projectId: testProjectId, - secretCache: secretly.NewSecretCache(), - } - - var err error - for _, secretInfo := range tt.secretInfos { - _, err = c.GetSecretWithVersion(context.Background(), secretInfo.name, secretInfo.version) - } - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - }) - } -} diff --git a/go.mod b/go.mod index 1f24bd9..d80cf87 100644 --- a/go.mod +++ b/go.mod @@ -2,58 +2,4 @@ module github.com/jack-mcveigh/secretly go 1.20 -require ( - cloud.google.com/go/secretmanager v1.10.1 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 - github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 - github.com/aws/aws-sdk-go v1.44.271 - github.com/googleapis/gax-go/v2 v2.9.1 - github.com/hashicorp/vault/api v1.9.2 - google.golang.org/api v0.124.0 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - cloud.google.com/go/compute v1.19.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect - github.com/golang-jwt/jwt v3.2.1+incompatible // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect - go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.55.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect -) +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 1e8e12d..a62c313 100644 --- a/go.sum +++ b/go.sum @@ -1,293 +1,4 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/secretmanager v1.10.1 h1:9QwQ3oMurvmPEmM80spGe2SFGDa+RRgkLIdTm3gMWO8= -cloud.google.com/go/secretmanager v1.10.1/go.mod h1:pxG0NLpcK6OMy54kfZgQmsKTPxJem708X1es7xv8n60= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 h1:leh5DwKv6Ihwi+h60uHtn6UWAxBbZ0q8DwQVMzf61zw= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 h1:xnO4sFyG8UH2fElBkcqLTOZsAajvKfnSlgBBW8dXYjw= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0/go.mod h1:XD3DIOOVgBCO03OleB1fHjgktVRFxlT++KwKgIOewdM= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= -github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= -github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.44.271 h1:aa+Nu2JcnFmW1TLIz/67SS7KPq1I1Adl4RmExSMjGVo= -github.com/aws/aws-sdk-go v1.44.271/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= -github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.9.1 h1:DpTpJqzZ3NvX9zqjhIuI1oVzYZMvboZe+3LoeEIJjHM= -github.com/googleapis/gax-go/v2 v2.9.1/go.mod h1:4FG3gMrVZlyMp5itSYKMU9z/lBE7+SbnUOvzH2HqbEY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.9.2 h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as= -github.com/hashicorp/vault/api v1.9.2/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.124.0 h1:dP6Ef1VgOGqQ8eiv4GiY8RhmeyqzovcXBYPDUYG8Syo= -google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/options.go b/options.go new file mode 100644 index 0000000..756bc84 --- /dev/null +++ b/options.go @@ -0,0 +1,164 @@ +package secretly + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + yaml "gopkg.in/yaml.v3" +) + +type ( + // ProcessOptions are optional modifiers for secret processing. + ProcessOption func(fields) error + + unmarshalFunc func([]byte, any) error + + secretConfig struct { + Type secretType `json:"type" yaml:"type"` + Name string `json:"name" yaml:"name"` + Key string `json:"key" yaml:"key"` + Version string `json:"version" yaml:"version"` + SplitWords bool `json:"split_words" yaml:"split_words"` + } +) + +var ErrInvalidFileType = errors.New("invalid file type") + +// WithDefaultVersion overwrites the default version, [secretly.DefaultVersion], +// with the provided version. +// Use this to set the default version to aliases like "latest" or "AWSCURRENT". +func WithDefaultVersion(version string) ProcessOption { + return func(fields fields) error { + for i, f := range fields { + if f.secretVersion == DefaultVersion { + fields[i].secretVersion = version + } + } + + return nil + } +} + +// WithCache caches secrets in memory +// to avoid unnecessary calls to the secret manager. +// Do not use this option if you want your application +// to handle secrets changes without restarting. +func WithCache() ProcessOption { + return func(fields fields) error { + cache := newCache() + + for i := range fields { + fields[i].cache = cache + } + + return nil + } +} + +// WithPatch returns an ProcessOption which overwrites +// the specified/default field values with the provided patch. +// Can be used to overwrite any of the configurable field values. +// +// Must be written in either YAML or YAML compatible JSON +func WithPatch(patch []byte) ProcessOption { + return func(fields fields) error { + return setFieldsWithPatch(yaml.Unmarshal, patch, fields) + } +} + +// WithPatchFile returns an ProcessOption which overwrites +// the specified/default field values with the provided patch. +// Can be used to overwrite any of the configurable field values. +// +// Types of patch files are determined by their extensions. +// Accepted patch file types are: +// 1. JSON (.json) +// 2. YAML (.yaml,.yml) +func WithPatchFile(filePath string) ProcessOption { + return func(fields fields) error { + b, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("reading patch file: %w", err) + } + + switch ext := filepath.Ext(filePath); ext { + case ".json": + err = setFieldsWithPatch(json.Unmarshal, b, fields) + case ".yaml", ".yml": + err = setFieldsWithPatch(yaml.Unmarshal, b, fields) + default: + err = fmt.Errorf("%w: %s", ErrInvalidFileType, ext) + } + if err != nil { + return fmt.Errorf("applying patch: %w", err) + } + + return nil + } +} + +// setFieldsWithPatch overwrites fields applying unmarshal to the bytes, b. +func setFieldsWithPatch(unmarshal unmarshalFunc, b []byte, fields fields) error { + secretConfigMap := make(map[string]secretConfig, len(fields)) + + err := unmarshal(b, &secretConfigMap) + if err != nil { + return err + } + + for i, f := range fields { + fmt.Println(f.Name()) + sc, ok := secretConfigMap[f.Name()] + if !ok { + continue + } + + if sc.Type != "" { + fields[i].secretType = sc.Type + } + if sc.Name != "" { + fields[i].secretName = sc.Name + } + if sc.Key != "" { + fields[i].mapKeyName = sc.Key + } + if sc.Version != "" { + fields[i].secretVersion = sc.Version + } + if sc.SplitWords { + fields[i].splitWords = sc.SplitWords + } + } + + return nil +} + +// WithVersionsFromEnv returns an ProcessOption which overwrites +// the specified/default secret versions with versions from the environment. +// Environment variables are to be named with the following logic: +// +// if prefix +// uppercase( prefix + "_" + field.FullName() ) + "_VERSION" +// else +// uppercase( field.FullName() ) + "_VERSION" +func WithVersionsFromEnv(prefix string) ProcessOption { + return func(fields fields) error { + if prefix != "" { + prefix += "_" + } + + for i, field := range fields { + name := strings.ReplaceAll(field.Name(), "-", "_") + key := strings.ToUpper(prefix + name + "_VERSION") + + if v, ok := os.LookupEnv(key); ok { + fields[i].secretVersion = v // TODO: Support types other than string + } + } + return nil + } +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..a6f46ca --- /dev/null +++ b/options_test.go @@ -0,0 +1,172 @@ +package secretly + +import ( + "testing" +) + +func TestWithDefaultVersion(t *testing.T) { + const newDefault = "current" + + fs := fields{ + {secretVersion: "0"}, + {secretVersion: "1"}, + } + + err := WithDefaultVersion(newDefault)(fs) + if err != nil { + t.Fatalf("Incorrect error. Want %v, Got %v", err, nil) + } + + if fs[0].secretVersion != newDefault { + t.Fatalf("Incorrect fields[0].SecretVersion. Want %v, Got %v", newDefault, fs[0].secretVersion) + } + + if fs[1].secretVersion != "1" { + t.Fatalf("Incorrect fields[0].SecretVersion. Want %v, Got %v", newDefault, fs[0].secretVersion) + } +} + +func TestWithCache(t *testing.T) { + fs := fields{{}} + + err := WithCache()(fs) + if err != nil { + t.Fatalf("Incorrect error. Want %v, Got %v", err, nil) + } + + if fs[0].cache == nil { + t.Fatalf("Incorrect fields[0].Cache. Got %v", fs) + } +} + +func TestWithPatch(t *testing.T) { + fs := fields{ + field{ + secretType: DefaultType, + secretName: "TopSecret", + secretVersion: DefaultVersion, + mapKeyName: "", + }, + field{ + secretType: JSON, + secretName: "TopSecret", + secretVersion: DefaultVersion, + mapKeyName: "SpecificSecret", + splitWords: true, + }, + } + + patch := []byte(`{ + "TopSecret": { + "version": "latest" + }, + "Top_Secret_Specific_Secret": { + "version": "1" + } + }`) + + err := WithPatch(patch)(fs) + if err != nil { + t.Fatalf("Incorrect error. Want %v, Got %v", nil, err) + } + + if fs[0].secretVersion != "latest" { + t.Errorf("Incorrect fields[0].SecretVersion. Want %v, got %v", "latest", fs[0].secretVersion) + } + + if fs[1].secretVersion != "1" { + t.Errorf("Incorrect fields[1].SecretVersion. Want %v, got %v", "1", fs[1].secretVersion) + } +} + +func TestWithPatchFile(t *testing.T) { + tests := []struct { + name string + fileName string + }{ + { + name: "JSON Patch File", + fileName: "testdata/patch_0.json", + }, + { + name: "YAML Patch File", + fileName: "testdata/patch_1.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := fields{ + field{ + secretType: DefaultType, + secretName: "TopSecret", + secretVersion: DefaultVersion, + mapKeyName: "", + }, + field{ + secretType: JSON, + secretName: "TopSecret", + secretVersion: DefaultVersion, + mapKeyName: "SpecificSecret", + }, + field{ + secretType: YAML, + secretName: "AnotherTopSecret", + secretVersion: DefaultVersion, + mapKeyName: "SpecificSecret", + splitWords: true, + }, + } + + err := WithPatchFile(tt.fileName)(fs) + if err != nil { + t.Fatalf("Incorrect error. Want %v, Got %v", nil, err) + } + + if fs[0].secretVersion != "latest" { + t.Errorf("Incorrect fields[0].SecretVersion. Want %v, got %v", "latest", fs[0].secretVersion) + } + + if fs[1].secretVersion != "1" { + t.Errorf("Incorrect fields[1].SecretVersion. Want %v, got %v", "1", fs[1].secretVersion) + } + + if fs[2].secretVersion != "100" { + t.Errorf("Incorrect fields[2].SecretVersion. Want %v, got %v", "1", fs[2].secretVersion) + } + }) + } +} + +func TestWithVersionsFromEnv(t *testing.T) { + fs := fields{ + field{ + secretType: DefaultType, + secretName: "SECRET", + secretVersion: DefaultVersion, + mapKeyName: "", + }, + field{ + secretType: JSON, + secretName: "SUPER", + secretVersion: DefaultVersion, + mapKeyName: "SECRET", + }, + } + + t.Setenv("TEST_SECRET_VERSION", "latest") + t.Setenv("TEST_SUPERSECRET_VERSION", "1") + + err := WithVersionsFromEnv("TEST")(fs) + if err != nil { + t.Fatalf("Incorrect error. Want %v, Got %v", nil, err) + } + + if fs[0].secretVersion != "latest" { + t.Errorf("Incorrect fields[0].SecretVersion. Want %v, got %v", "latest", fs[0].secretVersion) + } + + if fs[1].secretVersion != "1" { + t.Errorf("Incorrect fields[1].SecretVersion. Want %v, got %v", "latest", fs[1].secretVersion) + } +} diff --git a/process.go b/process.go deleted file mode 100644 index 8946a35..0000000 --- a/process.go +++ /dev/null @@ -1,135 +0,0 @@ -package secretly - -import ( - "context" - "fmt" - "reflect" -) - -// Process interprets the provided specification, -// resolving the described secrets -// with the provided secret management Client. -func Process(client Client, spec any, opts ...ProcessOption) error { - fields, err := processSpec(spec, opts...) - if err != nil { - return fmt.Errorf("Process: %w", err) - } - - for _, field := range fields { - b, err := client.GetSecretWithVersion(context.Background(), field.SecretName, field.SecretVersion) - if err != nil { - return fmt.Errorf("Process: %w", err) - } - - err = field.Set(b) - if err != nil { - return fmt.Errorf("Process: %w", err) - } - } - - return nil -} - -// processSpec interprets the provided specification, -// returning a slice of fields referencing the specification's fields. -// opts can be provided to add additional processing to the fields, -// like reading version info from the env or a file. -// -// spec must be a pointer to a struct, -// otherwise [ErrInvalidSpecification] is returned. -func processSpec(spec any, opts ...ProcessOption) ([]Field, error) { - // ensure spec is a struct pointer - specValue := reflect.ValueOf(spec) - if specValue.Kind() != reflect.Ptr { - return nil, ErrInvalidSpecification - } - - specValue = specValue.Elem() - if specValue.Kind() != reflect.Struct { - return nil, ErrInvalidSpecification - } - - specType := specValue.Type() - - fields, err := processStruct(specValue, specType) - if err != nil { - return nil, err - } - - for _, opt := range opts { - err := opt(fields) - if err != nil { - return nil, err - } - } - - return fields, nil -} - -// processStruct recursively processes the struct, specValue, -// returning a slice of its fields. -func processStruct(specValue reflect.Value, specType reflect.Type) ([]Field, error) { - fields := make([]Field, 0, specValue.NumField()) - - for i := 0; i < specValue.NumField(); i++ { - fValue, fStructField := specValue.Field(i), specType.Field(i) - - // Get the ignored value, setting it to false if not explicitly set - ignored, _, err := parseOptionalStructTagKey[bool](fStructField, tagIgnored) - if err != nil { - return nil, StructTagError{ - Name: fStructField.Name, - Key: tagIgnored, - Err: err, - } - } - - if ignored || !fValue.CanSet() { - continue - } - - switch fStructField.Type.Kind() { - case reflect.Interface | reflect.Array | reflect.Slice | reflect.Map: - // ignore these types - case reflect.Struct: - fs, err := processStruct(fValue, fStructField.Type) - if err != nil { - return nil, err - } - fields = append(fields, fs...) - case reflect.Pointer: - for fValue.Kind() == reflect.Ptr { - if fValue.IsNil() { - if fValue.Type().Elem().Kind() != reflect.Struct { - // value other than struct - break - } - // value is a struct, initialize it - fValue.Set(reflect.New(fValue.Type().Elem())) - } - fValue = fValue.Elem() - } - - if fValue.Kind() == reflect.Struct { - subFields, err := processStruct(fValue, fValue.Type()) - if err != nil { - return nil, err - } - - fields = append(fields, subFields...) - - continue - } - - fallthrough - default: - field, err := NewField(fValue, fStructField) - if err != nil { - return nil, err - } - - fields = append(fields, field) - } - } - return fields, nil -} diff --git a/process_options.go b/process_options.go deleted file mode 100644 index bad15f2..0000000 --- a/process_options.go +++ /dev/null @@ -1,117 +0,0 @@ -package secretly - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - yaml "gopkg.in/yaml.v3" -) - -type ( - // ProcessOptions are optional modifiers for secret processing. - ProcessOption func([]Field) error - - unmarshalFunc func([]byte, any) error - - secretConfig struct { - Type string `json:"type" yaml:"type"` - Name string `json:"name" yaml:"name"` - Key string `json:"key" yaml:"key"` - Version string `json:"version" yaml:"version"` - SplitWords bool `json:"split_words" yaml:"split_words"` - } -) - -var ErrInvalidFileType = errors.New("invalid file type") - -// ApplyPatch returns an ProcessOption which overwrites -// the specified/default field values with the provided patch. -// Can be used to overwrite any of the configurable field values. -// -// Types of patch files are determined by their extensions. -// Accepted patch file types are: -// 1. JSON (.json) -// 2. YAML (.yaml,.yml) -func ApplyPatch(filePath string) ProcessOption { - return func(fields []Field) error { - b, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("ApplyPatch: %w", err) - } - - switch ext := filepath.Ext(filePath); ext { - case ".json": - err = setFieldsWithPatch(json.Unmarshal, b, fields) - case ".yaml", ".yml": - err = setFieldsWithPatch(yaml.Unmarshal, b, fields) - default: - err = fmt.Errorf("%w: %s", ErrInvalidFileType, ext) - } - return fmt.Errorf("ApplyPatch: %w", err) - } -} - -// setFieldsWithPatch overwrites fields applying unmarshal to the bytes, b. -func setFieldsWithPatch(unmarshal unmarshalFunc, b []byte, fields []Field) error { - secretConfigMap := make(map[string]secretConfig, len(fields)) - - err := unmarshal(b, &secretConfigMap) - if err != nil { - return err - } - - for idx, f := range fields { - sc, ok := secretConfigMap[f.Name()] - if !ok { - continue - } - - if sc.Type != "" { - fields[idx].SecretType = sc.Type - } - if sc.Name != "" { - fields[idx].SecretName = sc.Name - } - if sc.Key != "" { - fields[idx].MapKeyName = sc.Key - } - if sc.Version != "" { - fields[idx].SecretVersion = sc.Version - } - if sc.SplitWords { - fields[idx].SplitWords = sc.SplitWords - } - } - - return nil -} - -// WithVersionsFromEnv returns an ProcessOption which overwrites -// the specified/default secret versions with versions from the environment. -// Environment variables are to be named with the following logic: -// -// if prefix -// uppercase( prefix + "_" + field.Name() ) + "_VERSION" -// else -// uppercase( field.Name() ) + "_VERSION" -func WithVersionsFromEnv(prefix string) ProcessOption { - return func(fields []Field) error { - if prefix != "" { - prefix += "_" - } - - for i, field := range fields { - name := strings.ReplaceAll(field.Name(), "-", "_") - key := strings.ToUpper(prefix + name + "_VERSION") - - if v, ok := os.LookupEnv(key); ok { - fields[i].SecretVersion = v // TODO: Support types other than string - } - } - return nil - } -} diff --git a/process_options_test.go b/process_options_test.go deleted file mode 100644 index f2f4aa6..0000000 --- a/process_options_test.go +++ /dev/null @@ -1,429 +0,0 @@ -package secretly - -import ( - "encoding/json" - "errors" - "os" - "reflect" - "testing" - - yaml "gopkg.in/yaml.v3" -) - -type testingSpecification struct { - TextSecret string `split_words:"true"` - JsonSecret string `type:"json" key:"Key" split_words:"true"` - YamlSecret string `type:"yaml" key:"Key"` -} - -func newTestingSpecificationFields() []Field { - return []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "latest", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: false, - }, - } -} - -func TestApplyConfig(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - unmarshalFunc unmarshalFunc - content []byte - want []Field - wantErr bool - }{ - { - name: "Only Versions JSON", - unmarshalFunc: json.Unmarshal, - content: onlyVersionsJsonBytes, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "1", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: false, - }, - }, - wantErr: false, - }, - { - name: "All Fields JSON", - unmarshalFunc: json.Unmarshal, - content: allFieldsJsonBytes, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret_Overwritten", - SecretVersion: "1", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret_Overwritten", - SecretVersion: "latest", - MapKeyName: "Key_Overwritten", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret_Overwritten", - SecretVersion: "latest", - MapKeyName: "Key_Overwritten", - SplitWords: true, - }, - }, - wantErr: false, - }, - { - name: "Invalid JSON", - unmarshalFunc: json.Unmarshal, - content: invalidJsonBytes, - want: newTestingSpecificationFields(), - wantErr: true, - }, - { - name: "Only Versions YAML", - unmarshalFunc: yaml.Unmarshal, - content: onlyVersionsYamlBytes, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "1", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: false, - }, - }, - wantErr: false, - }, - { - name: "All Fields Yaml", - unmarshalFunc: yaml.Unmarshal, - content: allFieldsYamlBytes, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret_Overwritten", - SecretVersion: "1", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret_Overwritten", - SecretVersion: "latest", - MapKeyName: "Key_Overwritten", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret_Overwritten", - SecretVersion: "latest", - MapKeyName: "Key_Overwritten", - SplitWords: true, - }, - }, - wantErr: false, - }, - { - name: "Invalid YAML", - unmarshalFunc: yaml.Unmarshal, - content: invalidYamlBytes, - want: newTestingSpecificationFields(), - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - fields := newTestingSpecificationFields() - err := setFieldsWithPatch(tt.unmarshalFunc, tt.content, fields) - - // If the test is set up with an invalid input, we don't care what the error is, - // only that there is an error. The same is true for the opposite scenario - if tt.wantErr { - if err == nil { - t.Errorf("Incorrect error. Want an error but did not get one") - } - } else { - if err != nil { - t.Errorf("Incorrect error. Do not want an error but got %v", err) - } - } - - // Don't check reflect.Value for equality - for i := range fields { - fields[i].Value = reflect.Value{} - } - - if !reflect.DeepEqual(tt.want, fields) { - t.Errorf("Incorrect fields. Want %v, got %v", tt.want, fields) - } - }) - } -} - -func TestWithVersionsFromEnv(t *testing.T) { - tests := []struct { - name string - prefix string - envVarMap map[string]string - want []Field - wantErr error - }{ - { - name: "All Env Vars Exist", - prefix: "TEST", - envVarMap: map[string]string{ - "TEST_TEXT_SECRET_VERSION": "1", - "TEST_JSON_SECRET_KEY_VERSION": "latest", - "TEST_YAMLSECRETKEY_VERSION": "latest", - }, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "1", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: false, - }, - }, - wantErr: nil, - }, - { - name: "One Env Var Missing", - prefix: "TEST", - envVarMap: map[string]string{ - "TEST_JSON_SECRET_KEY_VERSION": "latest", - "TEST_YAMLSECRETKEY_VERSION": "latest", - }, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "0", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "latest", - MapKeyName: "Key", - SplitWords: false, - }, - }, - wantErr: nil, - }, - { - name: "All Env Vars Missing", - prefix: "TEST", - envVarMap: map[string]string{}, - want: []Field{ - { - SecretType: DefaultType, - SecretName: "Text_Secret", - SecretVersion: "0", - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "Json_Secret", - SecretVersion: "0", - MapKeyName: "Key", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "YamlSecret", - SecretVersion: "0", - MapKeyName: "Key", - SplitWords: false, - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - // setup env vars. - for k, v := range tt.envVarMap { - t.Setenv(k, v) - } - - t.Run(tt.name, func(t *testing.T) { - spec := testingSpecification{} - fields, err := processSpec(&spec, WithVersionsFromEnv(tt.prefix)) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - // Don't check reflect.Value for equality - for i, field := range fields { - v := reflect.Value{} - if field.Value == v { - t.Errorf("Incorrect field.Value. Got the zero value, should be something else") - } - fields[i].Value = v - } - - if !reflect.DeepEqual(tt.want, fields) { - t.Errorf("Incorrect fields. Want %v, got %v", tt.want, fields) - } - }) - - // teardown env. vars. - for k := range tt.envVarMap { - err := os.Unsetenv(k) - if err != nil { - t.Fatalf("Failed to unset env. var. \"%s\": %v", k, err) - } - } - } -} - -var ( - onlyVersionsJsonBytes = []byte(` -{ - "Text_Secret": { - "version": "1" - }, - "Json_Secret_Key": { - "version": "latest" - }, - "YamlSecretKey": { - "version": "latest" - } -} -`) - - allFieldsJsonBytes = []byte(` -{ - "Text_Secret": { - "name": "Text_Secret_Overwritten", - "version": "1", - "split_words": true - }, - "Json_Secret_Key": { - "name": "Json_Secret_Overwritten", - "key": "Key_Overwritten", - "version": "latest", - "split_words": true - }, - "YamlSecretKey": { - "name": "YamlSecret_Overwritten", - "key": "Key_Overwritten", - "version": "latest", - "split_words": true - } -} -`) - - invalidJsonBytes = []byte(`NOT VALID JSON`) - - onlyVersionsYamlBytes = []byte(` -Text_Secret: - version: 1 -Json_Secret_Key: - version: latest -YamlSecretKey: - version: latest -`) - - allFieldsYamlBytes = []byte(` -Text_Secret: - name: Text_Secret_Overwritten - version: 1 - split_words: true -Json_Secret_Key: - name: Json_Secret_Overwritten - key: Key_Overwritten - version: latest - split_words: true -YamlSecretKey: - name: YamlSecret_Overwritten - key: Key_Overwritten - version: latest - split_words: true -`) - - invalidYamlBytes = []byte(`NOT VALID YAML`) -) diff --git a/process_test.go b/process_test.go deleted file mode 100644 index 57b4585..0000000 --- a/process_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package secretly - -import ( - "errors" - "reflect" - "testing" -) - -// exported to allow testing embedding support -type CorrectSubSpecification struct { - Text string -} - -type correctSpecification struct { - Text string - TextSplitWords string `split_words:"true"` - TextSecretName string `name:"a_secret"` - TextVersion string `version:"latest"` - // NOTE: split_words doesn't do anything when name is provided. - // It only modifies the default name and key, the struct field name. - TextAll string `name:"a_secret" version:"latest" split_words:"true"` - - Json string `type:"json"` - JsonSplitWords string `type:"json" split_words:"true"` - JsonSecretName string `type:"json" name:"a_secret"` - JsonKeyName string `type:"json" key:"a_key"` - JsonVersion string `type:"json" version:"1"` - // NOTE: split_words doesn't do anything when name and key is provided. - // It only modifies the default name and key, the struct field name. - JsonAll string `type:"json" name:"a_secret" key:"a_key" version:"latest" split_words:"true"` - - Yaml string `type:"yaml"` - YamlSplitWords string `type:"yaml" split_words:"true"` - YamlSecretName string `type:"yaml" name:"a_secret"` - YamlKeyName string `type:"yaml" key:"a_key"` - YamlVersion string `type:"yaml" version:"1"` - // NOTE: split_words doesn't do anything when name and key is provided. - // It only modifies the default name and key, the struct field name. - YamlAll string `type:"yaml" name:"a_secret" key:"a_key" version:"latest" split_words:"true"` - - Pointer *string - - ComposedSpecification CorrectSubSpecification - - ComposedSpecificationPtr *CorrectSubSpecification - - CorrectSubSpecification // test embedding - - Ignored string `ignored:"true"` - ignored string - IgnoredComposedSpecification CorrectSubSpecification `ignored:"true"` - ignoredComposedSpecification CorrectSubSpecification -} - -type TextWithKeyNameSpecification struct { - TextKeyName string `name:"a_secret" key:"a_key"` -} - -func TestParsingCorrectSpecification(t *testing.T) { - t.Parallel() - - want := correctSpecificationFields - - spec := correctSpecification{ignored: "testing", ignoredComposedSpecification: CorrectSubSpecification{}} - got, err := processSpec(&spec) - if err != nil { - t.Errorf("Incorrect error. Want %v, got %v", nil, err) - } - - // Ensure reflect.Value is not already the zero value. - // If it isn't set it to the zero value - for i, field := range got { - v := reflect.Value{} - if field.Value == v { - t.Errorf("Incorrect field.Value. Got the zero value, should be something else") - } - got[i].Value = v - } - - if !reflect.DeepEqual(want, got) { - t.Errorf("Incorrect fields. Want %v, got %v", want, got) - } -} - -func TestParsingTextWithKeyNameSpecification(t *testing.T) { - t.Parallel() - - spec := TextWithKeyNameSpecification{} - _, err := processSpec(&spec) - if err != nil { - if !errors.Is(err, ErrSecretTypeDoesNotSupportKey) { - t.Errorf("Incorrect error. Want %v, got %v", ErrSecretTypeDoesNotSupportKey, err) - } - } -} - -func TestParsingNonPointerSpecification(t *testing.T) { - t.Parallel() - - spec := correctSpecification{} - - _, err := processSpec(spec) - if err != nil { - if !errors.Is(err, ErrInvalidSpecification) { - t.Errorf("Incorrect error. Want %v, got %v", ErrInvalidSpecification, err) - } - } -} - -var correctSpecificationFields = []Field{ - // text - { - SecretType: DefaultType, - SecretName: "Text", - SecretVersion: DefaultVersion, - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: DefaultType, - SecretName: "Text_Split_Words", - SecretVersion: DefaultVersion, - MapKeyName: "", - SplitWords: true, - }, - { - SecretType: DefaultType, - SecretName: "a_secret", - SecretVersion: DefaultVersion, - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: DefaultType, - SecretName: "TextVersion", - SecretVersion: "latest", - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: DefaultType, - SecretName: "a_secret", - SecretVersion: "latest", - MapKeyName: "", - SplitWords: true, - }, - // json - { - SecretType: "json", - SecretName: "Json", - SecretVersion: DefaultVersion, - MapKeyName: "Json", - SplitWords: false, - }, - { - SecretType: "json", - SecretName: "Json_Split_Words", - SecretVersion: DefaultVersion, - MapKeyName: "Json_Split_Words", - SplitWords: true, - }, - { - SecretType: "json", - SecretName: "a_secret", - SecretVersion: DefaultVersion, - MapKeyName: "JsonSecretName", - SplitWords: false, - }, - { - SecretType: "json", - SecretName: "JsonKeyName", - SecretVersion: DefaultVersion, - MapKeyName: "a_key", - SplitWords: false, - }, - { - SecretType: "json", - SecretName: "JsonVersion", - SecretVersion: "1", - MapKeyName: "JsonVersion", - SplitWords: false, - }, - { - SecretType: "json", - SecretName: "a_secret", - SecretVersion: "latest", - MapKeyName: "a_key", - SplitWords: true, - }, - // yaml - { - SecretType: "yaml", - SecretName: "Yaml", - SecretVersion: DefaultVersion, - MapKeyName: "Yaml", - SplitWords: false, - }, - { - SecretType: "yaml", - SecretName: "Yaml_Split_Words", - SecretVersion: DefaultVersion, - MapKeyName: "Yaml_Split_Words", - SplitWords: true, - }, - { - SecretType: "yaml", - SecretName: "a_secret", - SecretVersion: DefaultVersion, - MapKeyName: "YamlSecretName", - SplitWords: false, - }, - { - SecretType: "yaml", - SecretName: "YamlKeyName", - SecretVersion: DefaultVersion, - MapKeyName: "a_key", - SplitWords: false, - }, - { - SecretType: "yaml", - SecretName: "YamlVersion", - SecretVersion: "1", - MapKeyName: "YamlVersion", - SplitWords: false, - }, - { - SecretType: "yaml", - SecretName: "a_secret", - SecretVersion: "latest", - MapKeyName: "a_key", - SplitWords: true, - }, - { - SecretType: "text", - SecretName: "Pointer", - SecretVersion: "0", - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: "text", - SecretName: "Text", - SecretVersion: "0", - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: "text", - SecretName: "Text", - SecretVersion: "0", - MapKeyName: "", - SplitWords: false, - }, - { - SecretType: "text", - SecretName: "Text", - SecretVersion: "0", - MapKeyName: "", - SplitWords: false, - }, -} diff --git a/secret_cache.go b/secret_cache.go deleted file mode 100644 index 3877bbb..0000000 --- a/secret_cache.go +++ /dev/null @@ -1,52 +0,0 @@ -package secretly - -// SecretCache describes a secret cache, -// which are used to limit calls -// to the upstream secret manager service. -type SecretCache interface { - // Add adds a secret with its version to the cache. - Add(name, version string, content []byte) - - // Get gets the secret version from the cache. - // A bool is returned to indicate a cache hit or miss. - Get(name, version string) ([]byte, bool) -} - -type noOpSecretCache struct{} - -// NewNoOpSecretCache constructs a no-op secret cache, -// meant to be used for disabling secret caching. -func NewNoOpSecretCache() noOpSecretCache { return noOpSecretCache{} } - -func (noOpSecretCache) Add(_, _ string, content []byte) {} -func (noOpSecretCache) Get(_, _ string) ([]byte, bool) { return nil, false } - -// secretCacheEntry is a map of versions to the secret content. -type secretCacheEntry map[string][]byte - -// secretCache contains the cache, mapping secrets to a [secretCacheEntry]. -type secretCache struct { - cache map[string]secretCacheEntry -} - -// NewSecretCache constructs a secretCache. -func NewSecretCache() secretCache { - return secretCache{cache: make(map[string]secretCacheEntry)} -} - -func (sc secretCache) Add(name, version string, content []byte) { - if sc.cache[name] == nil { - sc.cache[name] = make(secretCacheEntry) - } - sc.cache[name][version] = content -} - -func (sc secretCache) Get(name, version string) ([]byte, bool) { - if _, ok := sc.cache[name]; !ok { - return nil, false - } - if b, ok := sc.cache[name][version]; ok { - return b, true - } - return nil, false -} diff --git a/secretly.go b/secretly.go index e81becd..ff679e0 100644 --- a/secretly.go +++ b/secretly.go @@ -5,36 +5,150 @@ // for retrieving secrets. package secretly -import "context" - -// Config provides configuration -// to change the behavior -// of secretly client wrappers. -type Config struct { - // DisableCaching disables the secret caching feature. - // By default, secret caching is enabled. - // With this set to true, - // repeated gets to the same secret version will reach out - // to the secret manager client. - DisableCaching bool +import ( + "context" + "fmt" + "reflect" +) + +// GetSecretFunc gets the secret from the secret manager. +// If your secret manager does not accept versioning, +// just ignore the version parameter. +type GetSecretFunc func(ctx context.Context, name, version string) ([]byte, error) + +// Process interprets the provided specification, +// resolving the described secrets +// with the provided secret management Client. +func Process(ctx context.Context, spec any, getSecret GetSecretFunc, opts ...ProcessOption) error { + fields, err := processSpec(spec) + if err != nil { + return fmt.Errorf("processing: %w", err) + } + + for _, opt := range opts { + err := opt(fields) + if err != nil { + return err + } + } + + for _, field := range fields { + b, err := getSecret(ctx, field.SecretName(), field.secretVersion) + if err != nil { + return fmt.Errorf("getting secret: secret %q version %q: %w", field.SecretName(), field.secretVersion, err) + } + + err = field.Set(b) + if err != nil { + return fmt.Errorf("setting field: %s: %w", field.Name(), err) + } + } + + return nil +} + +// Process interprets the provided specification, +// resolving the described secrets +// with the provided secret management Client. +func MustProcess(ctx context.Context, spec any, getSecret GetSecretFunc, opts ...ProcessOption) { + if err := Process(ctx, spec, getSecret, opts...); err != nil { + panic(err) + } +} + +// processSpec interprets the provided specification, +// returning a slice of fields referencing the specification's fields. +// opts can be provided to add additional processing to the fields, +// like reading version info from the env or a file. +// +// spec must be a pointer to a struct, +// otherwise [ErrInvalidSpecification] is returned. +func processSpec(spec any) (fields, error) { + // ensure spec is a struct pointer + specValue := reflect.ValueOf(spec) + if specValue.Kind() != reflect.Ptr { + return nil, fmt.Errorf("%w: not a pointer to a struct", ErrInvalidSpecification) + } + + specValue = specValue.Elem() + if specValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("%w: not a pointer to a struct", ErrInvalidSpecification) + } + + specType := specValue.Type() + + fields, err := processStruct(specValue, specType) + if err != nil { + return nil, fmt.Errorf("processing: %w", err) + } + + return fields, nil } -// Client describes a secretly secret manager client wrapper. -type Client interface { - // Process resolves the provided specification. - // ProcessOptions can be provided - // to add additional processing for the fields, - // like reading version info from the env or a file. - // - // (*Client).Process is a convenience - // for calling secretly.Process with the Client. - Process(spec any, opts ...ProcessOption) error - - // GetSecret retrieves the latest secret version for name - // from the secret management service. - GetSecret(ctx context.Context, name string) ([]byte, error) - - // GetSecretWithVersion retrieves the specific secret version for name - // from the secret management service. - GetSecretWithVersion(ctx context.Context, name, version string) ([]byte, error) +// processStruct recursively processes the struct, specValue, +// returning a slice of its fields. +func processStruct(specValue reflect.Value, specType reflect.Type) (fields, error) { + fields := make(fields, 0, specValue.NumField()) + + for i := 0; i < specValue.NumField(); i++ { + fValue, fStructField := specValue.Field(i), specType.Field(i) + + // Get the ignored value, setting it to false if not explicitly set + ignored, _, err := parseOptionalStructTagKey[bool](fStructField, tagIgnored) + if err != nil { + return nil, StructTagError{ + Name: fStructField.Name, + Key: tagIgnored, + Err: err, + } + } + + if ignored || !fValue.CanSet() { + continue + } + + switch fStructField.Type.Kind() { + case reflect.Interface | reflect.Array | reflect.Slice | reflect.Map: + // ignore these types + case reflect.Struct: + fs, err := processStruct(fValue, fStructField.Type) + if err != nil { + return nil, err + } + fields = append(fields, fs...) + case reflect.Pointer: + for fValue.Kind() == reflect.Pointer { + if fValue.IsNil() { + if fValue.Type().Elem().Kind() != reflect.Struct { + // value other than struct + break + } + // value is a struct, initialize it + fValue.Set(reflect.New(fValue.Type().Elem())) + } + fValue = fValue.Elem() + } + + if fValue.Kind() == reflect.Struct { + subFields, err := processStruct(fValue, fValue.Type()) + if err != nil { + return nil, err + } + + fields = append(fields, subFields...) + + continue + } + + fallthrough + default: + field, err := newField(fValue, fStructField) + if err != nil { + return nil, err + } + + fields = append(fields, field) + } + } + return fields, nil } diff --git a/secretly_test.go b/secretly_test.go new file mode 100644 index 0000000..336b94a --- /dev/null +++ b/secretly_test.go @@ -0,0 +1,226 @@ +package secretly + +import ( + "context" + "errors" + "reflect" + "testing" +) + +var errGetSecret = errors.New("get secret error") + +func getSecretFromMapManager(secrets map[string]map[string]string, err error) GetSecretFunc { + return func(ctx context.Context, name, version string) ([]byte, error) { + return []byte(secrets[name][version]), err + } +} + +func TestProcess(t *testing.T) { + type SubSpecification struct { + SubField string + } + + type specification struct { + Field string + IgnoredField string `ignored:"true"` + unexportedField string + SplitField string `split_words:"true"` + VersionedField string `version:"1"` + VersionedField2 string `version:"latest"` + JSONField string `type:"json" key:"Field1"` + YAMLField string `type:"json" key:"Field2"` + SubSpecificationNotEmbedded SubSpecification + SubSpecification + } + _ = specification{unexportedField: ""} // Just to hide the staticcheck warning... + + var secretsMap = map[string]map[string]string{ + "Field": { + "0": "field secret", + }, + "IgnoredField": { + "0": "ignored field secret", + }, + "unexportedField": { + "0": "unexported field secret", + }, + "Split_Field": { + "0": "split field secret", + }, + "VersionedField": { + "1": "versioned field secret", + }, + "VersionedField2": { + "latest": "versioned field secret 2", + }, + "JSONField": { + "0": `{ + "Field1": "json field's field 1 secret", + "Field2": "json field's field 2 secret" + }`, + }, + "YAMLField": { + "0": `{ + "Field1": "yaml field's field 1 secret", + "Field2": "yaml field's field 2 secret" + }`, + }, + "SubField": { + "0": "sub-specification sub-field secret", + }, + } + + tests := []struct { + name string + getSecret GetSecretFunc + wantSpec specification + wantErr error + }{ + { + name: "Simple", + getSecret: getSecretFromMapManager(secretsMap, nil), + wantSpec: specification{ + Field: "field secret", + IgnoredField: "", + SplitField: "split field secret", + VersionedField: "versioned field secret", + VersionedField2: "versioned field secret 2", + JSONField: "json field's field 1 secret", + YAMLField: "yaml field's field 2 secret", + SubSpecificationNotEmbedded: SubSpecification{ + SubField: "sub-specification sub-field secret", + }, + SubSpecification: SubSpecification{ + SubField: "sub-specification sub-field secret", + }, + }, + wantErr: nil, + }, + { + name: "GetSecret Error", + getSecret: getSecretFromMapManager(nil, errGetSecret), + wantSpec: specification{}, + wantErr: errGetSecret, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var spec specification + err := Process(context.Background(), &spec, tt.getSecret) + + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Incorrect error. Want %+v, got %+v", tt.wantErr, err) + } + + if !reflect.DeepEqual(tt.wantSpec, spec) { + t.Fatalf("Incorrect specification. Want %v, got %v", tt.wantSpec, spec) + } + }) + } +} + +func TestProcessWithPatch(t *testing.T) { + type SubSpecification struct { + SubField string + } + + type specification struct { + Field string + IgnoredField string `ignored:"true"` + unexportedField string + SplitField string `split_words:"true"` + VersionedField string `version:"1"` + VersionedField2 string `version:"latest"` + JSONField string `type:"json" key:"Field1"` + YAMLField string `type:"json" key:"Field2"` + SubSpecificationNotEmbedded SubSpecification + SubSpecification + } + _ = specification{unexportedField: ""} // Just to hide the staticcheck warning... + + var secretsMap = map[string]map[string]string{ + "Field": { + "0": "field secret", + }, + "IgnoredField": { + "0": "ignored field secret", + }, + "unexportedField": { + "0": "unexported field secret", + }, + "Split_Field": { + "0": "split field secret", + }, + "VersionedField": { + "1": "versioned field secret", + }, + "VersionedField2": { + "latest": "versioned field secret 2", + }, + "JSONField": { + "0": `{ + "Field1": "json field's field 1 secret", + "Field2": "json field's field 2 secret" + }`, + }, + "YAMLField": { + "0": `{ + "Field1": "yaml field's field 1 secret", + "Field2": "yaml field's field 2 secret" + }`, + }, + "SubField": { + "0": "sub-specification sub-field secret", + }, + } + + tests := []struct { + name string + getSecret GetSecretFunc + wantSpec specification + wantErr error + }{ + { + name: "Simple", + getSecret: getSecretFromMapManager(secretsMap, nil), + wantSpec: specification{ + Field: "field secret", + IgnoredField: "", + SplitField: "split field secret", + VersionedField: "versioned field secret", + VersionedField2: "versioned field secret 2", + JSONField: "json field's field 1 secret", + YAMLField: "yaml field's field 2 secret", + SubSpecificationNotEmbedded: SubSpecification{ + SubField: "sub-specification sub-field secret", + }, + SubSpecification: SubSpecification{ + SubField: "sub-specification sub-field secret", + }, + }, + wantErr: nil, + }, + { + name: "GetSecret Error", + getSecret: getSecretFromMapManager(nil, errGetSecret), + wantSpec: specification{}, + wantErr: errGetSecret, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var spec specification + err := Process(context.Background(), &spec, tt.getSecret) + + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Incorrect error. Want %+v, got %+v", tt.wantErr, err) + } + + if !reflect.DeepEqual(tt.wantSpec, spec) { + t.Fatalf("Incorrect specification. Want %v, got %v", tt.wantSpec, spec) + } + }) + } +} diff --git a/testdata/patch_0.json b/testdata/patch_0.json new file mode 100644 index 0000000..0f0544f --- /dev/null +++ b/testdata/patch_0.json @@ -0,0 +1,11 @@ +{ + "TopSecret": { + "version": "latest" + }, + "TopSecretSpecificSecret": { + "version": "1" + }, + "Another_Top_Secret_Specific_Secret": { + "version": "100" + } +} \ No newline at end of file diff --git a/testdata/patch_1.yaml b/testdata/patch_1.yaml new file mode 100644 index 0000000..8f45869 --- /dev/null +++ b/testdata/patch_1.yaml @@ -0,0 +1,6 @@ +TopSecret: + version: latest +TopSecretSpecificSecret: + version: 1 +Another_Top_Secret_Specific_Secret: + version: 100 diff --git a/vault/kv_v1.go b/vault/kv_v1.go deleted file mode 100644 index 6739105..0000000 --- a/vault/kv_v1.go +++ /dev/null @@ -1,120 +0,0 @@ -package vault - -import ( - "context" - "encoding/json" - "errors" - - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" -) - -var ErrSpecificVersionPassedToKVv1 = errors.New("KVv1 does not accept versioning") - -// vaultKVv1 describes required Vault KVv1 Secrets Engine methods -type vaultKVv1 interface { - Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) -} - -// Client is the Vault KVv1 Secrets Engine wrapper. -// Implements secretly.Client -// -// Note: (*KVv1Client).GetSecretVersion does not accept versioning -// other than the default version. -// (This is a limitation of the secret engine, -// use KVv2 if you want secret versioning.) -type KVv1Client struct { - // client is the Vault KVv1 Secrets Engine. - client vaultKVv1 - - // secretCache is the cache that stores secrets => versions => content - // to reduce secret manager accesses. - secretCache secretly.SecretCache -} - -// Compile time check to assert that client implements secretly.Client -var _ secretly.Client = (*KVv1Client)(nil) - -// NewKVv1Client returns a Vault KVv1 Secrets Engine wrapper. -func NewKVv1Client(cfg Config) (*KVv1Client, error) { - client, err := vault.NewClient(cfg.VaultConfig) - if err != nil { - return nil, err - } - - if cfg.Token != "" { - client.SetToken(cfg.Token) - } - - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &KVv1Client{ - client: client.KVv1(cfg.MountPath), - secretCache: sc, - } - return c, nil -} - -// WrapKVv2 wraps the Vault KVv1 Secrets Engine client. -func WrapKVv1(client *vault.KVv1, cfg Config) *KVv1Client { - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &KVv1Client{ - client: client, - secretCache: sc, - } - return c -} - -// Process resolves the provided specification -// using Vault KVv1 Secrets Engine. -// ProcessOptions can be provided -// to add additional processing for the fields, -// like reading version info from the env or a file. -// -// (*Client).Process is a convenience -// for calling secretly.Process with the Client. -func (c *KVv1Client) Process(spec any, opts ...secretly.ProcessOption) error { - return secretly.Process(c, spec, opts...) -} - -// GetSecret retrieves the latest secret for name -// from Vault KVv1 Secrets Engine. -func (c *KVv1Client) GetSecret(ctx context.Context, name string) ([]byte, error) { - if b, hit := c.secretCache.Get(name, secretly.DefaultVersion); hit { - return b, nil - } - - secret, err := c.client.Get(ctx, name) - if err != nil { - return nil, err - } - - b, err := json.Marshal(secret.Data) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, secretly.DefaultVersion, b) - return b, nil -} - -// GetSecretWithVersion behaves the same as GetSecret -// but has a side effect of returning [ErrSpecificVersionPassedToKVv1] -// when a non default secret version is passed. -func (c *KVv1Client) GetSecretWithVersion(ctx context.Context, name, version string) ([]byte, error) { - if version != secretly.DefaultVersion { - return nil, ErrSpecificVersionPassedToKVv1 - } - return c.GetSecret(ctx, name) -} diff --git a/vault/kv_v1_test.go b/vault/kv_v1_test.go deleted file mode 100644 index 1c51e1a..0000000 --- a/vault/kv_v1_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package vault - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "reflect" - "strconv" - "testing" - - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" -) - -const secretKeyFormat = "%s_%d" - -var ( - errSecretNotFound = errors.New("secret not found") - errSecretAccessedMoreThanOnce = errors.New("secret accessed more than once") - - testSecretContent = []byte(`{"field1":"key1","field2":"key2"}`) -) - -type secretInfo struct { - name string - version string -} - -type stubKVv1Client struct { - secrets map[string][]byte - - accessed bool - failIfAccessedMoreThanOnce bool -} - -func newStubKVv1ClientWithSecrets() *stubKVv1Client { - c := &stubKVv1Client{ - secrets: make(map[string][]byte), - } - - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", 0)] = testSecretContent - - return c -} - -func (c *stubKVv1Client) Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - return c.GetVersion(ctx, secretPath, 0) -} - -func (c *stubKVv1Client) GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - if c.failIfAccessedMoreThanOnce && c.accessed { - return nil, errSecretAccessedMoreThanOnce - } - c.accessed = true - - if strconv.Itoa(version) != secretly.DefaultVersion { - return nil, ErrSpecificVersionPassedToKVv1 - } - - // At this point, version == int(secretly.DefaultVersion) - key := fmt.Sprintf(secretKeyFormat, secretPath, version) - - if b, ok := c.secrets[key]; ok { - // In vault, the most basic secret content is a map. - var secret map[string]interface{} - err := json.Unmarshal(b, &secret) - if err != nil { - return nil, err - } - - resp := &vault.KVSecret{ - Data: secret, - } - return resp, nil - } - return nil, errSecretNotFound -} - -func TestKVv1GetSecretVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfo secretInfo - want []byte - wantErr error - }{ - { - name: "Success With Default Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Latest Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Non-Default Secret Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "1", - }, - want: nil, - wantErr: ErrSpecificVersionPassedToKVv1, - }, - { - name: "Secret Does Not Exist", - secretInfo: secretInfo{ - name: "fake-secret-that-does-not-exist", - version: "0", - }, - want: nil, - wantErr: errSecretNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubKVv1ClientWithSecrets() - c := KVv1Client{client: smc, secretCache: secretly.NewSecretCache()} - - got, err := c.GetSecretWithVersion(context.Background(), tt.secretInfo.name, tt.secretInfo.version) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Incorrect secret content. Want %v, got %v", tt.want, got) - } - }) - } -} - -func TestKVv1GetSecretVersionCaching(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfos []secretInfo - wantErr error - }{ - { - name: "Cache Hit", - secretInfos: []secretInfo{ - { - name: "fake-secret", - version: "0", - }, - { - name: "fake-secret", - version: "0", - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubKVv1ClientWithSecrets() - smc.failIfAccessedMoreThanOnce = true - - c := KVv1Client{ - client: smc, - secretCache: secretly.NewSecretCache(), - } - - var err error - for _, secretInfo := range tt.secretInfos { - _, err = c.GetSecretWithVersion(context.Background(), secretInfo.name, secretInfo.version) - } - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - }) - } -} diff --git a/vault/kv_v2.go b/vault/kv_v2.go deleted file mode 100644 index 429cff7..0000000 --- a/vault/kv_v2.go +++ /dev/null @@ -1,134 +0,0 @@ -package vault - -import ( - "context" - "encoding/json" - "strconv" - - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" -) - -// vaultKVv2 describes required Vault KVv2 Secrets Engine methods -type vaultKVv2 interface { - Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) - GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) -} - -// KVv2Client is the Vault KVv2 Secrets Engine wrapper. -// Implements secretly.KVv2Client -type KVv2Client struct { - // client is the Vault KVv2 Secrets Engine. - client vaultKVv2 - - // secretCache is the cache that stores secrets => versions => content - // to reduce secret manager accesses. - secretCache secretly.SecretCache -} - -// Compile time check to assert that client implements secretly.Client -var _ secretly.Client = (*KVv2Client)(nil) - -// NewKVv2Client returns a Vault KVv2 Secrets Engine wrapper. -func NewKVv2Client(cfg Config) (*KVv2Client, error) { - client, err := vault.NewClient(cfg.VaultConfig) - if err != nil { - return nil, err - } - - if cfg.Token != "" { - client.SetToken(cfg.Token) - } - - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &KVv2Client{ - client: client.KVv2(cfg.MountPath), - secretCache: sc, - } - return c, nil -} - -// WrapKVv2 wraps the Vault KVv2 Secrets Engine client. -func WrapKVv2(client *vault.KVv2, cfg Config) *KVv2Client { - var sc secretly.SecretCache - if cfg.SecretlyConfig.DisableCaching { - sc = secretly.NewNoOpSecretCache() - } else { - sc = secretly.NewSecretCache() - } - - c := &KVv2Client{ - client: client, - secretCache: sc, - } - return c -} - -// Process resolves the provided specification -// using Vault KVv2 Secrets Engine. -// ProcessOptions can be provided -// to add additional processing for the fields, -// like reading version info from the env or a file. -// -// (*Client).Process is a convenience -// for calling secretly.Process with the Client. -func (c *KVv2Client) Process(spec any, opts ...secretly.ProcessOption) error { - return secretly.Process(c, spec, opts...) -} - -// GetSecret retrieves the latest secret for name -// from Vault KVv2 Secrets Engine. -func (c *KVv2Client) GetSecret(ctx context.Context, name string) ([]byte, error) { - if b, hit := c.secretCache.Get(name, secretly.DefaultVersion); hit { - return b, nil - } - - secret, err := c.client.Get(ctx, name) - if err != nil { - return nil, err - } - - b, err := json.Marshal(secret.Data) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, secretly.DefaultVersion, b) - return b, nil -} - -// GetSecretWithVersion retrieves the specific secret version for name -// from Vault KVv2 Secrets Engine. -func (c *KVv2Client) GetSecretWithVersion(ctx context.Context, name, version string) ([]byte, error) { - if version == secretly.DefaultVersion { - return c.GetSecret(ctx, name) - } - - if b, hit := c.secretCache.Get(name, version); hit { - return b, nil - } - - v, err := strconv.Atoi(version) - if err != nil { - return nil, err - } - - secret, err := c.client.GetVersion(ctx, name, v) - if err != nil { - return nil, err - } - - b, err := json.Marshal(secret.Data) - if err != nil { - return nil, err - } - - c.secretCache.Add(name, version, b) - return b, nil -} diff --git a/vault/kv_v2_test.go b/vault/kv_v2_test.go deleted file mode 100644 index 1da2f42..0000000 --- a/vault/kv_v2_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package vault - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "reflect" - "testing" - - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" -) - -type stubKVv2Client struct { - secrets map[string][]byte - - accessed bool - failIfAccessedMoreThanOnce bool -} - -func newStubKVv2ClientWithSecrets() *stubKVv2Client { - c := &stubKVv2Client{ - secrets: make(map[string][]byte), - } - - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", 0)] = testSecretContent - c.secrets[fmt.Sprintf(secretKeyFormat, "fake-secret", 1)] = testSecretContent - - return c -} - -func (c *stubKVv2Client) Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - return c.GetVersion(ctx, secretPath, 0) -} - -func (c *stubKVv2Client) GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - if c.failIfAccessedMoreThanOnce && c.accessed { - return nil, errSecretAccessedMoreThanOnce - } - c.accessed = true - - key := fmt.Sprintf(secretKeyFormat, secretPath, version) - - if b, ok := c.secrets[key]; ok { - // In vault, the most basic secret content is a map. - var secret map[string]interface{} - err := json.Unmarshal(b, &secret) - if err != nil { - return nil, err - } - - resp := &vault.KVSecret{ - Data: secret, - } - return resp, nil - } - return nil, errSecretNotFound -} - -func TestKVv2GetSecretVersion(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secretInfo secretInfo - want []byte - wantErr error - }{ - { - name: "Success With Default Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Numbered Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "1", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Success With Latest Version", - secretInfo: secretInfo{ - name: "fake-secret", - version: "0", - }, - want: testSecretContent, - wantErr: nil, - }, - { - name: "Secret Does Not Exist", - secretInfo: secretInfo{ - name: "fake-secret-that-does-not-exist", - version: "0", - }, - want: nil, - wantErr: errSecretNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt := tt - t.Parallel() - - smc := newStubKVv2ClientWithSecrets() - c := KVv2Client{client: smc, secretCache: secretly.NewSecretCache()} - - got, err := c.GetSecretWithVersion(context.Background(), tt.secretInfo.name, tt.secretInfo.version) - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Incorrect secret content. Want %v, got %v", tt.want, got) - } - }) - } -} - -func TestKVv2GetSecretVersionCaching(t *testing.T) { - tests := []struct { - name string - secretInfos []secretInfo - wantErr error - }{ - { - name: "Cache Hit", - secretInfos: []secretInfo{ - { - name: "fake-secret", - version: "0", - }, - { - name: "fake-secret", - version: "0", - }, - }, - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - smc := newStubKVv2ClientWithSecrets() - smc.failIfAccessedMoreThanOnce = true - - c := KVv2Client{ - client: smc, - secretCache: secretly.NewSecretCache(), - } - - var err error - for _, secretInfo := range tt.secretInfos { - _, err = c.GetSecretWithVersion(context.Background(), secretInfo.name, secretInfo.version) - } - - if err != tt.wantErr { - if !errors.Is(err, tt.wantErr) { - t.Errorf("Incorrect error. Want %v, got %v", tt.wantErr, err) - } - } - }) - } -} diff --git a/vault/vault.go b/vault/vault.go deleted file mode 100644 index 8e501f0..0000000 --- a/vault/vault.go +++ /dev/null @@ -1,23 +0,0 @@ -package vault - -import ( - vault "github.com/hashicorp/vault/api" - "github.com/jack-mcveigh/secretly" -) - -// Config provides both Vault KV V1 and secretly wrapper configurations. -type Config struct { - // Token is the Vault Auth Token. - Token string - - // MountPath is the location where the target KV secrets engine resides in Vault. - MountPath string - - // VaultConfig is the config for the Vault client. - // If the configuration is nil, - // Vault will use configuration from DefaultConfig(), - // which is the recommended starting configuration. - VaultConfig *vault.Config - - SecretlyConfig secretly.Config -}