From c467ce9a1b0716db2e88459ce20cb57caf8e1978 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Fri, 21 Nov 2025 07:31:20 +0000 Subject: [PATCH 1/7] BREAKING: add a parser (#54) * fix: intial regex only implementation we can do better * fix: parser implemented * fix: remove unused docs: update VAULT docs * fix: correct store pkg and tests * fix: make it right now let's refactor a bit * fix: add more notes in buildConfigManagerTokenFromBlocks * fix: update tests prep a move to v3 * !BREAKING CHANGE: update to package to v3 +semver: BREAKING +semver: MAJOR * fix: update go version * +semver: breaking fix: add tests +semver: breaking breaking: update the interface of the configmanager API * fix: clean up * fix: change defaults in CLI for token separator * fix: set version remove UNKNOWN from lexer * fix: tests * fix: linter * fix: additional tests * fix: add more implicit tests to lexer * fix: ineffassign linter errors * fix: add some docs --- .github/workflows/build.yml | 10 +- .github/workflows/release.yml | 12 +- Dockerfile | 2 +- README.md | 10 +- cmd/configmanager/configmanager.go | 14 +- cmd/configmanager/configmanager_test.go | 4 +- cmd/configmanager/fromfileinput.go | 2 +- cmd/main.go | 4 +- configmanager.go | 155 ++----- configmanager_test.go | 339 +++------------ docs/examples.md | 8 +- docs/v3-updates-migrations.md | 49 +++ eirctl.yaml | 13 +- examples/examples.go | 86 ++-- generator/generator.go | 258 +++++++++++ generator/generator_test.go | 370 ++++++++++++++++ generator/generatorvars.go | 94 ++++ go.mod | 2 +- internal/cmdutils/cmdutils.go | 15 +- internal/cmdutils/cmdutils_test.go | 34 +- internal/cmdutils/postprocessor.go | 16 +- internal/cmdutils/postprocessor_test.go | 18 +- internal/config/config.go | 167 +++----- internal/config/config_test.go | 124 ++++-- internal/config/token.go | 80 ++++ internal/lexer/lexer.go | 241 +++++++++++ internal/lexer/lexer_test.go | 99 +++++ internal/log/log_test.go | 4 +- internal/parser/doc.go | 5 + internal/parser/parser.go | 266 ++++++++++++ internal/parser/parser_test.go | 304 +++++++++++++ internal/store/azappconf.go | 16 +- internal/store/azappconf_test.go | 107 +++-- internal/store/azhelpers.go | 15 +- internal/store/azkeyvault.go | 16 +- internal/store/azkeyvault_test.go | 196 +++++---- internal/store/aztablestorage.go | 16 +- internal/store/aztablestorage_test.go | 280 +++++++----- internal/store/gcpsecrets.go | 10 +- internal/store/gcpsecrets_test.go | 109 +++-- internal/store/hashivault.go | 32 +- internal/store/hashivault_test.go | 344 ++++++++++----- internal/store/paramstore.go | 10 +- internal/store/paramstore_test.go | 146 ++++--- internal/store/secretsmanager.go | 10 +- internal/store/secretsmanager_test.go | 157 ++++--- internal/store/store.go | 6 +- internal/strategy/strategy.go | 131 +++--- internal/strategy/strategy_test.go | 108 +++-- pkg/.gitkeep | 0 pkg/generator/generator.go | 266 ------------ pkg/generator/generator_test.go | 548 ------------------------ 52 files changed, 3138 insertions(+), 2190 deletions(-) create mode 100644 docs/v3-updates-migrations.md create mode 100644 generator/generator.go create mode 100644 generator/generator_test.go create mode 100644 generator/generatorvars.go create mode 100644 internal/config/token.go create mode 100644 internal/lexer/lexer.go create mode 100644 internal/lexer/lexer_test.go create mode 100644 internal/parser/doc.go create mode 100644 internal/parser/parser.go create mode 100644 internal/parser/parser_test.go delete mode 100644 pkg/.gitkeep delete mode 100644 pkg/generator/generator.go delete mode 100644 pkg/generator/generator_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebccfcd..e0aae4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,9 +14,10 @@ permissions: jobs: set-version: + name: Set Version runs-on: ubuntu-latest container: - image: mcr.microsoft.com/dotnet/sdk:6.0 + image: mcr.microsoft.com/dotnet/sdk:10.0 outputs: semVer: ${{ steps.gitversion.outputs.semVer }} steps: @@ -30,11 +31,11 @@ jobs: git config user.email ${{ github.actor }}-ci@gha.org git config user.name ${{ github.actor }} - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v3.0.0 + uses: gittools/actions/gitversion/setup@v4.1.0 with: - versionSpec: "5.x" + versionSpec: "6.x" - name: Set SemVer Version - uses: gittools/actions/gitversion/execute@v3.0.0 + uses: gittools/actions/gitversion/execute@v4.1.0 id: gitversion - name: echo VERSIONS @@ -44,6 +45,7 @@ jobs: test: runs-on: ubuntu-latest + name: Run Tests needs: set-version env: SEMVER: ${{ needs.set-version.outputs.semVer }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ddc903..405c112 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,10 +14,11 @@ permissions: jobs: set-version: + name: Set Version runs-on: ubuntu-latest if: ${{ github.event.workflow_run.head_branch == 'master' && github.event.workflow_run.conclusion == 'success' }} container: - image: mcr.microsoft.com/dotnet/sdk:6.0 + image: mcr.microsoft.com/dotnet/sdk:10.0 outputs: semVer: ${{ steps.gitversion.outputs.semVer }} steps: @@ -31,14 +32,15 @@ jobs: git config user.email ${{ github.actor }}-ci@gha.org git config user.name ${{ github.actor }} - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v3.0.0 + uses: gittools/actions/gitversion/setup@v4.1.0 with: - versionSpec: '5.x' + versionSpec: '6.x' - name: Set SemVer Version - uses: gittools/actions/gitversion/execute@v3.0.0 + uses: gittools/actions/gitversion/execute@v4.1.0 id: gitversion release: + name: Release runs-on: ubuntu-latest needs: set-version env: @@ -66,7 +68,7 @@ jobs: generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} files: ./dist/* - prerelease: false + prerelease: true - name: release library run: | diff --git a/Dockerfile b/Dockerfile index 8dd4e1a..5871513 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /app COPY ./ /app RUN CGO_ENABLED=0 go build -mod=readonly -buildvcs=false \ - -ldflags="-s -w -X \"github.com/DevLabFoundry/configmanager/v2/cmd/configmanager.Version=${Version}\" -X \"github.com/DevLabFoundry/configmanager/v2/cmd/configmanager.Revision=${Revision}\" -extldflags -static" \ + -ldflags="-s -w -X \"github.com/DevLabFoundry/configmanager/v3/cmd/configmanager.Version=${Version}\" -X \"github.com/DevLabFoundry/configmanager/v3/cmd/configmanager.Revision=${Revision}\" -extldflags -static" \ -o bin/configmanager cmd/main.go FROM docker.io/alpine:3 diff --git a/README.md b/README.md index 635e376..5b73d7a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Where `configVar` can be either a parseable string `'som3#!S$CRet'` or a number - Kubernetes - Avoid storing overly large configmaps and especially using secrets objects to store actual secrets e.g. DB passwords, 3rd party API creds, etc... By only storing a config file or a script containing only the tokens e.g. `AWSSECRETS#/$ENV/service/db-config` it can be git committed without writing numerous shell scripts, only storing either some interpolation vars like `$ENV` in a configmap or the entire configmanager token for smaller use cases. + Avoid storing overly large configmaps and especially using secrets objects to store actual secrets e.g. DB passwords, 3rd party API creds, etc... By only storing a config file or a script containing only the tokens e.g. `AWSSECRETS:///$ENV/service/db-config` it can be git committed without writing numerous shell scripts, only storing either some interpolation vars like `$ENV` in a configmap or the entire configmanager token for smaller use cases. - VMs @@ -86,7 +86,7 @@ The token is made up of the following parts: _An example token would look like this_ -#### `AWSSECRETS#/path/to/my/key|lookup.Inside.Object[meta=data]` +#### `AWSSECRETS:///path/to/my/key|lookup.Inside.Object[meta=data]` ### Implementation indicator @@ -156,7 +156,9 @@ See [examples of working with files](docs/examples.md#working-with-files) for mo The `[meta=data]` from the [example token](#awssecretspathtomykeylookupinsideobjectmetadata) - is the optional metadata about the target in the backing provider -IT must have this format `[key=value]` - IT IS OPTIONAL + +> IT must have this format `[key=value]` - IT IS OPTIONAL +> IT must be specified last - either after a path lookup or if there is no key look up path specified then after the full path The `key` and `value` would be provider specific. Meaning that different providers support different config, these values _CAN_ be safely omitted configmanager would just use the defaults where applicable or not specify the additional @@ -220,7 +222,7 @@ All the usual token rules apply e.g. of `keySeparator` For HashicorpVault the first part of the token needs to be the name of the mountpath. In Dev Vaults this is `"secret"`, e.g.: `VAULT://secret___demo/configmanager|test` -or if the secrets are at another location: `VAULT://another/mount/path__config/app1/db` +or if the secrets are at another location: `VAULT://another/mount/path___config/app1/db` The hardcoded separator cannot be modified and you must separate your `mountPath` with `___` (3x `_`) followed by the key to the secret. diff --git a/cmd/configmanager/configmanager.go b/cmd/configmanager/configmanager.go index 01af646..1eed6cb 100644 --- a/cmd/configmanager/configmanager.go +++ b/cmd/configmanager/configmanager.go @@ -5,11 +5,11 @@ import ( "fmt" "io" - "github.com/DevLabFoundry/configmanager/v2" - "github.com/DevLabFoundry/configmanager/v2/internal/cmdutils" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/cmdutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/spf13/cobra" ) @@ -46,8 +46,8 @@ func NewRootCmd(logger log.ILogger) *Root { //channelOut, channelErr io.Writer } rc.Cmd.PersistentFlags().BoolVarP(&rc.rootFlags.verbose, "verbose", "v", false, "Verbosity level") - rc.Cmd.PersistentFlags().StringVarP(&rc.rootFlags.tokenSeparator, "token-separator", "s", "#", "Separator to use to mark concrete store and the key within it") - rc.Cmd.PersistentFlags().StringVarP(&rc.rootFlags.keySeparator, "key-separator", "k", "|", "Separator to use to mark a key look up in a map. e.g. AWSSECRETS#/token/map|key1") + rc.Cmd.PersistentFlags().StringVarP(&rc.rootFlags.tokenSeparator, "token-separator", "s", "://", "Separator to use to mark concrete store and the key within it") + rc.Cmd.PersistentFlags().StringVarP(&rc.rootFlags.keySeparator, "key-separator", "k", "|", "Separator to use to mark a key look up in a map. e.g. AWSSECRETS:///token/map|key1") rc.Cmd.PersistentFlags().BoolVarP(&rc.rootFlags.enableEnvSubst, "enable-envsubst", "e", false, "Enable envsubst on input. This will fail on any unset or empty variables") addSubCmds(rc) return rc diff --git a/cmd/configmanager/configmanager_test.go b/cmd/configmanager/configmanager_test.go index fce284a..5089ad3 100644 --- a/cmd/configmanager/configmanager_test.go +++ b/cmd/configmanager/configmanager_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - cmd "github.com/DevLabFoundry/configmanager/v2/cmd/configmanager" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + cmd "github.com/DevLabFoundry/configmanager/v3/cmd/configmanager" + "github.com/DevLabFoundry/configmanager/v3/internal/log" ) type cmdTestInput struct { diff --git a/cmd/configmanager/fromfileinput.go b/cmd/configmanager/fromfileinput.go index 4b8e915..f531033 100644 --- a/cmd/configmanager/fromfileinput.go +++ b/cmd/configmanager/fromfileinput.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" - "github.com/DevLabFoundry/configmanager/v2/internal/cmdutils" + "github.com/DevLabFoundry/configmanager/v3/internal/cmdutils" "github.com/spf13/cobra" ) diff --git a/cmd/main.go b/cmd/main.go index ee4999a..4f10c03 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,8 +4,8 @@ import ( "context" "os" - cfgmgr "github.com/DevLabFoundry/configmanager/v2/cmd/configmanager" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + cfgmgr "github.com/DevLabFoundry/configmanager/v3/cmd/configmanager" + "github.com/DevLabFoundry/configmanager/v3/internal/log" ) func main() { diff --git a/configmanager.go b/configmanager.go index a422dfb..f3f6134 100644 --- a/configmanager.go +++ b/configmanager.go @@ -2,50 +2,60 @@ package configmanager import ( "context" - "encoding/json" "errors" "fmt" - "regexp" + "io" + "slices" "strings" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/a8m/envsubst" - "gopkg.in/yaml.v3" ) const ( - TERMINATING_CHAR string = `[^\'\"\s\n\\\,]` + TERMINATING_CHAR string = `[^\'\"\s\n\\\,]` // :\@\?\/ ) // generateAPI type generateAPI interface { - Generate(tokens []string) (generator.ParsedMap, error) + Generate(tokens []string) (generator.ReplacedToken, error) } type ConfigManager struct { Config *config.GenVarsConfig generator generateAPI + logger log.ILogger } // New returns an initialised instance of ConfigManager // Uses default config for: // -// ``` -// outputPath = "" -// keySeparator = "|" -// tokenSeparator = "://" -// ``` +// outputPath = "" +// keySeparator = "|" +// tokenSeparator = "://" +// +// # Calling cm.Config.WithXXX() will overwrite the generator config +// +// Default logger will log to io.Discard +// Attach your own if you need via // -// Calling cm.Config.WithXXX() will overwrite the generator config +// WithLogger(l log.ILogger) *ConfigManager func New(ctx context.Context) *ConfigManager { cm := &ConfigManager{} cm.Config = config.NewConfig() cm.generator = generator.NewGenerator(ctx).WithConfig(cm.Config) + cm.logger = log.New(io.Discard) return cm } +func (c *ConfigManager) WithLogger(l log.ILogger) *ConfigManager { + c.logger = l + return c +} + // GeneratorConfig // Returns the gettable generator config func (c *ConfigManager) GeneratorConfig() *config.GenVarsConfig { @@ -60,25 +70,15 @@ func (c *ConfigManager) WithGenerator(generator generateAPI) *ConfigManager { // Retrieve gets a rawMap from a set implementation // will be empty if no matches found -func (c *ConfigManager) Retrieve(tokens []string) (generator.ParsedMap, error) { - return c.retrieve(tokens) -} - -func (c *ConfigManager) retrieve(tokens []string) (generator.ParsedMap, error) { +func (c *ConfigManager) Retrieve(tokens []string) (generator.ReplacedToken, error) { return c.generator.Generate(tokens) } var ErrEnvSubst = errors.New("envsubst enabled and errored on") -// RetrieveWithInputReplaced parses given input against all possible token strings -// using regex to grab a list of found tokens in the given string and returns the replaced string -func (c *ConfigManager) RetrieveWithInputReplaced(input string) (string, error) { +// RetrieveReplacedString parses given input against all possible token strings +func (c *ConfigManager) RetrieveReplacedString(input string) (string, error) { // replaces all env vars using strict mode of no unset and no empty - // - // NOTE: this happens before the FindTokens is called - // currently it uses a regex, and envsubst uses a more robust lexer => parser mechanism - // - // NOTE: configmanager needs an own lexer => parser to allow for easier modification extension in the future if c.GeneratorConfig().EnvSubstEnabled() { var err error input, err = envsubst.StringRestrictedNoDigit(input, true, true, false) @@ -86,7 +86,9 @@ func (c *ConfigManager) RetrieveWithInputReplaced(input string) (string, error) return "", fmt.Errorf("%w\n%v", ErrEnvSubst, err) } } - m, err := c.retrieve(FindTokens(input)) + + // calling the same Generate method with the input as single item in a slice + m, err := c.generator.Generate([]string{input}) if err != nil { return "", err @@ -95,19 +97,14 @@ func (c *ConfigManager) RetrieveWithInputReplaced(input string) (string, error) return replaceString(m, input), nil } -// FindTokens extracts all replaceable tokens -// from a given input string -func FindTokens(input string) []string { - tokens := []string{} - for k := range config.VarPrefix { - matches := regexp.MustCompile(regexp.QuoteMeta(string(k))+`.(`+TERMINATING_CHAR+`+)`).FindAllString(input, -1) - tokens = append(tokens, matches...) - } - return tokens +// RetrieveReplacedBytes is functionally identical RetrieveReplacedString +func (c *ConfigManager) RetrieveReplacedBytes(input []byte) ([]byte, error) { + r, err := c.RetrieveReplacedString(string(input)) + return []byte(r), err } // replaceString fills tokens in a provided input with their actual secret/config values -func replaceString(inputMap generator.ParsedMap, inputString string) string { +func replaceString(inputMap generator.ReplacedToken, inputString string) string { oldNew := []string(nil) // ordered values by index @@ -118,7 +115,7 @@ func replaceString(inputMap generator.ParsedMap, inputString string) string { return replacer.Replace(inputString) } -func orderedKeysList(inputMap generator.ParsedMap) []string { +func orderedKeysList(inputMap generator.ReplacedToken) []string { mkeys := inputMap.MapKeys() // order map by keys length so that when passed to the // replacer it will replace the longest first @@ -128,85 +125,3 @@ func orderedKeysList(inputMap generator.ParsedMap) []string { slices.Sort(mkeys) return mkeys } - -// RetrieveMarshalledJson -// -// It marshalls an input pointer value of a type with appropriate struct tags in JSON -// marshalls it into a string and runs the appropriate token replacement. -// and fills the same pointer value with the replaced fields. -// -// This is useful for when you have another tool or framework already passing you a known type. -// e.g. a CRD Spec in kubernetes - where you POSTed the json/yaml spec with tokens in it -// but now want to use them with tokens replaced for values in a stateless way. -// -// Enables you to store secrets in CRD Specs and other metadata your controller can use -func (cm *ConfigManager) RetrieveMarshalledJson(input any) error { - - // marshall type into a []byte - // with tokens in a string like object - rawBytes, err := json.Marshal(input) - if err != nil { - return err - } - // run the replacement of tokens for values - replacedString, err := cm.RetrieveWithInputReplaced(string(rawBytes)) - if err != nil { - return err - } - // replace the original pointer value with replaced tokens - if err := json.Unmarshal([]byte(replacedString), input); err != nil { - return err - } - return nil -} - -// RetrieveUnmarshalledFromJson -// It accepts an already marshalled byte slice and pointer to the value type. -// It fills the type with the replaced -func (c *ConfigManager) RetrieveUnmarshalledFromJson(input []byte, output any) error { - replaced, err := c.RetrieveWithInputReplaced(string(input)) - if err != nil { - return err - } - if err := json.Unmarshal([]byte(replaced), output); err != nil { - return err - } - return nil -} - -// RetrieveMarshalledYaml -// -// Same as RetrieveMarshalledJson -func (cm *ConfigManager) RetrieveMarshalledYaml(input any) error { - - // marshall type into a []byte - // with tokens in a string like object - rawBytes, err := yaml.Marshal(input) - if err != nil { - return err - } - // run the replacement of tokens for values - replacedString, err := cm.RetrieveWithInputReplaced(string(rawBytes)) - if err != nil { - return err - } - // replace the original pointer value with replaced tokens - if err := yaml.Unmarshal([]byte(replacedString), input); err != nil { - return err - } - return nil -} - -// RetrieveUnmarshalledFromYaml -// -// Same as RetrieveUnmarshalledFromJson -func (c *ConfigManager) RetrieveUnmarshalledFromYaml(input []byte, output any) error { - replaced, err := c.RetrieveWithInputReplaced(string(input)) - if err != nil { - return err - } - if err := yaml.Unmarshal([]byte(replaced), output); err != nil { - return err - } - return nil -} diff --git a/configmanager_test.go b/configmanager_test.go index 43e0ef8..1321232 100644 --- a/configmanager_test.go +++ b/configmanager_test.go @@ -2,28 +2,29 @@ package configmanager_test import ( "context" + "encoding/json" "fmt" "os" "reflect" - "sort" "testing" - "github.com/DevLabFoundry/configmanager/v2" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/go-test/deep" + "gopkg.in/yaml.v3" ) type mockGenerator struct { - generate func(tokens []string) (generator.ParsedMap, error) + generate func(tokens []string) (generator.ReplacedToken, error) } -func (m *mockGenerator) Generate(tokens []string) (generator.ParsedMap, error) { +func (m *mockGenerator) Generate(tokens []string) (generator.ReplacedToken, error) { if m.generate != nil { return m.generate(tokens) } - pm := generator.ParsedMap{} + pm := generator.ReplacedToken{} pm["FOO#/test"] = "val1" pm["ANOTHER://bar/quz"] = "fux" pm["ZODTHER://bar/quz"] = "xuf" @@ -63,15 +64,15 @@ func Test_Retrieve_from_token_list(t *testing.T) { } } -func Test_retrieveWithInputReplaced(t *testing.T) { +func Test_retrieveReplacedBytes(t *testing.T) { tests := map[string]struct { name string - input string + input []byte genvar *mockGenerator expect string }{ "strYaml": { - input: ` + input: []byte(` space: preserved indents: preserved arr: [ "FOO#/test" ] @@ -79,7 +80,7 @@ space: preserved arr: - "FOO#/test" - ANOTHER://bar/quz -`, +`), genvar: &mockGenerator{}, expect: ` space: preserved @@ -92,11 +93,11 @@ space: preserved `, }, "strToml": { - input: ` + input: []byte(` // TOML [[somestuff]] key = "FOO#/test" -`, +`), genvar: &mockGenerator{}, expect: ` // TOML @@ -105,14 +106,14 @@ key = "val1" `, }, "strTomlWithoutQuotes": { - input: ` + input: []byte(` // TOML [[somestuff]] key = FOO#/test,FOO#/test-FOO#/test key2 = FOO#/test key3 = FOO#/test key4 = FOO#/test -`, +`), genvar: &mockGenerator{}, expect: ` // TOML @@ -124,7 +125,7 @@ key4 = val1 `, }, "strTomlWithoutMultiline": { - input: ` + input: []byte(` export FOO='FOO#/test' export FOO1=FOO#/test export FOO2="FOO#/test" @@ -134,7 +135,7 @@ export FOO4=FOO#/test [[section]] foo23 = FOO#/test -`, +`), genvar: &mockGenerator{}, expect: ` export FOO='val1' @@ -149,7 +150,7 @@ foo23 = val1 `, }, "escaped input": { - input: `"{\"patchPayloadTemplate\":\"{\\\"password\\\":\\\"FOO#/test\\\",\\\"passwordConfirm\\\":\\\"FOO#/test\\\"}\\n\"}"`, + input: []byte(`"{\"patchPayloadTemplate\":\"{\\\"password\\\":\\\"FOO#/test\\\",\\\"passwordConfirm\\\":\\\"FOO#/test\\\"}\\n\"}"`), genvar: &mockGenerator{}, expect: `"{\"patchPayloadTemplate\":\"{\\\"password\\\":\\\"val1\\\",\\\"passwordConfirm\\\":\\\"val1\\\"}\\n\"}"`, }, @@ -159,11 +160,11 @@ foo23 = val1 t.Run(tt.name, func(t *testing.T) { cm := configmanager.New(context.TODO()) cm.WithGenerator(tt.genvar) - got, err := cm.RetrieveWithInputReplaced(tt.input) + got, err := cm.RetrieveReplacedBytes([]byte(tt.input)) if err != nil { t.Errorf("failed with %v", err) } - if got != tt.expect { + if string(got) != string(tt.expect) { t.Errorf(testutils.TestPhrase, got, tt.expect) } }) @@ -171,7 +172,6 @@ foo23 = val1 } func Test_replaceString_with_envsubst(t *testing.T) { - t.Parallel() ttests := map[string]struct { expect string setup func() func() @@ -198,7 +198,7 @@ func Test_replaceString_with_envsubst(t *testing.T) { cm := configmanager.New(context.TODO()) cm.WithGenerator(tt.genvar) cm.Config.WithEnvSubst(true) - got, err := cm.RetrieveWithInputReplaced(tt.input) + got, err := cm.RetrieveReplacedString(tt.input) if err != nil { t.Errorf("failed with %v", err) } @@ -264,8 +264,8 @@ var marshallTests = map[string]struct { }, generator: func(t *testing.T) *mockGenerator { m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) + m.generate = func(tokens []string) (generator.ReplacedToken, error) { + pm := make(generator.ReplacedToken) pm[testTokenAWS] = "baz" return pm, nil } @@ -284,8 +284,8 @@ var marshallTests = map[string]struct { }, generator: func(t *testing.T) *mockGenerator { m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) + m.generate = func(tokens []string) (generator.ReplacedToken, error) { + pm := make(generator.ReplacedToken) pm[testTokenAWS] = "baz" return pm, nil } @@ -294,30 +294,44 @@ var marshallTests = map[string]struct { }, } -func Test_RetrieveMarshalledJson(t *testing.T) { +func Test_RetrieveBytes_MarshalledJson(t *testing.T) { for name, tt := range marshallTests { t.Run(name, func(t *testing.T) { c := configmanager.New(context.TODO()) c.Config.WithTokenSeparator("://") c.WithGenerator(tt.generator(t)) - input := &tt.testType - err := c.RetrieveMarshalledJson(input) - MarhsalledHelper(t, err, input, &tt.expect) + b, err := json.Marshal(tt.testType) + if err != nil { + t.Fatal(err) + } + got, err := c.RetrieveReplacedBytes(b) + output := testNestedStruct{} + json.Unmarshal(got, &output) + MarhsalledHelper(t, err, &output, &tt.expect) }) } } -func Test_YamlRetrieveMarshalled(t *testing.T) { +// func Example_RetrieveReplacedBytesMarshalledJSON(t *testing.T) { +// return +// } + +func Test_RetrieveBytes_MarshalledYaml(t *testing.T) { for name, tt := range marshallTests { t.Run(name, func(t *testing.T) { c := configmanager.New(context.TODO()) c.Config.WithTokenSeparator("://") c.WithGenerator(tt.generator(t)) - input := &tt.testType - err := c.RetrieveMarshalledYaml(input) - MarhsalledHelper(t, err, input, &tt.expect) + b, err := yaml.Marshal(tt.testType) + if err != nil { + t.Fatal(err) + } + got, err := c.RetrieveReplacedBytes(b) + output := testNestedStruct{} + yaml.Unmarshal(got, &output) + MarhsalledHelper(t, err, &output, &tt.expect) }) } } @@ -332,261 +346,6 @@ func MarhsalledHelper(t *testing.T, err error, input, expectOut any) { } } -func Test_YamlRetrieveUnmarshalled(t *testing.T) { - ttests := map[string]struct { - input []byte - expect testNestedStruct - generator func(t *testing.T) *mockGenerator - }{ - "happy path complex struct complete": { - input: []byte(`foo: AWSSECRETS:///bar/foo -bar: quz -lol: - bla: booo - another: - number: 1235 - float: 123.09`), - expect: testNestedStruct{ - Foo: "baz", - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, - generator: func(t *testing.T) *mockGenerator { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) - pm[testTokenAWS] = "baz" - return pm, nil - } - return m - }, - }, - "complex struct - missing fields": { - input: []byte(`foo: AWSSECRETS:///bar/foo -bar: quz`), - expect: testNestedStruct{ - Foo: "baz", - Bar: "quz", - Lol: testLol{}, - }, - generator: func(t *testing.T) *mockGenerator { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) - pm[testTokenAWS] = "baz" - return pm, nil - } - return m - }, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(tt.generator(t)) - output := &testNestedStruct{} - err := c.RetrieveUnmarshalledFromYaml(tt.input, output) - MarhsalledHelper(t, err, output, &tt.expect) - }) - } -} - -func Test_JsonRetrieveUnmarshalled(t *testing.T) { - tests := map[string]struct { - input []byte - expect testNestedStruct - generator func(t *testing.T) *mockGenerator - }{ - "happy path complex struct complete": { - input: []byte(`{"foo":"AWSSECRETS:///bar/foo","bar":"quz","lol":{"bla":"booo","another":{"number":1235,"float":123.09}}}`), - expect: testNestedStruct{ - Foo: "baz", - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, - generator: func(t *testing.T) *mockGenerator { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) - pm[testTokenAWS] = "baz" - return pm, nil - } - return m - }, - }, - "complex struct - missing fields": { - input: []byte(`{"foo":"AWSSECRETS:///bar/foo","bar":"quz"}`), - expect: testNestedStruct{ - Foo: "baz", - Bar: "quz", - Lol: testLol{}, - }, - generator: func(t *testing.T) *mockGenerator { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - pm := make(generator.ParsedMap) - pm[testTokenAWS] = "baz" - return pm, nil - } - return m - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(tt.generator(t)) - output := &testNestedStruct{} - err := c.RetrieveUnmarshalledFromJson(tt.input, output) - MarhsalledHelper(t, err, output, &tt.expect) - }) - } -} - -func TestFindTokens(t *testing.T) { - ttests := map[string]struct { - input string - expect []string - }{ - "extract from text correctly": { - `Where does it come from? - Contrary to popular belief, - Lorem Ipsum is AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj <= in middle of sentencenot simply random text. - It has roots in a piece of classical Latin literature from 45 - BC, making it over 2000 years old. Richard McClintock, a Latin professor at - Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, c - onsectetur, from a Lorem Ipsum passage , at the end of line => AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj - and going through the cites of the word in c - lassical literature, discovered the undoubtable source. Lorem Ipsum comes from secti - ons in singles =>'AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj'1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) - in doubles => "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj" - by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular - during the :=> embedded in text RenaissanceAWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[] embedded in text <=: - The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`, - []string{ - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[]"}, - }, - "unknown implementation not picked up": { - `foo: AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj - bar: AWSPARAMSTR#bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version:123] - unknown: GCPPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj`, - []string{ - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR#bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version:123]"}, - }, - "all implementations": { - `param: AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj - secretsmgr: AWSSECRETS#bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version:123] - gcp: GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj - vault: VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[] - som othere strufsd - azkv: AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj`, - []string{ - "GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "AWSSECRETS#bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version:123]", - "AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", - "VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[]"}, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - got := configmanager.FindTokens(tt.input) - sort.Strings(got) - sort.Strings(tt.expect) - - if !reflect.DeepEqual(got, tt.expect) { - t.Errorf("input=(%q)\n\ngot: %v\n\nwant: %v", tt.input, got, tt.expect) - } - }) - } -} - -func Test_YamlRetrieveMarshalled_errored_in_generator(t *testing.T) { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - return nil, fmt.Errorf("failed to generate a parsedMap") - } - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(m) - input := &testNestedStruct{} - err := c.RetrieveMarshalledYaml(input) - if err != nil { - } else { - t.Errorf(testutils.TestPhrase, nil, "err") - } -} - -func Test_YamlRetrieveMarshalled_errored_in_marshal(t *testing.T) { - t.Skip() - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - return generator.ParsedMap{}, nil - } - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(m) - err := c.RetrieveMarshalledYaml(&struct { - A int - B map[string]int `yaml:",inline"` - }{1, map[string]int{"a": 2}}) - if err != nil { - } else { - t.Errorf(testutils.TestPhrase, nil, "err") - } -} - -func Test_JsonRetrieveMarshalled_errored_in_generator(t *testing.T) { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - return nil, fmt.Errorf("failed to generate a parsedMap") - } - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(m) - input := &testNestedStruct{} - err := c.RetrieveMarshalledJson(input) - if err != nil { - } else { - t.Errorf(testutils.TestPhrase, nil, "err") - } -} - -func Test_JsonRetrieveMarshalled_errored_in_marshal(t *testing.T) { - m := &mockGenerator{} - m.generate = func(tokens []string) (generator.ParsedMap, error) { - return generator.ParsedMap{}, nil - } - c := configmanager.New(context.TODO()) - c.Config.WithTokenSeparator("://") - c.WithGenerator(m) - // input := &testNestedStruct{} - err := c.RetrieveMarshalledJson(nil) - if err != nil { - } else { - t.Errorf(testutils.TestPhrase, nil, "err") - } -} - // config tests func Test_Generator_Config_(t *testing.T) { ttests := map[string]struct { diff --git a/docs/examples.md b/docs/examples.md index 73313c8..07634fa 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -84,8 +84,8 @@ import ( "context" "fmt" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" - "github.com/DevLabFoundry/configmanager/v2" + "github.com/DevLabFoundry/configmanager/v3/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3" ) func main() { @@ -128,8 +128,8 @@ import ( "log" "os" - "github.com/DevLabFoundry/configmanager/v2" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3" + "github.com/DevLabFoundry/configmanager/v3/pkg/generator" ) var ( diff --git a/docs/v3-updates-migrations.md b/docs/v3-updates-migrations.md new file mode 100644 index 0000000..3f05690 --- /dev/null +++ b/docs/v3-updates-migrations.md @@ -0,0 +1,49 @@ +# V3 Changes and updates + +As part of the V3 update we are aiming to streamline and improve the following high level areas. + +- Network call optimisation +- Backing store plugin architecture + +## Network Call optimisation + +There are many cases when an input string/file or array of `--token` can point to the same underlying token. + +e.g. +```yaml +db_user: AWSSECRETS:///app1/db|user +db_password: AWSSECRETS:///app1/db|password +db_port: AWSSECRETS:///app1/db|port +db_host: AWSSECRETS:///app1/db|host +``` + +Given the above input passed into the CLI i.e. `configmanager fromstr -i above-config.yaml` + +This would result in 4 network calls to the underlying service, in this case the AWS Secrets Manager. + +The V3 update would fan in these 4 tokens into a single network call and then fan out back to a full map with the individual values for each of the look up keys. + +> NB: any token using a metadata annotation on any token would guarantee a unique call to the underlying service + +e.g.: + +```yaml +db_user: AWSSECRETS:///app1/db|user +db_password: AWSSECRETS:///app1/db|password +db_port: AWSSECRETS:///app1/db|port +db_host: AWSSECRETS:///app1/db|host +db_host_2: AWSSECRETS:///app1/db|host[version=2] +``` + +Even though `AWSSECRETS:///app1/db|host[version=2]` and `AWSSECRETS:///app1/db|host` are technically the same AWS Secrets Manager item, specifying the version requires two separate network calls. + +## Backing Store plugin architecture + +The current implementation of the backing stores is defined entirely within the configmanager source code which becomes part of the final staticly linked binary. In order to avoid the bigger size binary and **more importantly** avoid security alerts for libraries that are nothing to do with a backing store provider which is not used! + +Most probably, and most commonly, one would only use single or a combination of providers within the same Cloud for example. + +### Plugin Architecture + +There are a few options to choose from - terraform style provider plugins using gRPC. + diff --git a/eirctl.yaml b/eirctl.yaml index fd5349c..f9e4f9c 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -12,17 +12,20 @@ contexts: name: mirror.gcr.io/bash:5.0.18-alpine3.22 pipelines: - gha:unit:test: + unit:test: - pipeline: test:unit env: ROOT_PKG_NAME: github.com/DevLabFoundry + + gha:unit:test: + - pipeline: unit:test - task: sonar:coverage:prep - depends_on: test:unit + depends_on: unit:test show_coverage: - - pipeline: test:unit + - pipeline: unit:test - task: show:coverage - depends_on: test:unit + depends_on: unit:test build:bin: - task: clean @@ -53,7 +56,7 @@ tasks: - | mkdir -p .deps unset GOTOOLCHAIN - ldflags="-s -w -X \"github.com/DevLabFoundry/configmanager/v2/cmd/configmanager.Version=${VERSION}\" -X \"github.com/DevLabFoundry/configmanager/v2/cmd/configmanager.Revision=${REVISION}\" -extldflags -static" + ldflags="-s -w -X \"github.com/DevLabFoundry/configmanager/v3/cmd/configmanager.Version=${VERSION}\" -X \"github.com/DevLabFoundry/configmanager/v3/cmd/configmanager.Revision=${REVISION}\" -extldflags -static" GOPATH=/eirctl/.deps GOOS=${BUILD_GOOS} GOARCH=${BUILD_GOARCH} CGO_ENABLED=0 go build -mod=readonly -buildvcs=false -ldflags="$ldflags" \ -o ./dist/configmanager-${BUILD_GOOS}-${BUILD_GOARCH}${BUILD_SUFFIX} ./cmd echo "---" diff --git a/examples/examples.go b/examples/examples.go index b15016a..379a84d 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/DevLabFoundry/configmanager/v2" + "github.com/DevLabFoundry/configmanager/v3" ) const DO_STUFF_WITH_VALS_HERE = "connstring:user@%v:host=%s/someschema..." @@ -46,7 +46,7 @@ spec: secret_val: AWSSECRETS#/customfoo/secret-val owner: test_10016@example.com ` - pm, err := cm.RetrieveWithInputReplaced(exampleK8sCrdMarshalled) + pm, err := cm.RetrieveReplacedString(exampleK8sCrdMarshalled) if err != nil { panic(err) @@ -69,7 +69,7 @@ func SpecConfigTokenReplace[T any](inputType T) (*T, error) { // use custom token separator cm.Config.WithTokenSeparator("://") - replaced, err := cm.RetrieveWithInputReplaced(string(rawBytes)) + replaced, err := cm.RetrieveReplacedBytes(rawBytes) if err != nil { return nil, err } @@ -79,57 +79,29 @@ func SpecConfigTokenReplace[T any](inputType T) (*T, error) { return outType, nil } -// Example -func exampleRetrieveYamlUnmarshalled() { - - type config struct { - DbHost string `yaml:"dbhost"` - Username string `yaml:"user"` - Password string `yaml:"pass"` - } - configMarshalled := ` -user: AWSPARAMSTR:///int-test/pocketbase/config|user -pass: AWSPARAMSTR:///int-test/pocketbase/config|pwd -dbhost: AWSPARAMSTR:///int-test/pocketbase/config|host -` - - appConf := &config{} - cm := configmanager.New(context.TODO()) - // use custom token separator inline with future releases - cm.Config.WithTokenSeparator("://") - err := cm.RetrieveUnmarshalledFromYaml([]byte(configMarshalled), appConf) - if err != nil { - panic(err) - } - fmt.Println(appConf.DbHost) - fmt.Println(appConf.Username) - fmt.Println(appConf.Password) -} - -// ### exampleRetrieveYamlMarshalled -func exampleRetrieveYamlMarshalled() { - type config struct { - DbHost string `yaml:"dbhost"` - Username string `yaml:"user"` - Password string `yaml:"pass"` - } - - appConf := &config{ - DbHost: "AWSPARAMSTR:///int-test/pocketbase/config|host", - Username: "AWSPARAMSTR:///int-test/pocketbase/config|user", - Password: "AWSPARAMSTR:///int-test/pocketbase/config|pwd", - } - - cm := configmanager.New(context.TODO()) - cm.Config.WithTokenSeparator("://") - err := cm.RetrieveMarshalledYaml(appConf) - if err != nil { - panic(err) - } - if appConf.DbHost == "AWSPARAMSTR:///int-test/pocketbase/config|host" { - panic(fmt.Errorf("value of DbHost should have been replaced with a value from token")) - } - fmt.Println(appConf.DbHost) - fmt.Println(appConf.Username) - fmt.Println(appConf.Password) -} +// // Example +// func exampleRetrieveYamlUnmarshalled() { + +// type config struct { +// DbHost string `yaml:"dbhost"` +// Username string `yaml:"user"` +// Password string `yaml:"pass"` +// } +// configMarshalled := ` +// user: AWSPARAMSTR:///int-test/pocketbase/config|user +// pass: AWSPARAMSTR:///int-test/pocketbase/config|pwd +// dbhost: AWSPARAMSTR:///int-test/pocketbase/config|host +// ` + +// appConf := &config{} +// cm := configmanager.New(context.TODO()) +// // use custom token separator inline with future releases +// cm.Config.WithTokenSeparator("://") +// err := cm.RetrieveUnmarshalledFromYaml([]byte(configMarshalled), appConf) +// if err != nil { +// panic(err) +// } +// fmt.Println(appConf.DbHost) +// fmt.Println(appConf.Username) +// fmt.Println(appConf.Password) +// } diff --git a/generator/generator.go b/generator/generator.go new file mode 100644 index 0000000..237c6a6 --- /dev/null +++ b/generator/generator.go @@ -0,0 +1,258 @@ +package generator + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/lexer" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/parser" + "github.com/DevLabFoundry/configmanager/v3/internal/strategy" +) + +// GenVars is the main struct holding the +// strategy patterns iface +// any initialised config if overridded with withers +// as well as the final outString and the initial rawMap +// which wil be passed in a loop into a goroutine to perform the +// relevant strategy network calls to the config store implementations +type GenVars struct { + Logger log.ILogger + strategy strategy.StrategyFuncMap + ctx context.Context + config config.GenVarsConfig +} + +type Opts func(*GenVars) + +// NewGenerator returns a new instance of Generator +// with a default strategy pattern wil be overwritten +// during the first run of a found tokens map +func NewGenerator(ctx context.Context, opts ...Opts) *GenVars { + // defaultStrategy := NewDefatultStrategy() + return newGenVars(ctx, opts...) +} + +func newGenVars(ctx context.Context, opts ...Opts) *GenVars { + conf := config.NewConfig() + g := &GenVars{ + Logger: log.New(io.Discard), + ctx: ctx, + // return using default config + config: *conf, + } + g.strategy = nil + + // now apply additional opts + for _, o := range opts { + o(g) + } + + return g +} + +// WithStrategyMap +// +// Adds addtional funcs for storageRetrieval used for testing only +func (c *GenVars) WithStrategyMap(sm strategy.StrategyFuncMap) *GenVars { + c.strategy = sm + return c +} + +// WithConfig uses custom config +func (c *GenVars) WithConfig(cfg *config.GenVarsConfig) *GenVars { + // backwards compatibility + if cfg != nil { + c.config = *cfg + } + return c +} + +// WithContext uses caller passed context +func (c *GenVars) WithContext(ctx context.Context) *GenVars { + c.ctx = ctx + return c +} + +// Config gets Config on the GenVars +func (c *GenVars) Config() *config.GenVarsConfig { + return &c.config +} + +// Generate generates a k/v map of the tokens with their corresponding secret/paramstore values +// the standard pattern of a token should follow a path like string +// +// Called only from a slice of tokens +func (c *GenVars) Generate(tokens []string) (ReplacedToken, error) { + + ntm, err := c.DiscoverTokens(strings.Join(tokens, "\n")) + if err != nil { + return nil, err + } + + // pass in default initialised retrieveStrategy + // input should be + rt, err := c.generate(ntm) + if err != nil { + return nil, err + } + return rt, nil +} + +var ErrTokenDiscovery = errors.New("failed to discover tokens") + +// DiscoverToken generates a k/v map of the tokens with their corresponding secret/paramstore values +// the standard pattern of a token should follow a path like string +// +// Called only from a slice of tokens +func (c *GenVars) DiscoverTokens(text string) (NormalizedTokenSafe, error) { + + rtm := NewRawTokenConfig() + + lexerSource := lexer.Source{FileName: text[0:min(len(text), 20)], FullPath: "", Input: text} + l := lexer.New(lexerSource, c.config) + p := parser.New(l, &c.config).WithLogger(log.New(os.Stderr)) + parsed, errs := p.Parse() + if len(errs) > 0 { + return NormalizedTokenSafe{}, fmt.Errorf("%w in input (%s) with errors: %q", ErrTokenDiscovery, text[0:min(len(text), 25)], errs) + } + for _, prsdToken := range parsed { + rtm.AddToken(prsdToken.ParsedToken.String(), &prsdToken.ParsedToken) + } + return c.NormalizeRawToken(rtm), nil +} + +// IsParsed will try to parse the return found string into +// map[string]string +// If found it will convert that to a map with all keys uppercased +// and any characters +func IsParsed(v any, trm ReplacedToken) bool { + str := fmt.Sprint(v) + err := json.Unmarshal([]byte(str), &trm) + return err == nil +} + +// generate initiates waitGroup to handle 1 or more normalized network calls concurrently to the underlying stores +// +// Captures the response/error in TokenResponse struct +// It then denormalizes the NormalizedTokenSafe back to a ReplacedToken map +// which stores the values for each token to be returned to the caller +func (c *GenVars) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { + if len(ntm.normalizedTokenMap) < 1 { + c.Logger.Debug("no replaceable tokens found in input") + return nil, nil + } + + wg := &sync.WaitGroup{} + + s := strategy.New(c.config, c.Logger, strategy.WithStrategyFuncMap(c.strategy)) + + // safe read of normalized token map + // this will ensure that we are minimizing + // the number of network calls to each underlying store + for _, prsdTkn := range ntm.GetMap() { + if len(prsdTkn.parsedTokens) == 0 { + // TODO: err type this + return nil, fmt.Errorf("no tokens assigned to parsedTokens slice") + } + token := prsdTkn.parsedTokens[0] + wg.Go(func() { + prsdTkn.resp = &strategy.TokenResponse{} + storeStrategy, err := s.GetImplementation(c.ctx, token) + if err != nil { + prsdTkn.resp.Err = err + return + } + prsdTkn.resp = strategy.ExchangeToken(storeStrategy, token) + }) + } + + wg.Wait() + + // now we fan out the normalized value to ReplacedToken map + // this will ensure all found tokens will have a value assigned to them + replacedToken := make(ReplacedToken) + for _, r := range ntm.GetMap() { + if r == nil { + // defensive as this shouldn't happen + continue + } + if r.resp.Err != nil { + c.Logger.Debug("cr.err %v, for token: %s", r.resp.Err, r.resp.Key().String()) + continue + } + for _, originalToken := range r.parsedTokens { + replacedToken[originalToken.String()] = keySeparatorLookup(originalToken, r.resp.Value()) + } + } + return replacedToken, nil +} + +// NormalizedToken represents the struct after all the possible tokens +// were merged into the lowest commmon denominator. +// The idea is to minimize the number of networks calls to the underlying `store` Implementations +// +// The merging is based on the implemenentation and sanitized token being the same, +// if the token contains metadata then it must be +// +// # Merging strategy +// +// Same Prefix + Same SanitisedToken && No Metadata +type NormalizedToken struct { + // all the tokens that can be used to do a replacement + parsedTokens []*config.ParsedTokenConfig + // will be assigned post generate + resp *strategy.TokenResponse + // // configToken is the last assigned full config in the loop if multip + // configToken *config.ParsedTokenConfig +} + +func (n *NormalizedToken) WithParsedToken(v *config.ParsedTokenConfig) *NormalizedToken { + n.parsedTokens = append(n.parsedTokens, v) + return n +} + +// NormalizedTokenSafe is the map of lowest common denominators +// by token.Keypathless or token.String (full token) if metadata is included +type NormalizedTokenSafe struct { + mu *sync.Mutex + normalizedTokenMap map[string]*NormalizedToken +} + +func (n NormalizedTokenSafe) GetMap() map[string]*NormalizedToken { + n.mu.Lock() + defer n.mu.Unlock() + return n.normalizedTokenMap +} + +func (c *GenVars) NormalizeRawToken(rtm *RawTokenConfig) NormalizedTokenSafe { + ntm := NormalizedTokenSafe{mu: &sync.Mutex{}, normalizedTokenMap: make(map[string]*NormalizedToken)} + + for _, r := range rtm.RawTokenMap() { + // if a string contains we need to store it uniquely + // future improvements might group all the metadata values together + if len(r.Metadata()) > 0 { + if n, found := ntm.normalizedTokenMap[r.String()]; found { + n.WithParsedToken(r) + continue + } + ntm.normalizedTokenMap[r.String()] = (&NormalizedToken{}).WithParsedToken(r) + continue + } + + if n, found := ntm.normalizedTokenMap[r.Keypathless()]; found { + n.WithParsedToken(r) + continue + } + ntm.normalizedTokenMap[r.Keypathless()] = (&NormalizedToken{}).WithParsedToken(r) + continue + } + return ntm +} diff --git a/generator/generator_test.go b/generator/generator_test.go new file mode 100644 index 0000000..e48e546 --- /dev/null +++ b/generator/generator_test.go @@ -0,0 +1,370 @@ +package generator_test + +import ( + "bytes" + "context" + "fmt" + "slices" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/strategy" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" +) + +type mockGenerate struct { + inToken, value string + err error +} + +func (m *mockGenerate) SetToken(s *config.ParsedTokenConfig) { +} +func (m *mockGenerate) Value() (s string, e error) { + return m.value, m.err +} + +func TestGenerate(t *testing.T) { + + t.Run("succeeds with funcMap", func(t *testing.T) { + var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"AWSPARAMSTR://mountPath/token", "bar", nil} + return m, nil + } + + g := generator.NewGenerator(context.TODO(), func(gv *generator.GenVars) { + gv.Logger = log.New(&bytes.Buffer{}) + }) + g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) + got, err := g.Generate([]string{"AWSPARAMSTR://mountPath/token"}) + + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 1 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 1) + } + }) + + t.Run("errors in retrieval and logs it out", func(t *testing.T) { + var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"AWSPARAMSTR://mountPath/token", "bar", fmt.Errorf("failed to get value")} + return m, nil + } + + g := generator.NewGenerator(context.TODO()) + g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) + got, err := g.Generate([]string{"AWSPARAMSTR://mountPath/token"}) + + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 0 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) + } + }) + + t.Run("retrieves values correctly from a keylookup inside", func(t *testing.T) { + var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"token-unused", `{"foo":"bar","key1":{"key2":"val"}}`, nil} + return m, nil + } + + g := generator.NewGenerator(context.TODO()) + g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) + got, err := g.Generate([]string{"AWSPARAMSTR://mountPath/token|key1.key2"}) + + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 1 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) + } + if got["AWSPARAMSTR://mountPath/token|key1.key2"] != "val" { + t.Errorf(testutils.TestPhraseWithContext, "incorrect value returned in parsedMap", got["AWSPARAMSTR://mountPath/token|key1.key2"], "val") + } + }) +} + +func TestGenerate_withKeys_lookup(t *testing.T) { + ttests := map[string]struct { + custFunc strategy.StrategyFunc + token string + expectVal string + }{ + "retrieves string value correctly from a keylookup inside": { + custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":"val"}}`, nil} + return m, nil + }, + token: "AWSPARAMSTR://mountPath/token|key1.key2", + expectVal: "val", + }, + "retrieves number value correctly from a keylookup inside": { + custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":123}}`, nil} + return m, nil + }, + token: "AWSPARAMSTR://mountPath/token|key1.key2", + expectVal: "123", + }, + "retrieves nothing as keylookup is incorrect": { + custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":123}}`, nil} + return m, nil + }, + token: "AWSPARAMSTR://mountPath/token|noprop", + expectVal: "", + }, + "retrieves value as is due to incorrectly stored json in backing store": { + custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"token", `foo":"bar","key1":{"key2":123}}`, nil} + return m, nil + }, + token: "AWSPARAMSTR://mountPath/token|noprop", + expectVal: `foo":"bar","key1":{"key2":123}}`, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + g := generator.NewGenerator(context.TODO()) + g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: tt.custFunc}) + got, err := g.Generate([]string{tt.token}) + + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 1 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) + } + if got[tt.token] != tt.expectVal { + t.Errorf(testutils.TestPhraseWithContext, "incorrect value returned in parsedMap", got[tt.token], tt.expectVal) + } + }) + } +} + +func Test_IsParsed(t *testing.T) { + ttests := map[string]struct { + val any + isParsed bool + }{ + "not parseable": { + `notparseable`, false, + }, + "one level parseable": { + `{"parseable":"foo"}`, true, + }, + "incorrect JSON": { + `parseable":"foo"}`, false, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + typ := generator.ReplacedToken{} + got := generator.IsParsed(tt.val, typ) + if got != tt.isParsed { + t.Errorf(testutils.TestPhraseWithContext, "unexpected IsParsed", got, tt.isParsed) + } + }) + } +} + +func TestGenVars_NormalizeRawToken(t *testing.T) { + + t.Run("multiple tokens", func(t *testing.T) { + g := generator.NewGenerator(context.TODO()) + + input := `GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|a + GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|b + GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|c + AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123] + AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|key1 + AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|key2 + AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj` + want := []string{"GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123]", + "AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj"} + got, err := g.DiscoverTokens(input) + if err != nil { + t.Fatal(err) + } + if len(got.GetMap()) != len(want) { + t.Errorf("got %v wanted %d", len(got.GetMap()), len(want)) + } + for key := range got.GetMap() { + if !slices.Contains(want, key) { + t.Errorf("got %s, wanted to be included in %v", key, want) + } + } + }) +} + +func Test_ConfigManager_DiscoverTokens(t *testing.T) { + ttests := map[string]struct { + input string + separator string + expect []string + }{ + "multiple tokens in single string": { + `Lorem_Ipsum: AWSPARAMSTR:///path/config|foo.user:AWSPARAMSTR:///path/config|password@AWSPARAMSTR:///path/config|foo.endpoint:AWSPARAMSTR:///path/config|foo.port/?someQ=AWSPARAMSTR:///path/queryparam|p1[version=123]&anotherQ=false`, + "://", + []string{ + "AWSPARAMSTR:///path/config", + // "AWSPARAMSTR:///path/config|password", + // "AWSPARAMSTR:///path/config|foo.endpoint", + // "AWSPARAMSTR:///path/config|foo.port", + "AWSPARAMSTR:///path/queryparam|p1[version=123]"}, + }, + "# tokens in single string": { + `Lorem_Ipsum: AWSPARAMSTR#/path/config|foo.user:AWSPARAMSTR#/path/config|password@AWSPARAMSTR#/path/config|foo.endpoint:AWSPARAMSTR#/path/config|foo.port/?someQ=AWSPARAMSTR#/path/queryparam|p1[version=123]&anotherQ=false`, + "#", + []string{ + "AWSPARAMSTR#/path/config", + // "AWSPARAMSTR#/path/config|password", + // "AWSPARAMSTR#/path/config|foo.endpoint", + // "AWSPARAMSTR#/path/config|foo.port", + "AWSPARAMSTR#/path/queryparam|p1[version=123]"}, + }, + "without leading slash and path like name # tokens in single string": { + `Lorem_Ipsum: AWSPARAMSTR#path_config|foo.user:AWSPARAMSTR#path_config|password@AWSPARAMSTR#path_config|foo.endpoint:AWSPARAMSTR#path_config|foo.port/?someQ=AWSPARAMSTR#path_queryparam|p1[version=123]&anotherQ=false`, + "#", + []string{ + "AWSPARAMSTR#path_config", + // "AWSPARAMSTR#path_config|password", + // "AWSPARAMSTR#path_config|foo.endpoint", + // "AWSPARAMSTR#path_config|foo.port", + "AWSPARAMSTR#path_queryparam|p1[version=123]"}, + }, + // Ensures all previous test cases pass as well + "extract from text correctly": { + `Where does it come from? + Contrary to popular belief, + Lorem Ipsum is AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl1 <= in middle of sentencenot simply random text. + It has roots in a piece of classical Latin literature from 45 + BC, making it over 2000 years old. Richard McClintock, a Latin professor at + Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, c + onsectetur, from a Lorem Ipsum passage , at the end of line => AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl4 + and going through the cites of the word in c + lassical literature, discovered the undoubtable source. Lorem Ipsum comes from secti + ons in singles =>'AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl2'1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) + in doubles => "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl3" + by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular + during the :=> embedded in text RenaissanceAWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl5 embedded in text <=: + The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.`, + "://", + []string{ + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl1", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl2", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl3", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl4", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsfl5", + }, + }, + "unknown implementation not picked up": { + `foo: AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + bar: AWSPARAMSTR://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123] + unknown: GCPPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + unknown: GCPSECRETS#/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj`, + "://", + []string{ + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AWSPARAMSTR://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123]"}, + }, + "all implementations": { + `param: AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + secretsmgr: AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123] + gcp: GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + vault: VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + som othere strufsd + azkv: AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj`, + "://", + []string{ + "GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123]", + "AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", + "VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj"}, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + config.VarPrefix = map[config.ImplementationPrefix]bool{"AWSPARAMSTR": true} + g := generator.NewGenerator(context.TODO()) + g.Config().WithTokenSeparator(tt.separator) + gdt, err := g.DiscoverTokens(tt.input) + if err != nil { + t.Fatal(err) + } + got := gdt.GetMap() + + if len(got) != len(tt.expect) { + t.Errorf("wrong nmber of tokens resolved\ngot (%d) want (%d)", len(got), len(tt.expect)) + } + // for _, v := range got { + // if !slices.Contains(tt.expect, v.String()) { + // t.Errorf("got (%s) not found in expected slice (%v)", v, tt.expect) + // } + // } + }) + } +} + +func Test_Generate_EnsureRaceFree(t *testing.T) { + g := generator.NewGenerator(context.TODO()) + + input := ` +fg +dfg gdfgfdGCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj +GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|a +GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|b +GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|c +ddsffds AWSPARAMSTR:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj + 'AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj[version=123]' + AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|key1 + AWSSECRETS://bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|key2 + AZKVSECRET:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj gdf gdfgdf + dfg gdf gdf gdf + fdg dgf dgf + VAULT:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj . dfg dfgdf dfg fddf` + + g.WithStrategyMap(strategy.StrategyFuncMap{ + config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", `{"a":"bar","b":{"key2":"val"},"c":123}`, nil} + return m, nil + }, + config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", `{"a":"bar","b":{"key2":"val"},"c":123}`, nil} + return m, nil + }, + config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"bar/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", `{"key1":"bar","key2":"val","c":123}`, nil} + return m, nil + }, + config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", `{"key1":"bar","key2":"val","c":123}`, nil} + return m, nil + }, + config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + m := &mockGenerate{"/djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj", `{"key1":"bar","key2":"val","c":123}`, nil} + return m, nil + }, + }) + + got, err := g.Generate([]string{input}) + if err != nil { + t.Fatal(err) + } + if len(got) != 10 { + t.Errorf("got %v wanted %d", len(got), 10) + } + +} diff --git a/generator/generatorvars.go b/generator/generatorvars.go new file mode 100644 index 0000000..79a56ae --- /dev/null +++ b/generator/generatorvars.go @@ -0,0 +1,94 @@ +package generator + +import ( + "fmt" + "strconv" + "sync" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/spyzhov/ajson" +) + +// ReplacedToken is the internal working object definition and +// the return type if results are not flushed to file +type ReplacedToken map[string]any + +func (pm ReplacedToken) MapKeys() (keys []string) { + for k := range pm { + keys = append(keys, k) + } + return +} + +// RawTokenConfig represents the map of +// discovered tokens via the lexer/parser +type RawTokenConfig struct { + mu *sync.Mutex + tokenMap map[string]*config.ParsedTokenConfig +} + +func NewRawTokenConfig() *RawTokenConfig { + return &RawTokenConfig{mu: &sync.Mutex{}, tokenMap: map[string]*config.ParsedTokenConfig{}} +} + +func (rtm *RawTokenConfig) AddToken(name string, parsedToken *config.ParsedTokenConfig) { + rtm.mu.Lock() + defer rtm.mu.Unlock() + rtm.tokenMap[name] = parsedToken +} + +func (rtm *RawTokenConfig) RawTokenMap() map[string]*config.ParsedTokenConfig { + rtm.mu.Lock() + defer rtm.mu.Unlock() + return rtm.tokenMap +} + +// type tokenMapSafe struct { +// mu *sync.Mutex +// tokenMap ReplacedToken +// } + +// func (tms *tokenMapSafe) getTokenMap() ReplacedToken { +// tms.mu.Lock() +// defer tms.mu.Unlock() +// return tms.tokenMap +// } + +// func (tms *tokenMapSafe) addKeyVal(key *config.ParsedTokenConfig, val string) { +// tms.mu.Lock() +// defer tms.mu.Unlock() +// // NOTE: still use the metadata in the key +// // there could be different versions / labels for the same token and hence different values +// // However the JSONpath look up +// tms.tokenMap[key.String()] = keySeparatorLookup(key, val) +// } + +// keySeparatorLookup checks if the key contains +// keySeparator character +// If it does contain one then it tries to parse +func keySeparatorLookup(token *config.ParsedTokenConfig, val string) string { + k := token.LookupKeys() + if k == "" { + return val + } + + keys, err := ajson.JSONPath([]byte(val), fmt.Sprintf("$..%s", k)) + if err != nil { + return val + } + + if len(keys) == 1 { + v := keys[0] + if v.Type() == ajson.String { + str, err := strconv.Unquote(fmt.Sprintf("%v", v)) + if err != nil { + return fmt.Sprintf("%v", v) + } + return str + } + + return fmt.Sprintf("%v", v) + } + + return "" +} diff --git a/go.mod b/go.mod index fc3f536..1d37d5a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/DevLabFoundry/configmanager/v2 +module github.com/DevLabFoundry/configmanager/v3 go 1.25.3 diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index f1f71dc..b86b292 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -11,15 +11,16 @@ import ( "os" "strings" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/spf13/cobra" ) type configManagerIface interface { - RetrieveWithInputReplaced(input string) (string, error) - Retrieve(tokens []string) (generator.ParsedMap, error) + RetrieveReplacedBytes(input []byte) ([]byte, error) + RetrieveReplacedString(input string) (string, error) + Retrieve(tokens []string) (generator.ReplacedToken, error) GeneratorConfig() *config.GenVarsConfig } @@ -111,13 +112,13 @@ func (c *CmdUtils) generateStrOutFromInput(input io.Reader, writer io.Writer) er return err } - str, err := c.configManager.RetrieveWithInputReplaced(string(b)) + replacedBytes, err := c.configManager.RetrieveReplacedBytes(b) if err != nil { return err } pp := &PostProcessor{} - return pp.StrToFile(writer, str) + return pp.StrToFile(writer, string(replacedBytes)) } type WriterCloserWrapper struct { diff --git a/internal/cmdutils/cmdutils_test.go b/internal/cmdutils/cmdutils_test.go index f8fad0f..7ec1daf 100644 --- a/internal/cmdutils/cmdutils_test.go +++ b/internal/cmdutils/cmdutils_test.go @@ -8,16 +8,16 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/cmdutils" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - log "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/cmdutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + log "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/spf13/cobra" ) type mockCfgMgr struct { - parsedMap generator.ParsedMap + parsedMap generator.ReplacedToken err error parsedString string config *config.GenVarsConfig @@ -27,7 +27,13 @@ func (m mockCfgMgr) RetrieveWithInputReplaced(input string) (string, error) { return m.parsedString, m.err } -func (m mockCfgMgr) Retrieve(tokens []string) (generator.ParsedMap, error) { +func (m mockCfgMgr) RetrieveReplacedBytes(input []byte) ([]byte, error) { + return []byte(m.parsedString), m.err +} +func (m mockCfgMgr) RetrieveReplacedString(input string) (string, error) { + return m.parsedString, m.err +} +func (m mockCfgMgr) Retrieve(tokens []string) (generator.ReplacedToken, error) { return m.parsedMap, m.err } @@ -61,15 +67,13 @@ func cmdTestHelper(t *testing.T, err error, got []byte, expect []string) { } func Test_GenerateFromCmd(t *testing.T) { - t.Parallel() - ttests := map[string]struct { - mockMap generator.ParsedMap + mockMap generator.ReplacedToken tokens []string expect []string }{ "succeeds with 3 tokens": { - generator.ParsedMap{"FOO://bar/qusx": "aksujg", "FOO://bar/lorem": "", "FOO://bar/ducks": "sdhbjk0293"}, + generator.ReplacedToken{"FOO://bar/qusx": "aksujg", "FOO://bar/lorem": "", "FOO://bar/ducks": "sdhbjk0293"}, []string{"FOO://bar/qusx", "FOO://bar/lorem", "FOO://bar/ducks"}, []string{"export QUSX='aksujg'", "export LOREM=''", "export DUCKS='sdhbjk0293'"}, }, @@ -106,8 +110,6 @@ func (m *mockWriter) Write(in []byte) (int, error) { } func Test_GenerateStrOut(t *testing.T) { - t.Parallel() - inputStr := `FOO://bar/qusx FOO://bar/lorem FOO://bar/ducks` mockParsedStr := `aksujg fooLorem Mighty` expect := []string{"aksujg", "fooLorem", "Mighty"} @@ -210,7 +212,7 @@ func Test_CmdUtils_Errors_on(t *testing.T) { t.Run("REtrieve from tokens in fetching ANY of the tokens", func(t *testing.T) { m := &mockCfgMgr{ config: config.NewConfig(), - parsedMap: generator.ParsedMap{}, + parsedMap: generator.ReplacedToken{}, err: fmt.Errorf("err in fetching tokens"), } @@ -225,7 +227,7 @@ func Test_CmdUtils_Errors_on(t *testing.T) { t.Run("REtrieve from tokens in fetching SOME of the tokens", func(t *testing.T) { m := &mockCfgMgr{ config: config.NewConfig(), - parsedMap: generator.ParsedMap{"IMNP://foo": "bar"}, + parsedMap: generator.ReplacedToken{"IMNP://foo": "bar"}, err: fmt.Errorf("err in fetching tokens"), } @@ -239,7 +241,7 @@ func Test_CmdUtils_Errors_on(t *testing.T) { t.Run("REtrieve from string in fetching SOME of the tokens", func(t *testing.T) { m := &mockCfgMgr{ config: config.NewConfig().WithOutputPath("stdout"), - parsedMap: generator.ParsedMap{"IMNP://foo": "bar"}, + parsedMap: generator.ReplacedToken{"IMNP://foo": "bar"}, parsedString: `bar `, err: fmt.Errorf("err in fetching tokens"), } diff --git a/internal/cmdutils/postprocessor.go b/internal/cmdutils/postprocessor.go index 8419cc7..3b4a33b 100644 --- a/internal/cmdutils/postprocessor.go +++ b/internal/cmdutils/postprocessor.go @@ -5,15 +5,15 @@ import ( "io" "strings" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/config" ) // PostProcessor // processes the rawMap and outputs the result // depending on cmdline options type PostProcessor struct { - ProcessedMap generator.ParsedMap + ProcessedMap generator.ReplacedToken Config *config.GenVarsConfig outString []string } @@ -24,7 +24,7 @@ func (p *PostProcessor) ConvertToExportVar() []string { for k, v := range p.ProcessedMap { rawKeyToken := strings.Split(k, "/") // assumes a path like token was used topLevelKey := rawKeyToken[len(rawKeyToken)-1] - trm := generator.ParsedMap{} + trm := generator.ReplacedToken{} if parsedOk := generator.IsParsed(v, trm); parsedOk { // if is a map // try look up on key if separator defined @@ -32,21 +32,21 @@ func (p *PostProcessor) ConvertToExportVar() []string { p.exportVars(normMap) continue } - p.exportVars(generator.ParsedMap{topLevelKey: v}) + p.exportVars(generator.ReplacedToken{topLevelKey: v}) } return p.outString } // envVarNormalize -func (p *PostProcessor) envVarNormalize(pmap generator.ParsedMap) generator.ParsedMap { - normalizedMap := make(generator.ParsedMap) +func (p *PostProcessor) envVarNormalize(pmap generator.ReplacedToken) generator.ReplacedToken { + normalizedMap := make(generator.ReplacedToken) for k, v := range pmap { normalizedMap[p.normalizeKey(k)] = v } return normalizedMap } -func (p *PostProcessor) exportVars(exportMap generator.ParsedMap) { +func (p *PostProcessor) exportVars(exportMap generator.ReplacedToken) { for k, v := range exportMap { // NOTE: \n line ending is not totally cross platform diff --git a/internal/cmdutils/postprocessor_test.go b/internal/cmdutils/postprocessor_test.go index 001ea68..5c18e23 100644 --- a/internal/cmdutils/postprocessor_test.go +++ b/internal/cmdutils/postprocessor_test.go @@ -5,10 +5,10 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/cmdutils" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" + "github.com/DevLabFoundry/configmanager/v3/generator" + "github.com/DevLabFoundry/configmanager/v3/internal/cmdutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func postprocessorHelper(t *testing.T) { @@ -17,14 +17,14 @@ func postprocessorHelper(t *testing.T) { } func Test_ConvertToExportVars(t *testing.T) { tests := map[string]struct { - rawMap generator.ParsedMap + rawMap generator.ReplacedToken expectStr string expectLength int }{ - "number included": {generator.ParsedMap{"foo": "BAR", "num": 123}, `export FOO='BAR'`, 2}, - "strings only": {generator.ParsedMap{"foo": "BAR", "num": "a123"}, `export FOO='BAR'`, 2}, - "numbers only": {generator.ParsedMap{"foo": 123, "num": 456}, `export FOO=123`, 2}, - "map inside response": {generator.ParsedMap{"map": `{"foo":"bar","baz":"qux"}`, "num": 123}, `export FOO='bar'`, 3}, + "number included": {generator.ReplacedToken{"foo": "BAR", "num": 123}, `export FOO='BAR'`, 2}, + "strings only": {generator.ReplacedToken{"foo": "BAR", "num": "a123"}, `export FOO='BAR'`, 2}, + "numbers only": {generator.ReplacedToken{"foo": 123, "num": 456}, `export FOO=123`, 2}, + "map inside response": {generator.ReplacedToken{"map": `{"foo":"bar","baz":"qux"}`, "num": 123}, `export FOO='bar'`, 3}, } for name, tt := range tests { diff --git a/internal/config/config.go b/internal/config/config.go index 50aecce..d1d05ed 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,7 @@ var ( GcpSecretsPrefix: true, HashicorpVaultPrefix: true, AzTableStorePrefix: true, AzAppConfigPrefix: true, UnknownPrefix: true, } + ErrConfigValidation = errors.New("config validation failed") ) // GenVarsConfig defines the input config object to be passed @@ -60,7 +61,9 @@ type GenVarsConfig struct { // parseAdditionalVars func(token string) TokenConfigVars } -// NewConfig +// NewConfig returns a new GenVarsConfig with default values +// +// keySeparator should be only a single character func NewConfig() *GenVarsConfig { return &GenVarsConfig{ tokenSeparator: tokenSeparator, @@ -120,49 +123,51 @@ func (c *GenVarsConfig) Config() GenVarsConfig { return cc } -// Parsed token config section +// Config returns the derefed value +func (c *GenVarsConfig) Validate() error { + if len(c.keySeparator) > 1 { + return fmt.Errorf("%w, keyseparator can only be 1 character", ErrConfigValidation) + } + return nil +} +// Parsed token config section var ErrInvalidTokenPrefix = errors.New("token prefix has no implementation") type ParsedTokenConfig struct { - prefix ImplementationPrefix + prefix ImplementationPrefix + // cofig values keySeparator, tokenSeparator string - prefixLessToken, fullToken string - metadataStr, keysPath string - storeToken, metadataLess string + // tokenb parts + metadataStr string + keysPath string + sanitizedToken string } -// NewParsedTokenConfig returns a pointer to a new TokenConfig struct -// returns nil if current prefix does not correspond to an Implementation -// -// The caller needs to make sure it is not nil -// TODO: a custom parser would be best here -func NewParsedTokenConfig(token string, config GenVarsConfig) (*ParsedTokenConfig, error) { - ptc := &ParsedTokenConfig{} - prfx := strings.Split(token, config.TokenSeparator())[0] - - // This should already only be a list of properly supported tokens but just in case - if found := VarPrefix[ImplementationPrefix(prfx)]; !found { - return nil, fmt.Errorf("prefix: %s\n%w", prfx, ErrInvalidTokenPrefix) +// NewToken initialises a *ParsedTokenConfig +func NewToken(prefix ImplementationPrefix, config GenVarsConfig) (*ParsedTokenConfig, error) { + tokenConf := &ParsedTokenConfig{} + if err := config.Validate(); err != nil { + return nil, err } + tokenConf.keySeparator = config.keySeparator + tokenConf.tokenSeparator = config.tokenSeparator + + tokenConf.prefix = prefix + + return tokenConf, nil +} - ptc.keySeparator = config.keySeparator - ptc.tokenSeparator = config.tokenSeparator - ptc.prefix = ImplementationPrefix(prfx) - ptc.fullToken = token - return ptc.new(), nil +func (ptc *ParsedTokenConfig) WithKeyPath(kp string) { + ptc.keysPath = kp } -func (ptc *ParsedTokenConfig) new() *ParsedTokenConfig { - // order must be respected here - // - ptc.prefixLessToken = strings.Replace(ptc.fullToken, fmt.Sprintf("%s%s", ptc.prefix, ptc.tokenSeparator), "", 1) +func (ptc *ParsedTokenConfig) WithMetadata(md string) { + ptc.metadataStr = md +} - // token without metadata and the string itself - ptc.extractMetadataStr() - // token without keys - ptc.keysLookup() - return ptc +func (ptc *ParsedTokenConfig) WithSanitizedToken(v string) { + ptc.sanitizedToken = v } func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { @@ -189,86 +194,52 @@ func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { return nil } -func (t *ParsedTokenConfig) StripPrefix() string { - return t.prefixLessToken -} - -// StripMetadata returns the fullToken without the -// metadata -func (t *ParsedTokenConfig) StripMetadata() string { - return t.metadataLess -} - -// Strip -// -// returns the only the store indicator string -// without any of the configmanager token enrichment: -// -// - metadata -// -// - keySeparator -// -// - keys -// -// - prefix +// StoreToken returns the sanitized token without: +// - metadata +// - keySeparator +// - keys +// - prefix func (t *ParsedTokenConfig) StoreToken() string { - return t.storeToken + return t.sanitizedToken } // Full returns the full Token path. // Including key separator and metadata values func (t *ParsedTokenConfig) String() string { - return t.fullToken + token := t.Metadaless() + if len(t.metadataStr) > 0 { + token += fmt.Sprintf("[%s]", t.metadataStr) + } + return token } -func (t *ParsedTokenConfig) LookupKeys() string { - return t.keysPath +// Keypathless returns the token without the key and metadata attributes +// Token will include the ImplementationPrefix + token separator + path to item +func (t *ParsedTokenConfig) Keypathless() string { + token := fmt.Sprintf("%s%s%s", t.prefix, t.tokenSeparator, t.sanitizedToken) + return token } -func (t *ParsedTokenConfig) Prefix() ImplementationPrefix { - return t.prefix +func (t *ParsedTokenConfig) Metadaless() string { + token := fmt.Sprintf("%s%s%s", t.prefix, t.tokenSeparator, t.sanitizedToken) + if len(t.keysPath) > 0 { + token += t.keySeparator + t.keysPath + } + return token } -const ( - startMetaStr string = `[` - endMetaStr string = `]` -) +func (t *ParsedTokenConfig) LookupKeys() string { + return t.keysPath +} -// extractMetadataStr returns anything between the start and end -// metadata markers in the token string itself -// returns the token without meta -func (t *ParsedTokenConfig) extractMetadataStr() { - token := t.prefixLessToken - t.metadataLess = token - startIndex := strings.Index(token, startMetaStr) - // token has no startMetaStr - if startIndex == -1 { - return - } - newS := token[startIndex+len(startMetaStr):] +func (t *ParsedTokenConfig) Metadata() string { + return t.metadataStr +} - endIndex := strings.Index(newS, endMetaStr) - // token has no meta end - if endIndex == -1 { - return - } - // metastring extracted - // complete [key=value] has been found - metaString := newS[:endIndex] - t.metadataStr = metaString - // Set Metadataless token - t.metadataLess = strings.ReplaceAll(token, startMetaStr+metaString+endMetaStr, "") +func (t *ParsedTokenConfig) Prefix() ImplementationPrefix { + return t.prefix } -// keysLookup returns the keysLookup path and the string without it -// -// NOTE: metadata was already stripped at this point -func (t *ParsedTokenConfig) keysLookup() { - keysIndex := strings.Index(t.metadataLess, t.keySeparator) - if keysIndex >= 0 { - t.keysPath = t.metadataLess[keysIndex+len(t.keySeparator):] - t.storeToken = t.metadataLess[:keysIndex] - return - } - t.storeToken = t.metadataLess +func (t *ParsedTokenConfig) TokenSeparator() string { + return t.tokenSeparator } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6bc6803..8fc33f0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -3,8 +3,8 @@ package config_test import ( "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func Test_SelfName(t *testing.T) { @@ -30,69 +30,77 @@ func Test_MarshalMetadata_with_label_struct_succeeds(t *testing.T) { } ttests := map[string]struct { - config *config.GenVarsConfig - rawToken string + token func() *config.ParsedTokenConfig wantLabel string wantMetaStrippedToken string }{ "when provider expects label on token and label exists": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123|d88[label=dev]`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithKeyPath("d88") + tkn.WithMetadata("label=dev") + tkn.WithSanitizedToken("basjh/dskjuds/123") + return tkn + }, "dev", "basjh/dskjuds/123", }, "when provider expects label on token and label does not exist": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123|d88[someother=dev]`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithKeyPath("d88") + tkn.WithMetadata("someother=dev") + tkn.WithSanitizedToken("basjh/dskjuds/123") + return tkn + }, "", "basjh/dskjuds/123", }, "no metadata found": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123|d88`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithKeyPath("d88") + tkn.WithSanitizedToken("basjh/dskjuds/123") + return tkn + }, "", "basjh/dskjuds/123", }, "no metadata found incorrect marker placement": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123|d88]asdas=bar[`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithKeyPath("d88]asdas=bar[") + tkn.WithSanitizedToken("basjh/dskjuds/123") + return tkn + }, "", "basjh/dskjuds/123", }, "no metadata found incorrect marker placement and no key separator": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123]asdas=bar[`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithSanitizedToken("basjh/dskjuds/123]asdas=bar[") + return tkn + }, "", "basjh/dskjuds/123]asdas=bar[", }, - "no end found incorrect marker placement and no key separator": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123[asdas=bar`, - "", - "basjh/dskjuds/123[asdas=bar", - }, "no start found incorrect marker placement and no key separator": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123]asdas=bar]`, + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("://")) + tkn.WithKeyPath("d88") + tkn.WithMetadata("someother=dev") + tkn.WithSanitizedToken("basjh/dskjuds/123]asdas=bar]") + return tkn + }, "", "basjh/dskjuds/123]asdas=bar]", }, - "metadata is in the middle of path lookup": { - config.NewConfig().WithTokenSeparator("://"), - `AZTABLESTORE://basjh/dskjuds/123[label=bar]|lookup`, - "bar", - "basjh/dskjuds/123", - }, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { inputTyp := &labelMeta{} - got, err := config.NewParsedTokenConfig(tt.rawToken, *tt.config) - - if err != nil { - t.Fatalf("got an error on NewParsedTokenconfig (%s)\n", tt.rawToken) - } - + got := tt.token() if got == nil { t.Errorf(testutils.TestPhraseWithContext, "Unable to parse token", nil, config.ParsedTokenConfig{}) } @@ -100,7 +108,7 @@ func Test_MarshalMetadata_with_label_struct_succeeds(t *testing.T) { got.ParseMetadata(inputTyp) if got.StoreToken() != tt.wantMetaStrippedToken { - t.Errorf(testutils.TestPhraseWithContext, "Token does not match", got.StripMetadata(), tt.wantMetaStrippedToken) + t.Errorf(testutils.TestPhraseWithContext, "Token does not match", got.StoreToken(), tt.wantMetaStrippedToken) } if inputTyp.Label != tt.wantLabel { @@ -115,23 +123,27 @@ func Test_TokenParser_config(t *testing.T) { Version string `json:"version"` } ttests := map[string]struct { - input string - expPrefix config.ImplementationPrefix - expLookupKeys string - expStoreToken string - expString string // fullToken - expMetadataVersion string + rawToken, keyPath, metadataStr string + expPrefix config.ImplementationPrefix + expLookupKeys string + expStoreToken string // sanitised + expString string // fullToken + expMetadataVersion string }{ - "bare": {"AWSSECRETS://foo/bar", config.SecretMgrPrefix, "", "foo/bar", "AWSSECRETS://foo/bar", ""}, - "with metadata version": {"AWSSECRETS://foo/bar[version=123]", config.SecretMgrPrefix, "", "foo/bar", "AWSSECRETS://foo/bar[version=123]", "123"}, - "with keys lookup and label": {"AWSSECRETS://foo/bar|key1.key2[version=123]", config.SecretMgrPrefix, "key1.key2", "foo/bar", "AWSSECRETS://foo/bar|key1.key2[version=123]", "123"}, - "with keys lookup and longer token": {"AWSSECRETS://foo/bar|key1.key2]version=123]", config.SecretMgrPrefix, "key1.key2]version=123]", "foo/bar", "AWSSECRETS://foo/bar|key1.key2]version=123]", ""}, - "with keys lookup but no keys": {"AWSSECRETS://foo/bar/sdf/sddd.90dsfsd|[version=123]", config.SecretMgrPrefix, "", "foo/bar/sdf/sddd.90dsfsd", "AWSSECRETS://foo/bar/sdf/sddd.90dsfsd|[version=123]", "123"}, + "bare": {"foo/bar", "", "", config.SecretMgrPrefix, "", "foo/bar", "AWSSECRETS://foo/bar", ""}, + "with metadata version": {"foo/bar", "", "version=123", config.SecretMgrPrefix, "", "foo/bar", "AWSSECRETS://foo/bar[version=123]", "123"}, + "with keys lookup and label": {"foo/bar", "key1.key2", "version=123", config.SecretMgrPrefix, "key1.key2", "foo/bar", "AWSSECRETS://foo/bar|key1.key2[version=123]", "123"}, + "with keys lookup and longer token": {"foo/bar", "key1.key2]version=123]", "", config.SecretMgrPrefix, "key1.key2]version=123]", "foo/bar", "AWSSECRETS://foo/bar|key1.key2]version=123]", ""}, + "with keys lookup but no keys": {"foo/bar/sdf/sddd.90dsfsd", "", "version=123", config.SecretMgrPrefix, "", "foo/bar/sdf/sddd.90dsfsd", "AWSSECRETS://foo/bar/sdf/sddd.90dsfsd[version=123]", "123"}, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { conf := &mockConfAwsSecrMgr{} - got, _ := config.NewParsedTokenConfig(tt.input, *config.NewConfig()) + got, _ := config.NewToken(tt.expPrefix, *config.NewConfig()) + got.WithSanitizedToken(tt.rawToken) + got.WithKeyPath(tt.keyPath) + got.WithMetadata(tt.metadataStr) + got.ParseMetadata(conf) if got.LookupKeys() != tt.expLookupKeys { @@ -152,3 +164,21 @@ func Test_TokenParser_config(t *testing.T) { }) } } + +func TestLookupIdent(t *testing.T) { + ttests := map[string]struct { + char string + expect config.TokenType + }{ + "new line": {"\n", config.NEW_LINE}, + "dash": {"-", config.TEXT}, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + got := config.LookupIdent(tt.char) + if got != tt.expect { + t.Errorf("got %v wanted %v", got, tt.expect) + } + }) + } +} diff --git a/internal/config/token.go b/internal/config/token.go new file mode 100644 index 0000000..00b13a6 --- /dev/null +++ b/internal/config/token.go @@ -0,0 +1,80 @@ +package config + +// TokenType is the lexer parsed TokenType +type TokenType string + +const ( + ILLEGAL TokenType = "ILLEGAL" + EOF TokenType = "EOF" + + SPACE TokenType = "SPACE" // ' ' + TAB TokenType = "TAB" // '\t' + NEW_LINE TokenType = "NEW_LINE" // '\n' + CARRIAGE_RETURN TokenType = "CARRIAGE_RETURN" // '\r' + CONTROL TokenType = "CONTROL" + + // Identifiers + literals + TEXT TokenType = "TEXT" + + EXCLAMATION TokenType = "!" + DOUBLE_QUOTE TokenType = "\"" + SINGLE_QUOTE TokenType = "'" + // other separators + AT_SIGN TokenType = "AT_SIGN" // `@` + PIPE TokenType = "PIPE" // `|` + COLON TokenType = "COLON" // `:` + EQUALS TokenType = "EQUALS" // `=` + DOT TokenType = "DOT" // `.` + COMMA TokenType = "COMMA" // `,` + QUESTION_MARK TokenType = "QUESTION_MARK" // `?` + BACK_SLASH TokenType = "BACK_SLASH" // `\` + FORWARD_SLASH TokenType = "FORWARD_SLASH" // `/` + SLASH_QUESTION_MARK TokenType = "SLASH_QUESTION_MARK" // `/?` + + // Comment Tokens + DOUBLE_FORWARD_SLASH TokenType = "DOUBLE_FORWARD_SLASH" // `//` + HASH TokenType = "HASH" // `#` + + // CONFIGMANAGER_TOKEN Keywords + // CONFIGMANAGER_TOKEN_SEPARATOR TokenType = "CONFIGMANAGER_TOKEN_SEPARATOR" // Dynamically set + BEGIN_CONFIGMANAGER_TOKEN TokenType = "BEGIN_CONFIGMANAGER_TOKEN" // Dynamically set + CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR TokenType = "CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR" // Dynamically set + BEGIN_META_CONFIGMANAGER_TOKEN TokenType = "BEGIN_META_CONFIGMANAGER_TOKEN" // `[` + END_META_CONFIGMANAGER_TOKEN TokenType = "END_META_CONFIGMANAGER_TOKEN" // `]` + // This may not possible + END_CONFIGMANAGER_TOKEN TokenType = "END_CONFIGMANAGER_TOKEN" + + // Parsed "expressions" + CONFIGMANAGER_TOKEN_CONTENT TokenType = "CONFIGMANAGER_TOKEN_CONTENT" + UNUSED_TEXT TokenType = "UNUSED_TEXT" +) + +type Source struct { + File string `json:"file"` + Path string `json:"path"` +} + +// Token is the basic structure of the captured token +type Token struct { + Type TokenType + Literal string + ImpPrefix ImplementationPrefix + Line int + Column int + Source Source +} + +var keywords = map[string]TokenType{ + " ": SPACE, + "\n": NEW_LINE, + "\r": CARRIAGE_RETURN, + "\t": TAB, + "\f": CONTROL, +} + +func LookupIdent(ident string) TokenType { + if tok, ok := keywords[ident]; ok { + return tok + } + return TEXT +} diff --git a/internal/lexer/lexer.go b/internal/lexer/lexer.go new file mode 100644 index 0000000..a8f5d5a --- /dev/null +++ b/internal/lexer/lexer.go @@ -0,0 +1,241 @@ +// Package lexer +// +// Performs lexical analysis on the source files and emits tokens. +package lexer + +import ( + "github.com/DevLabFoundry/configmanager/v3/internal/config" +) + +// nonText characters captures all character sets that are _not_ assignable to TEXT +var nonText = map[string]bool{ + // separators + " ": true, "\n": true, "\r": true, "\t": true, + "=": true, ".": true, ",": true, "|": true, "?": true, "/": true, "@": true, ":": true, + "]": true, "[": true, "'": true, "\"": true, + // initial chars of potential identifiers + // this forces the lexer to not treat at as TEXT + // and enter the switch statement of the state machine + // NOTE: when a new implementation is added we should add it here + // AWS|AZure + "A": true, + // VAULT (HashiCorp) + "V": true, + // GCP + "G": true, +} + +type Source struct { + Input string + FileName string + FullPath string +} + +// Lexer +type Lexer struct { + config config.GenVarsConfig + keySeparator byte + length int + source Source + position int // current position in input (points to current char) + readPosition int // current reading position in input (after current char) + ch byte // current char under examination + line int // current line - start at 1 + column int // column of text - gets set to 0 on every new line - start at 0 +} + +// New returns a Lexer pointer allocation +func New(source Source, config config.GenVarsConfig) *Lexer { + l := &Lexer{ + source: source, + line: 1, + column: 0, + length: len(source.Input), + config: config, + keySeparator: config.KeySeparator()[0], + } + l.readChar() + return l +} + +// NextToken advances through the source returning a found token +func (l *Lexer) NextToken() config.Token { + var tok config.Token + + switch l.ch { + // identify the dynamically selected key separator + case l.keySeparator: + tok = config.Token{Type: config.CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR, Literal: string(l.ch)} + // Specific cases for BEGIN_CONFIGMANAGER_TOKEN possibilities + case 'A': + if l.peekChar() == 'W' { + // AWS store types + l.readChar() + if found, literal, imp := l.peekIsBeginOfToken([]config.ImplementationPrefix{config.SecretMgrPrefix, config.ParamStorePrefix}, "AW"); found { + tok = config.Token{Type: config.BEGIN_CONFIGMANAGER_TOKEN, Literal: literal, ImpPrefix: imp} + } else { + // it is not a marker AW as text + tok = config.Token{Type: config.TEXT, Literal: "AW"} + } + } else if l.peekChar() == 'Z' { + // Azure Store Types + l.readChar() + if found, literal, imp := l.peekIsBeginOfToken([]config.ImplementationPrefix{config.AzKeyVaultSecretsPrefix, config.AzTableStorePrefix, config.AzAppConfigPrefix}, "AZ"); found { + tok = config.Token{Type: config.BEGIN_CONFIGMANAGER_TOKEN, Literal: literal, ImpPrefix: imp} + } else { + // it is not a marker AZ as text + tok = config.Token{Type: config.TEXT, Literal: "AZ"} + } + } else { + tok = config.Token{Type: config.TEXT, Literal: "A"} + } + case 'G': + // GCP TOKENS + if l.peekChar() == 'C' { + l.readChar() + if found, literal, imp := l.peekIsBeginOfToken([]config.ImplementationPrefix{config.GcpSecretsPrefix}, "GC"); found { + tok = config.Token{Type: config.BEGIN_CONFIGMANAGER_TOKEN, Literal: literal, ImpPrefix: imp} + } else { + // it is not a marker - GC literal as text + tok = config.Token{Type: config.TEXT, Literal: "GC"} + } + } else { + tok = config.Token{Type: config.TEXT, Literal: "G"} + } + case 'V': + // HASHI VAULT Tokens + if l.peekChar() == 'A' { + l.readChar() + if found, literal, imp := l.peekIsBeginOfToken([]config.ImplementationPrefix{config.HashicorpVaultPrefix}, "VA"); found { + tok = config.Token{Type: config.BEGIN_CONFIGMANAGER_TOKEN, Literal: literal, ImpPrefix: imp} + } else { + // it is not a marker VA as text + tok = config.Token{Type: config.TEXT, Literal: "VA"} + } + } else { + tok = config.Token{Type: config.TEXT, Literal: "V"} + } + case '=': + tok = config.Token{Type: config.EQUALS, Literal: "="} + case '.': + tok = config.Token{Type: config.DOT, Literal: "."} + case ',': + tok = config.Token{Type: config.COMMA, Literal: ","} + case '/': + if l.peekChar() == '?' { + l.readChar() + tok = config.Token{Type: config.SLASH_QUESTION_MARK, Literal: "/?"} + } else { + tok = config.Token{Type: config.FORWARD_SLASH, Literal: "/"} + } + case '\\': + tok = config.Token{Type: config.BACK_SLASH, Literal: "\\"} + case '?': + tok = config.Token{Type: config.QUESTION_MARK, Literal: "?"} + case ']': + tok = config.Token{Type: config.END_META_CONFIGMANAGER_TOKEN, Literal: "]"} + case '[': + tok = config.Token{Type: config.BEGIN_META_CONFIGMANAGER_TOKEN, Literal: "["} + case '|': + tok = config.Token{Type: config.PIPE, Literal: "|"} + case '@': + tok = config.Token{Type: config.AT_SIGN, Literal: "@"} + case ':': + tok = config.Token{Type: config.COLON, Literal: ":"} + case '"': + tok = config.Token{Type: config.DOUBLE_QUOTE, Literal: "\""} + case '\'': + tok = config.Token{Type: config.SINGLE_QUOTE, Literal: "'"} + case '\n': + l.line = l.line + 1 + l.column = 0 // reset column count + tok = l.setTextSeparatorToken() + // want to preserve all indentations and punctuation + case ' ', '\r', '\t', '\f': + tok = l.setTextSeparatorToken() + case 0: + tok.Literal = "" + tok.Type = config.EOF + default: + if isText(l.ch) { + tok.Literal = l.readText() + tok.Type = config.TEXT + return tok + } + tok = newToken(config.ILLEGAL, l.ch) + } + // add general properties to each token + tok.Line = l.line + tok.Column = l.column + tok.Source = config.Source{Path: l.source.FullPath, File: l.source.FileName} + l.readChar() + return tok +} + +// readChar moves cursor along +func (l *Lexer) readChar() { + if l.readPosition >= l.length { + l.ch = 0 + } else { + l.ch = l.source.Input[l.readPosition] + } + l.position = l.readPosition + l.readPosition += 1 + l.column += 1 +} + +// peekChar reveals next char withouh advancing the cursor along +func (l *Lexer) peekChar() byte { + if l.readPosition >= l.length { + return 0 + } else { + return l.source.Input[l.readPosition] + } +} + +func (l *Lexer) readText() string { + position := l.position + for isText(l.ch) && l.readPosition <= l.length { + l.readChar() + } + return l.source.Input[position:l.position] +} + +func (l *Lexer) setTextSeparatorToken() config.Token { + tok := newToken(config.LookupIdent(string(l.ch)), l.ch) + return tok +} + +// peekIsBeginOfToken attempts to identify the possible token +func (l *Lexer) peekIsBeginOfToken(possibleBeginToken []config.ImplementationPrefix, charsRead string) (bool, string, config.ImplementationPrefix) { + for _, pbt := range possibleBeginToken { + configToken := "" + pbtWithTokenSep := string(pbt[len(charsRead):]) + l.config.TokenSeparator() + for i := 0; i < len(pbtWithTokenSep); i++ { + configToken += string(l.peekChar()) + l.readChar() + } + + if configToken == pbtWithTokenSep { + return true, charsRead + configToken, pbt + } + l.resetAfterPeek(len(pbtWithTokenSep)) + } + return false, "", "" +} + +// resetAfterPeek will go back specified amount on the cursor +func (l *Lexer) resetAfterPeek(back int) { + l.position = l.position - back + l.readPosition = l.readPosition - back +} + +// isText only deals with any text characters defined as +// outside of the capture group +func isText(ch byte) bool { + return !nonText[string(ch)] +} + +func newToken(tokenType config.TokenType, ch byte) config.Token { + return config.Token{Type: tokenType, Literal: string(ch)} +} diff --git a/internal/lexer/lexer_test.go b/internal/lexer/lexer_test.go new file mode 100644 index 0000000..97f6ba0 --- /dev/null +++ b/internal/lexer/lexer_test.go @@ -0,0 +1,99 @@ +package lexer_test + +import ( + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/lexer" +) + +func Test_Lexer_NextToken(t *testing.T) { + input := `foo stuyfsdfsf +foo=AWSPARAMSTR:///path|keyAWSSECRETS:///foo +META_INCLUDED=VAULT://baz/bar/123|key1.prop2[role=arn:aws:iam::1111111:role,version=1082313] +` + ttests := []struct { + expectedType config.TokenType + expectedLiteral string + }{ + {config.TEXT, "foo"}, + {config.SPACE, " "}, + {config.TEXT, "stuyfsdfsf"}, + {config.NEW_LINE, "\n"}, + {config.TEXT, "foo"}, + {config.EQUALS, "="}, + {config.BEGIN_CONFIGMANAGER_TOKEN, "AWSPARAMSTR://"}, + {config.FORWARD_SLASH, "/"}, + {config.TEXT, "path"}, + {config.CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR, "|"}, + {config.TEXT, "key"}, + {config.BEGIN_CONFIGMANAGER_TOKEN, "AWSSECRETS://"}, + {config.FORWARD_SLASH, "/"}, + {config.TEXT, "foo"}, + {config.NEW_LINE, "\n"}, + {config.TEXT, "MET"}, + {config.TEXT, "A"}, + {config.TEXT, "_INCLUDED"}, + // {config.TEXT, "U"}, + // {config.TEXT, "DED"}, + {config.EQUALS, "="}, + {config.BEGIN_CONFIGMANAGER_TOKEN, "VAULT://"}, + {config.TEXT, "baz"}, + {config.FORWARD_SLASH, "/"}, + {config.TEXT, "bar"}, + {config.FORWARD_SLASH, "/"}, + {config.TEXT, "123"}, + {config.CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR, "|"}, + {config.TEXT, "key1"}, + {config.DOT, "."}, + {config.TEXT, "prop2"}, + {config.BEGIN_META_CONFIGMANAGER_TOKEN, "["}, + {config.TEXT, "role"}, + {config.EQUALS, "="}, + {config.TEXT, "arn"}, + {config.COLON, ":"}, + {config.TEXT, "aws"}, + {config.COLON, ":"}, + {config.TEXT, "iam"}, + {config.COLON, ":"}, + {config.COLON, ":"}, + {config.TEXT, "1111111"}, + {config.COLON, ":"}, + {config.TEXT, "role"}, + {config.COMMA, ","}, + {config.TEXT, "version"}, + {config.EQUALS, "="}, + {config.TEXT, "1082313"}, + {config.END_META_CONFIGMANAGER_TOKEN, "]"}, + {config.NEW_LINE, "\n"}, + {config.EOF, ""}, + } + + l := lexer.New(lexer.Source{Input: input, FullPath: "/foo/bar", FileName: "bar"}, *config.NewConfig()) + + for i, tt := range ttests { + + tok := l.NextToken() + if tok.Type != tt.expectedType { + t.Fatalf("tests[%d] - tokentype wrong. got=%q, expected=%q", + i, tok.Type, tt.expectedType) + } + + if tok.Literal != tt.expectedLiteral { + t.Fatalf("tests[%d] - literal wrong. got=%q, expected=%q", + i, tok.Literal, tt.expectedLiteral) + } + if tok.Type == config.BEGIN_CONFIGMANAGER_TOKEN { + + } + } +} + +func Test_empty_file(t *testing.T) { + input := `` + l := lexer.New(lexer.Source{Input: input, FullPath: "/foo/bar", FileName: "bar"}, *config.NewConfig()) + tok := l.NextToken() + if tok.Type != config.EOF { + t.Fatal("expected EOF") + } +} diff --git a/internal/log/log_test.go b/internal/log/log_test.go index be24a28..10922fa 100644 --- a/internal/log/log_test.go +++ b/internal/log/log_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func Test_LogInfo(t *testing.T) { diff --git a/internal/parser/doc.go b/internal/parser/doc.go new file mode 100644 index 0000000..c33ca52 --- /dev/null +++ b/internal/parser/doc.go @@ -0,0 +1,5 @@ +// Package parser +// Analyses a given string of text and extracts any configmanager tokens +// +// It builds any additiona metadata as part of the analysis +package parser diff --git a/internal/parser/parser.go b/internal/parser/parser.go new file mode 100644 index 0000000..b785dc2 --- /dev/null +++ b/internal/parser/parser.go @@ -0,0 +1,266 @@ +package parser + +import ( + "errors" + "fmt" + "os" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/lexer" + "github.com/DevLabFoundry/configmanager/v3/internal/log" +) + +func wrapErr(incompleteToken *config.ParsedTokenConfig, sanitized string, line, position int, etyp error) error { + return fmt.Errorf("\n- token: (%s%s%s) on line: %d column: %d] %w", incompleteToken.Prefix(), incompleteToken.TokenSeparator(), sanitized, line, position, etyp) +} + +var ( + ErrNoEndTagFound = errors.New("no corresponding end tag found") + ErrUnableToReplaceVarPlaceholder = errors.New("variable specified in the content was not found in the environment") +) + +type ConfigManagerTokenBlock struct { + BeginToken config.Token + ParsedToken config.ParsedTokenConfig + EndToken config.Token +} + +type Parser struct { + l *lexer.Lexer + errors []error + log log.ILogger + currentToken config.Token + peekToken config.Token + config *config.GenVarsConfig + environ []string +} + +func New(l *lexer.Lexer, c *config.GenVarsConfig) *Parser { + p := &Parser{ + l: l, + log: log.New(os.Stderr), + errors: []error{}, + config: c, + environ: os.Environ(), + } + + // Read two tokens, so curToken and peekToken are both set + // first one sets the curToken to the value of peekToken - + // which at this point is just the first upcoming token + p.nextToken() + // second one sets the curToken to the actual value of the first upcoming + // token and peekToken is the actual second upcoming token + p.nextToken() + + return p +} + +func (p *Parser) WithLogger(logger log.ILogger) *Parser { + p.log = nil //speed up GC + p.log = logger + return p +} + +// Parse creates a flat list of ConfigManagerTokenBlock +// In the order they were declared in the source text +// +// The parser does not do a second pass and interprets the source from top to bottom +func (p *Parser) Parse() ([]ConfigManagerTokenBlock, []error) { + stmts := []ConfigManagerTokenBlock{} + + for !p.currentTokenIs(config.EOF) { + if p.currentTokenIs(config.BEGIN_CONFIGMANAGER_TOKEN) { + // continues to read the tokens until it hits an end token or errors + configManagerToken, err := config.NewToken(p.currentToken.ImpPrefix, *p.config) + if err != nil { + return nil, []error{err} + } + if stmt := p.buildConfigManagerTokenFromBlocks(configManagerToken); stmt != nil { + stmts = append(stmts, *stmt) + } + } + p.nextToken() + } + + return stmts, p.errors +} + +func (p *Parser) nextToken() { + p.currentToken = p.peekToken + p.peekToken = p.l.NextToken() +} + +func (p *Parser) currentTokenIs(t config.TokenType) bool { + return p.currentToken.Type == t +} + +func (p *Parser) peekTokenIs(t config.TokenType) bool { + return p.peekToken.Type == t +} + +func (p *Parser) peekTokenIsEnd() bool { + endTokens := map[config.TokenType]bool{ + config.AT_SIGN: true, config.QUESTION_MARK: true, config.COLON: true, + config.SLASH_QUESTION_MARK: true, config.EOF: true, + // traditional ends of tokens + config.DOUBLE_QUOTE: true, config.SINGLE_QUOTE: true, config.SPACE: true, + config.NEW_LINE: true, + } + return endTokens[p.peekToken.Type] +} + +// buildConfigManagerTokenFromBlocks +func (p *Parser) buildConfigManagerTokenFromBlocks(configManagerToken *config.ParsedTokenConfig) *ConfigManagerTokenBlock { + currentToken := p.currentToken + stmt := &ConfigManagerTokenBlock{BeginToken: currentToken} + + // move past current token + p.nextToken() + + // built as part of the below parser + sanitizedToken := "" + + // stop on end of file + for !p.peekTokenIs(config.EOF) { + // // This is the target state when there is an optional token wrapping + // // e.g. `{{ IMP://path }}` + // // currently this is untestable + // if p.peekTokenIs(config.END_CONFIGMANAGER_TOKEN) { + // notFoundEnd = false + // fullToken += p.curToken.Literal + // sanitizedToken += p.curToken.Literal + // stmt.EndToken = p.curToken + // break + // } + + // when next token is another token + // i.e. the tokens are adjacent + if p.peekTokenIs(config.BEGIN_CONFIGMANAGER_TOKEN) { + sanitizedToken += p.currentToken.Literal + stmt.EndToken = p.currentToken + break + } + + // reached the end of token + if p.peekTokenIsEnd() { + sanitizedToken += p.currentToken.Literal + stmt.EndToken = p.currentToken + break + } + + //sample token will be consumed like this + // AWSSECRETS:///path/to/my/key|lookup.Inside.Object[meta=data] + // + // everything is token path until (if any key separator exists) + // check key separator this marks the end of a normal token path + // + // keyLookup and Metadata are optional - is always specified in that order + if p.currentTokenIs(config.CONFIGMANAGER_TOKEN_KEY_PATH_SEPARATOR) { + if err := p.buildKeyPathSeparator(configManagerToken); err != nil { + p.errors = append(p.errors, wrapErr(configManagerToken, sanitizedToken, currentToken.Line, currentToken.Column, err)) + return nil + } + // keyPath would have built the keyPath and metadata if any + break + } + + // optionally at the end of the path without key separator + // check metadata there can be a metadata bracket `[key=val,k1=v2]` + if p.currentTokenIs(config.BEGIN_META_CONFIGMANAGER_TOKEN) { + if err := p.buildMetadata(configManagerToken); err != nil { + p.errors = append(p.errors, wrapErr(configManagerToken, sanitizedToken, currentToken.Line, currentToken.Column, err)) + return nil + } + break + } + + sanitizedToken += p.currentToken.Literal + + // when the next token is EOF + // we want set the current token + // else it would be lost once the parser is advanced below + p.nextToken() + if p.peekTokenIs(config.EOF) { + sanitizedToken += p.currentToken.Literal + stmt.EndToken = p.currentToken + break + } + } + + configManagerToken.WithSanitizedToken(sanitizedToken) + stmt.ParsedToken = *configManagerToken + + return stmt +} + +// buildKeyPathSeparator already advanced to the first token +func (p *Parser) buildKeyPathSeparator(configManagerToken *config.ParsedTokenConfig) error { + // advance to next token i.e. post the path separator + p.nextToken() + keyPath := "" + if p.peekTokenIs(config.EOF) { + // if the next token EOF we set the path as current token and exit + // otherwise we would never hit the below loop + configManagerToken.WithKeyPath(p.currentToken.Literal) + return nil + } + for !p.peekTokenIs(config.EOF) { + if p.peekTokenIs(config.BEGIN_META_CONFIGMANAGER_TOKEN) { + // add current token to the keysPath and move onto the metadata + keyPath += p.currentToken.Literal + p.nextToken() + if err := p.buildMetadata(configManagerToken); err != nil { + return err + } + break + } + // touching another token or end of token + if p.peekTokenIs(config.BEGIN_CONFIGMANAGER_TOKEN) || p.peekTokenIsEnd() { + keyPath += p.currentToken.Literal + break + } + keyPath += p.currentToken.Literal + p.nextToken() + if p.peekTokenIs(config.EOF) { + // check if the next token is EOF once advanced + // if it is we want to consume current token else it will be skipped + keyPath += p.currentToken.Literal + break + } + } + configManagerToken.WithKeyPath(keyPath) + return nil +} + +var ErrMetadataEmpty = errors.New("emtpy metadata") + +// buildMetadata adds metadata to the ParsedTokenConfig +func (p *Parser) buildMetadata(configManagerToken *config.ParsedTokenConfig) error { + metadata := "" + found := false + if p.peekTokenIs(config.END_META_CONFIGMANAGER_TOKEN) { + return fmt.Errorf("%w, metadata brackets must include at least one set of key=value pairs", ErrMetadataEmpty) + } + p.nextToken() + for !p.peekTokenIs(config.EOF) { + if p.peekTokenIsEnd() { + // next token is an end of token but no closing `]` found + return fmt.Errorf("%w, metadata (%s) string has no closing", ErrNoEndTagFound, metadata) + } + if p.peekTokenIs(config.END_META_CONFIGMANAGER_TOKEN) { + metadata += p.currentToken.Literal + found = true + p.nextToken() + break + } + metadata += p.currentToken.Literal + p.nextToken() + } + configManagerToken.WithMetadata(metadata) + + if !found { + // hit the end of file and no end tag found + return fmt.Errorf("%w, metadata string has no closing", ErrNoEndTagFound) + } + return nil +} diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go new file mode 100644 index 0000000..2a97c04 --- /dev/null +++ b/internal/parser/parser_test.go @@ -0,0 +1,304 @@ +package parser_test + +import ( + "errors" + "os" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/lexer" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/parser" + "github.com/DevLabFoundry/configmanager/v3/internal/store" +) + +var lexerSource = lexer.Source{FileName: "bar", FullPath: "/foo/bar"} + +func Test_ParserBlocks(t *testing.T) { + ttests := map[string]struct { + input string + // prefix,path,keyLookup + expected [][3]string + }{ + "tokens touching each other in source after key path": { + `foo stuyfsdfsf + foo=AWSPARAMSTR:///path|keyAWSSECRETS:///foo + other text her + BAR=something + `, [][3]string{ + {string(config.ParamStorePrefix), "/path", "key"}, + {string(config.SecretMgrPrefix), "/foo", ""}, + }}, + "full URL of tokens": { + `foo stuyfsdfsf + foo=proto://AWSPARAMSTR:///config|user:AWSSECRETS:///creds|password@AWSPARAMSTR:///config|endpoint:AWSPARAMSTR:///config|port/?queryParam1=123&queryParam2=AWSPARAMSTR:///config|qp2 + # some comment + BAR=something + `, [][3]string{ + {string(config.ParamStorePrefix), "/config", "user"}, + {string(config.SecretMgrPrefix), "/creds", "password"}, + {string(config.ParamStorePrefix), "/config", "endpoint"}, + {string(config.ParamStorePrefix), "/config", "port"}, + {string(config.ParamStorePrefix), "/config", "qp2"}, + }, + }, + "tokens touching each other in source after metadata": { + `foo stuyfsdfsf + foo=AWSPARAMSTR:///path|key[meta=val]AWSSECRETS:///foo + other text her + BAR=something + `, [][3]string{ + {string(config.ParamStorePrefix), "/path", "key"}, + {string(config.SecretMgrPrefix), "/foo", ""}, + }, + }, + "tokens touching each other in source": { + `foo stuyfsdfsf GCFOO VAbarAWbuX AZmore + foo=AWSPARAMSTR:///pathAWSSECRETS:///foo + other text her + BAR=something + `, [][3]string{ + {string(config.ParamStorePrefix), "/path", ""}, + {string(config.SecretMgrPrefix), "/foo", ""}, + }, + }, + "touching EOF single token": { + `AWSPARAMSTR:///config|qp2`, + [][3]string{ + {string(config.ParamStorePrefix), "/config", "qp2"}, + }, + }, + "touching EOF multi token": { + `proto://AWSPARAMSTR:///config|user:AWSSECRETS:///creds|password@AWSPARAMSTR:///config|endpoint:AWSPARAMSTR:///config|port/?queryParam1=123&queryParam2=AWSPARAMSTR:///config|qp2`, + [][3]string{ + {string(config.ParamStorePrefix), "/config", "user"}, + {string(config.SecretMgrPrefix), "/creds", "password"}, + {string(config.ParamStorePrefix), "/config", "endpoint"}, + {string(config.ParamStorePrefix), "/config", "port"}, + {string(config.ParamStorePrefix), "/config", "qp2"}, + }, + }, + } + + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + lexerSource.Input = tt.input + l := lexer.New(lexerSource, *config.NewConfig()) + p := parser.New(l, config.NewConfig()).WithLogger(log.New(os.Stderr)) + parsed, errs := p.Parse() + if len(errs) > 0 { + t.Fatalf("parser had errors, expected \nerror: %v", errs) + } + + if len(parsed) != len(tt.expected) { + t.Fatalf("parsed statements count does not match\ngot=%d want=%d\nparsed %q", + len(parsed), + len(tt.expected), + parsed) + } + + for idx, stmt := range parsed { + if !testHelperGenDocBlock(t, stmt, config.ImplementationPrefix(tt.expected[idx][0]), tt.expected[idx][1], tt.expected[idx][2]) { + return + } + } + }) + } +} + +func Test_Parse_should_fail_on_metadata(t *testing.T) { + ttests := map[string]struct { + input string + errTyp error + }{ + "when _end_tag_found without keysPath": { + `AWSSECRETS:///foo[version=1.2.3`, + parser.ErrNoEndTagFound, + }, + "when _end_tag_found with keysPath": { + `AWSSECRETS:///foo|path.one[version=1.2.3`, + parser.ErrNoEndTagFound, + }, + "when _end_tag_found with keysPath in the middle": { + `AWSSECRETS:///foo|path.one[version=1.2.3 + more content here +`, + parser.ErrNoEndTagFound, + }, + "when no metadata has been supplied": { + `AWSSECRETS:///foo|path.one[]`, + parser.ErrMetadataEmpty, + }, + "when no metadata has been supplied - without key path": { + `AWSSECRETS:///foo[]`, + parser.ErrMetadataEmpty, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + lexerSource.Input = tt.input + cfg := config.NewConfig() + l := lexer.New(lexerSource, *cfg) + p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) + _, errs := p.Parse() + if len(errs) != 1 { + t.Fatalf("unexpected number of errors\n got: %v, wanted: 1", errs) + } + if !errors.Is(errs[0], tt.errTyp) { + t.Errorf("unexpected error type\n got: %T, wanted: %T", errs, parser.ErrNoEndTagFound) + } + }) + } +} + +func Test_Parse_should_pass_with_metadata_end_tag(t *testing.T) { + ttests := map[string]struct { + input string + metdataStr string + }{ + "without keysPath": { + `AWSSECRETS:///foo[version=1.2.3]`, + `version=1.2.3`, + }, + "without keysPath in the middle of content": { + `AWSSECRETS:///foo[version=1.2.3] +`, + `version=1.2.3`, + }, + "with keysPath": { + `AWSSECRETS:///foo|path.one[version=1.2.3]`, + `version=1.2.3`, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + lexerSource.Input = tt.input + cfg := config.NewConfig() + l := lexer.New(lexerSource, *cfg) + p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) + parsed, errs := p.Parse() + if len(errs) > 0 { + t.Fatalf("unexpected number of errors\n got: %v, wanted: 0", errs) + } + for _, prsd := range parsed { + prsd.ParsedToken.LookupKeys() + + } + }) + } +} + +func Test_Parse_ParseMetadata(t *testing.T) { + + ttests := map[string]struct { + input string + typ *store.SecretsMgrConfig + }{ + "without keysPath": { + `AWSSECRETS:///foo[version=1.2.3]`, + &store.SecretsMgrConfig{}, + }, + "with keysPath": { + `AWSSECRETS:///foo|path.one[version=1.2.3]`, + &store.SecretsMgrConfig{}, + }, + "nestled in text": { + `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, + &store.SecretsMgrConfig{}, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + lexerSource.Input = tt.input + cfg := config.NewConfig() + l := lexer.New(lexerSource, *cfg) + p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) + parsed, errs := p.Parse() + if len(errs) > 0 { + t.Fatalf("%v", errs) + } + + for _, p := range parsed { + if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { + t.Fatal(err) + } + if tt.typ.Version != "1.2.3" { + t.Errorf("got %v wanted 1.2.3", tt.typ.Version) + } + } + }) + } +} + +func Test_Parse_Path_Keys_WithParsedMetadat(t *testing.T) { + + ttests := map[string]struct { + input string + typ *store.SecretsMgrConfig + wantSanitizedPath string + wantKeyPath string + }{ + "without keysPath": { + `AWSSECRETS:///foo[version=1.2.3]`, + &store.SecretsMgrConfig{}, + "/foo", "", + }, + "with keysPath": { + `AWSSECRETS:///foo|path.one[version=1.2.3]`, + &store.SecretsMgrConfig{}, + "/foo", "path.one", + }, + "nestled in text": { + `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, + &store.SecretsMgrConfig{}, + "/path/queryparam", "p1", + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + lexerSource.Input = tt.input + cfg := config.NewConfig() + l := lexer.New(lexerSource, *cfg) + p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) + parsed, errs := p.Parse() + if len(errs) > 0 { + t.Fatalf("%v", errs) + } + + for _, p := range parsed { + if p.ParsedToken.StoreToken() != tt.wantSanitizedPath { + t.Errorf("got %s want %s", p.ParsedToken.StoreToken(), tt.wantSanitizedPath) + } + if p.ParsedToken.LookupKeys() != tt.wantKeyPath { + t.Errorf("got %s want %s", p.ParsedToken.LookupKeys(), tt.wantKeyPath) + } + if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { + t.Fatal(err) + } + if tt.typ.Version != "1.2.3" { + t.Errorf("got %v wanted 1.2.3", tt.typ.Version) + } + } + }) + } +} + +func testHelperGenDocBlock(t *testing.T, stmtBlock parser.ConfigManagerTokenBlock, tokenType config.ImplementationPrefix, tokenValue, keysLookupPath string) bool { + t.Helper() + if stmtBlock.ParsedToken.Prefix() != tokenType { + t.Errorf("got=%q, wanted stmtBlock.ImpPrefix = '%v'.", stmtBlock.ParsedToken.Prefix(), tokenType) + return false + } + + if stmtBlock.ParsedToken.StoreToken() != tokenValue { + t.Errorf("token StoreToken got=%s, wanted=%s", stmtBlock.ParsedToken.StoreToken(), tokenValue) + return false + } + + if stmtBlock.ParsedToken.LookupKeys() != keysLookupPath { + t.Errorf("token LookupKeys. got=%s, wanted=%s", stmtBlock.ParsedToken.LookupKeys(), keysLookupPath) + return false + } + + return true +} diff --git a/internal/store/azappconf.go b/internal/store/azappconf.go index a37cc8a..c35f538 100644 --- a/internal/store/azappconf.go +++ b/internal/store/azappconf.go @@ -11,8 +11,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" ) // appConfApi @@ -51,8 +51,8 @@ func NewAzAppConf(ctx context.Context, token *config.ParsedTokenConfig, logger l token: token, logger: logger, } - srvInit := azServiceFromToken(token.StoreToken(), "https://%s.azconfig.io", 1) - backingStore.strippedToken = srvInit.token + srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.azconfig.io", 1) + backingStore.strippedToken = srvInit.Token cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -60,7 +60,7 @@ func NewAzAppConf(ctx context.Context, token *config.ParsedTokenConfig, logger l return nil, err } - c, err := azappconfig.NewClient(srvInit.serviceUri, cred, nil) + c, err := azappconfig.NewClient(srvInit.ServiceUri, cred, nil) if err != nil { logger.Error("failed to init the client: %v", err) return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) @@ -71,6 +71,10 @@ func NewAzAppConf(ctx context.Context, token *config.ParsedTokenConfig, logger l } +func (s *AzAppConf) WithSvc(svc appConfApi) { + s.svc = svc +} + // setTokenVal sets the token func (implmt *AzAppConf) SetToken(token *config.ParsedTokenConfig) {} @@ -78,7 +82,7 @@ func (implmt *AzAppConf) SetToken(token *config.ParsedTokenConfig) {} // label can be specified // From this point then normal rules of configmanager apply, // including keySeperator and lookup. -func (imp *AzAppConf) Token() (string, error) { +func (imp *AzAppConf) Value() (string, error) { imp.logger.Info("Concrete implementation AzAppConf") imp.logger.Info("AzAppConf Token: %s", imp.token.String()) diff --git a/internal/store/azappconf_test.go b/internal/store/azappconf_test.go index 82ed7e9..17da526 100644 --- a/internal/store/azappconf_test.go +++ b/internal/store/azappconf_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "bytes" @@ -8,9 +8,10 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - logger "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + logger "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func azAppConfCommonChecker(t *testing.T, key string, expectedKey string, expectLabel string, opts *azappconfig.GetSettingOptions) { @@ -36,20 +37,25 @@ func (m mockAzAppConfApi) GetSetting(ctx context.Context, key string, options *a } func Test_AzAppConf_Success(t *testing.T) { - t.Parallel() tsuccessParam := "somecvla" logr := logger.New(&bytes.Buffer{}) tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) appConfApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzAppConfApi }{ "successVal": { - "AZAPPCONF#/test-app-config-instance/table//token/1", + func() *config.ParsedTokenConfig { + // "AZAPPCONF#/test-app-config-instance/table//token/1", + tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-app-config-instance/table//token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, tsuccessParam, - func(t *testing.T) appConfApi { + func(t *testing.T) mockAzAppConfApi { return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { azAppConfCommonChecker(t, key, "table//token/1", "", options) resp := azappconfig.GetSettingResponse{} @@ -57,12 +63,18 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, "successVal with :// token Separator": { - "AZAPPCONF:///test-app-config-instance/conf_key[label=dev]", + func() *config.ParsedTokenConfig { + // "AZAPPCONF:///test-app-config-instance/conf_key[label=dev]", + tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://")) + tkn.WithSanitizedToken("/test-app-config-instance/conf_key") + tkn.WithKeyPath("") + tkn.WithMetadata("label=dev") + return tkn + }, tsuccessParam, - func(t *testing.T) appConfApi { + func(t *testing.T) mockAzAppConfApi { return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { azAppConfCommonChecker(t, key, "conf_key", "dev", options) resp := azappconfig.GetSettingResponse{} @@ -70,12 +82,17 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), }, "successVal with :// token Separator and etag specified": { - "AZAPPCONF:///test-app-config-instance/conf_key[label=dev,etag=sometifdsssdsfdi_string01209222]", + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-app-config-instance/conf_key") + tkn.WithKeyPath("") + tkn.WithMetadata("label=dev,etag=sometifdsssdsfdi_string01209222") + return tkn + }, tsuccessParam, - func(t *testing.T) appConfApi { + func(t *testing.T) mockAzAppConfApi { return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { azAppConfCommonChecker(t, key, "conf_key", "dev", options) if !options.OnlyIfChanged.Equals("sometifdsssdsfdi_string01209222") { @@ -86,12 +103,17 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), }, "successVal with keyseparator but no val returned": { - "AZAPPCONF#/test-app-config-instance/try_to_find|key_separator.lookup", + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-app-config-instance/try_to_find") + tkn.WithKeyPath("key_separator.lookup") + tkn.WithMetadata("") + return tkn + }, "", - func(t *testing.T) appConfApi { + func(t *testing.T) mockAzAppConfApi { return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { azAppConfCommonChecker(t, key, "try_to_find", "", options) resp := azappconfig.GetSettingResponse{} @@ -99,21 +121,18 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - - impl, err := NewAzAppConf(context.TODO(), token, logr) + impl, err := store.NewAzAppConf(context.TODO(), tt.token(), logr) if err != nil { t.Errorf("failed to init AZAPPCONF") } - impl.svc = tt.mockClient(t) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -129,39 +148,41 @@ func Test_AzAppConf_Success(t *testing.T) { } func Test_AzAppConf_Error(t *testing.T) { - t.Parallel() - logr := logger.New(&bytes.Buffer{}) tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect error - mockClient func(t *testing.T) appConfApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzAppConfApi }{ "errored on service method call": { - "AZAPPCONF#/test-app-config-instance/table/token/ok", - ErrRetrieveFailed, - func(t *testing.T) appConfApi { + func() *config.ParsedTokenConfig { + // "AZAPPCONF#/test-app-config-instance/table/token/ok", + tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-app-config-instance/table/token/ok") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + store.ErrRetrieveFailed, + func(t *testing.T) mockAzAppConfApi { return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { t.Helper() resp := azappconfig.GetSettingResponse{} return resp, fmt.Errorf("network error") }) }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - impl, err := NewAzAppConf(context.TODO(), token, logr) + impl, err := store.NewAzAppConf(context.TODO(), tt.token(), logr) if err != nil { t.Fatal("failed to init AZAPPCONF") } - impl.svc = tt.mockClient(t) - if _, err := impl.Token(); !errors.Is(err, tt.expect) { + impl.WithSvc(tt.mockClient(t)) + if _, err := impl.Value(); !errors.Is(err, tt.expect) { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) } }) @@ -169,19 +190,19 @@ func Test_AzAppConf_Error(t *testing.T) { } func Test_fail_AzAppConf_Client_init(t *testing.T) { - t.Parallel() logr := logger.New(&bytes.Buffer{}) // this is basically a wrap around test for the url.Parse method in the stdlib // as that is what the client uses under the hood - token, _ := config.NewParsedTokenConfig("AZAPPCONF:///%25%65%6e%301-._~/") } - if !errors.Is(err, ErrClientInitialization) { - t.Fatalf(testutils.TestPhraseWithContext, "azappconf client init", err.Error(), ErrClientInitialization.Error()) + if !errors.Is(err, store.ErrClientInitialization) { + t.Fatalf(testutils.TestPhraseWithContext, "azappconf client init", err.Error(), store.ErrClientInitialization.Error()) } } diff --git a/internal/store/azhelpers.go b/internal/store/azhelpers.go index 7b85387..29e66e8 100644 --- a/internal/store/azhelpers.go +++ b/internal/store/azhelpers.go @@ -8,13 +8,14 @@ import ( /* Generic Azure Service Init Helpers */ -// azServiceHelper returns a service URI and the stripped token -type azServiceHelper struct { - serviceUri string - token string + +// AzServiceHelper returns a service URI and the stripped token +type AzServiceHelper struct { + ServiceUri string + Token string } -// azServiceFromToken for azure the first part of the token __must__ always be the +// AzServiceFromToken for azure the first part of the token __must__ always be the // identifier of the service e.g. the account name for tableStore or the Vault name for KVSecret or // AppConfig instance // take parameter specifies the number of elements to take from the start only @@ -22,7 +23,7 @@ type azServiceHelper struct { // e.g. a value of 2 for take will take first 2 elements from the slices // // For AppConfig or KeyVault we ONLY need the AppConfig instance or KeyVault instance name -func azServiceFromToken(token string, formatUri string, take int) azServiceHelper { +func AzServiceFromToken(token string, formatUri string, take int) AzServiceHelper { // ensure preceding slash is trimmed stringToken := strings.Split(strings.TrimPrefix(token, "/"), "/") splitToken := []any{} @@ -32,5 +33,5 @@ func azServiceFromToken(token string, formatUri string, take int) azServiceHelpe } uri := fmt.Sprintf(formatUri, splitToken[0:take]...) - return azServiceHelper{serviceUri: uri, token: strings.Join(stringToken[take:], "/")} + return AzServiceHelper{ServiceUri: uri, Token: strings.Join(stringToken[take:], "/")} } diff --git a/internal/store/azkeyvault.go b/internal/store/azkeyvault.go index 84f1715..781b066 100644 --- a/internal/store/azkeyvault.go +++ b/internal/store/azkeyvault.go @@ -8,8 +8,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" ) type kvApi interface { @@ -45,8 +45,8 @@ func NewKvScrtStore(ctx context.Context, token *config.ParsedTokenConfig, logger token: token, } - srvInit := azServiceFromToken(token.StoreToken(), "https://%s.vault.azure.net", 1) - backingStore.strippedToken = srvInit.token + srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.vault.azure.net", 1) + backingStore.strippedToken = srvInit.Token cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -54,7 +54,7 @@ func NewKvScrtStore(ctx context.Context, token *config.ParsedTokenConfig, logger return nil, err } - c, err := azsecrets.NewClient(srvInit.serviceUri, cred, nil) + c, err := azsecrets.NewClient(srvInit.ServiceUri, cred, nil) if err != nil { logger.Error("%v\n%w", err, ErrClientInitialization) return nil, err @@ -65,10 +65,14 @@ func NewKvScrtStore(ctx context.Context, token *config.ParsedTokenConfig, logger } +func (s *KvScrtStore) WithSvc(svc kvApi) { + s.svc = svc +} + // setToken already happens in AzureKVClient in the constructor func (implmt *KvScrtStore) SetToken(token *config.ParsedTokenConfig) {} -func (imp *KvScrtStore) Token() (string, error) { +func (imp *KvScrtStore) Value() (string, error) { imp.logger.Info("Concrete implementation AzKeyVault Secret") imp.logger.Info("AzKeyVault Token: %s", imp.token.String()) diff --git a/internal/store/azkeyvault_test.go b/internal/store/azkeyvault_test.go index 98ccda8..35b5c7d 100644 --- a/internal/store/azkeyvault_test.go +++ b/internal/store/azkeyvault_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -8,58 +8,59 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func Test_azSplitToken(t *testing.T) { tests := []struct { name string token string - expect azServiceHelper + expect store.AzServiceHelper }{ { name: "simple_with_preceding_slash", token: "/test-vault/somejsontest", - expect: azServiceHelper{ - serviceUri: "https://test-vault.vault.azure.net", - token: "somejsontest", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-vault.vault.azure.net", + Token: "somejsontest", }, }, { name: "missing_initial_slash", token: "test-vault/somejsontest", - expect: azServiceHelper{ - serviceUri: "https://test-vault.vault.azure.net", - token: "somejsontest", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-vault.vault.azure.net", + Token: "somejsontest", }, }, { name: "missing_initial_slash_multislash_secretname", token: "test-vault/some/json/test", - expect: azServiceHelper{ - serviceUri: "https://test-vault.vault.azure.net", - token: "some/json/test", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-vault.vault.azure.net", + Token: "some/json/test", }, }, { name: "with_initial_slash_multislash_secretname", token: "test-vault//some/json/test", - expect: azServiceHelper{ - serviceUri: "https://test-vault.vault.azure.net", - token: "/some/json/test", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-vault.vault.azure.net", + Token: "/some/json/test", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := azServiceFromToken(tt.token, "https://%s.vault.azure.net", 1) - if got.token != tt.expect.token { - t.Errorf(testutils.TestPhrase, tt.expect.token, got.token) + got := store.AzServiceFromToken(tt.token, "https://%s.vault.azure.net", 1) + if got.Token != tt.expect.Token { + t.Errorf(testutils.TestPhrase, tt.expect.Token, got.Token) } - if got.serviceUri != tt.expect.serviceUri { - t.Errorf(testutils.TestPhrase, tt.expect.serviceUri, got.serviceUri) + if got.ServiceUri != tt.expect.ServiceUri { + t.Errorf(testutils.TestPhrase, tt.expect.ServiceUri, got.ServiceUri) } }) } @@ -93,82 +94,115 @@ func (m mockAzKvSecretApi) GetSecret(ctx context.Context, name string, version s } func TestAzKeyVault(t *testing.T) { - t.Parallel() - tsuccessParam := "dssdfdweiuyh" tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) kvApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzKvSecretApi }{ - "successVal": {"AZKVSECRET#/test-vault//token/1", tsuccessParam, func(t *testing.T) kvApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "/token/1") - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - }, - "successVal with version": {"AZKVSECRET#/test-vault//token/1[version:123]", tsuccessParam, func(t *testing.T) kvApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "/token/1") - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - }, - "successVal with keyseparator": {"AZKVSECRET#/test-vault/token/1|somekey", tsuccessParam, func(t *testing.T) kvApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + "successVal": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault//token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + tsuccessParam, func(t *testing.T) mockAzKvSecretApi { + return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + t.Helper() + azKvCommonGetSecretChecker(t, name, "", "/token/1") + resp := azsecrets.GetSecretResponse{} + resp.Value = &tsuccessParam + return resp, nil + }) + }, }, - "errored": {"AZKVSECRET#/test-vault/token/1|somekey", "unable to retrieve secret", func(t *testing.T) kvApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - return resp, fmt.Errorf("unable to retrieve secret") - }) + "successVal with version": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault//token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("version:123") + return tkn + }, tsuccessParam, func(t *testing.T) mockAzKvSecretApi { + return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + t.Helper() + azKvCommonGetSecretChecker(t, name, "", "/token/1") + resp := azsecrets.GetSecretResponse{} + resp.Value = &tsuccessParam + return resp, nil + }) + }, }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + "successVal with keyseparator": { + func() *config.ParsedTokenConfig { + // "AZKVSECRET#/test-vault/token/1|somekey" + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, tsuccessParam, func(t *testing.T) mockAzKvSecretApi { + return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + t.Helper() + azKvCommonGetSecretChecker(t, name, "", "token/1") + + resp := azsecrets.GetSecretResponse{} + resp.Value = &tsuccessParam + return resp, nil + }) + }, }, - "empty": {"AZKVSECRET#/test-vault/token/1|somekey", "", func(t *testing.T) kvApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - return resp, nil - }) + "errored": { + func() *config.ParsedTokenConfig { + // "AZKVSECRET#/test-vault/token/1|somekey" + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, + "unable to retrieve secret", + func(t *testing.T) mockAzKvSecretApi { + return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + t.Helper() + azKvCommonGetSecretChecker(t, name, "", "token/1") + + resp := azsecrets.GetSecretResponse{} + return resp, fmt.Errorf("unable to retrieve secret") + }) + }, }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + "empty": { + func() *config.ParsedTokenConfig { + // "AZKVSECRET#/test-vault/token/1|somekey" + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, "", func(t *testing.T) mockAzKvSecretApi { + return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + t.Helper() + azKvCommonGetSecretChecker(t, name, "", "token/1") + + resp := azsecrets.GetSecretResponse{} + return resp, nil + }) + }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - - impl, err := NewKvScrtStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewKvScrtStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { t.Errorf("failed to init azkvstore") } - impl.svc = tt.mockClient(t) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/internal/store/aztablestorage.go b/internal/store/aztablestorage.go index 539979b..eedef16 100644 --- a/internal/store/aztablestorage.go +++ b/internal/store/aztablestorage.go @@ -12,8 +12,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" ) var ErrIncorrectlyStructuredToken = errors.New("incorrectly structured token") @@ -52,8 +52,8 @@ func NewAzTableStore(ctx context.Context, token *config.ParsedTokenConfig, logge token: token, } - srvInit := azServiceFromToken(token.StoreToken(), "https://%s.table.core.windows.net/%s", 2) - backingStore.strippedToken = srvInit.token + srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.table.core.windows.net/%s", 2) + backingStore.strippedToken = srvInit.Token cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -61,7 +61,7 @@ func NewAzTableStore(ctx context.Context, token *config.ParsedTokenConfig, logge return nil, err } - c, err := aztables.NewClient(srvInit.serviceUri, cred, nil) + c, err := aztables.NewClient(srvInit.ServiceUri, cred, nil) if err != nil { logger.Error("failed to init the client: %v", err) return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) @@ -71,6 +71,10 @@ func NewAzTableStore(ctx context.Context, token *config.ParsedTokenConfig, logge return backingStore, nil } +func (s *AzTableStore) WithSvc(svc tableStoreApi) { + s.svc = svc +} + // setToken already happens in the constructor func (implmt *AzTableStore) SetToken(token *config.ParsedTokenConfig) {} @@ -79,7 +83,7 @@ func (implmt *AzTableStore) SetToken(token *config.ParsedTokenConfig) {} // // From this point then normal rules of configmanager apply, // including keySeperator and lookup. -func (imp *AzTableStore) Token() (string, error) { +func (imp *AzTableStore) Value() (string, error) { imp.logger.Info("AzTableSTore Token: %s", imp.token.String()) imp.logger.Info("Concrete implementation AzTableSTore") diff --git a/internal/store/aztablestorage_test.go b/internal/store/aztablestorage_test.go index 54006cc..892ee9e 100644 --- a/internal/store/aztablestorage_test.go +++ b/internal/store/aztablestorage_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -9,9 +9,10 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" ) func azTableStoreCommonChecker(t *testing.T, partitionKey, rowKey, expectedPartitionKey, expectedRowKey string) { @@ -41,55 +42,62 @@ func (m mockAzTableStoreApi) GetEntity(ctx context.Context, partitionKey string, func Test_AzTableStore_Success(t *testing.T) { tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) tableStoreApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzTableStoreApi }{ - "successVal": {"AZTABLESTORE#/test-account/table//token/1", "tsuccessParam", func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - resp := aztables.GetEntityResponse{} - resp.Value = []byte("tsuccessParam") - return resp, nil - }) - }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - }, - "successVal with :// token Separator": {"AZTABLESTORE:///test-account/table//token/1", "tsuccessParam", func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - resp := aztables.GetEntityResponse{} - resp.Value = []byte("tsuccessParam") - return resp, nil - }) - }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), + "successVal": { + func() *config.ParsedTokenConfig { + // "AZTABLESTORE#/test-account/table//token/1" + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-account/table//token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "tsuccessParam", func(t *testing.T) mockAzTableStoreApi { + return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + t.Helper() + azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") + resp := aztables.GetEntityResponse{} + resp.Value = []byte("tsuccessParam") + return resp, nil + }) + }, }, - "successVal with keyseparator but no val returned": {"AZTABLESTORE#/test-account/table/token/1|somekey", "", func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") + // "successVal with :// token Separator": {"AZTABLESTORE:///test-account/table//token/1", "tsuccessParam", func(t *testing.T) tableStoreApi { + // return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + // t.Helper() + // azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") + // resp := aztables.GetEntityResponse{} + // resp.Value = []byte("tsuccessParam") + // return resp, nil + // }) + // }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), + // }, + // "successVal with keyseparator but no val returned": {"AZTABLESTORE#/test-account/table/token/1|somekey", "", func(t *testing.T) tableStoreApi { + // return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + // t.Helper() + // azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - resp := aztables.GetEntityResponse{} - resp.Value = nil - return resp, nil - }) - }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - }, + // resp := aztables.GetEntityResponse{} + // resp.Value = nil + // return resp, nil + // }) + // }, + // config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + // }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - impl, err := NewAzTableStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { t.Errorf("failed to init aztablestore") } - impl.svc = tt.mockClient(t) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -105,75 +113,95 @@ func Test_AzTableStore_Success(t *testing.T) { } func Test_azstorage_with_value_property(t *testing.T) { - t.Parallel() + conf := config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://") ttests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) tableStoreApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzTableStoreApi }{ "return value property with json like object": { - "AZTABLESTORE:///test-account/table/partitionkey/rowKey|host", + func() *config.ParsedTokenConfig { + // "AZTABLESTORE:///test-account/table/partitionkey/rowKey|host", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) + tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") + tkn.WithKeyPath("host") + return tkn + }, "map[bool:true host:foo port:1234]", - func(t *testing.T) tableStoreApi { + func(t *testing.T) mockAzTableStoreApi { return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { t.Helper() resp := aztables.GetEntityResponse{Value: []byte(`{"value":{"host":"foo","port":1234,"bool":true}}`)} return resp, nil }) }, - conf, }, "return value property with string only": { - "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + func() *config.ParsedTokenConfig { + // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) + tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") + // tkn.WithKeyPath("host") + // tkn.WithMetadata("version:123]") + return tkn + }, "foo.bar.com", - func(t *testing.T) tableStoreApi { + func(t *testing.T) mockAzTableStoreApi { return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { t.Helper() resp := aztables.GetEntityResponse{Value: []byte(`{"value":"foo.bar.com"}`)} return resp, nil }) }, - conf, }, "return value property with numeric only": { - "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + func() *config.ParsedTokenConfig { + // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) + tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") + // tkn.WithKeyPath("host") + // tkn.WithMetadata("version:123]") + return tkn + }, "1234", - func(t *testing.T) tableStoreApi { + func(t *testing.T) mockAzTableStoreApi { return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { t.Helper() resp := aztables.GetEntityResponse{Value: []byte(`{"value":1234}`)} return resp, nil }) }, - conf, }, "return value property with boolean only": { - "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + func() *config.ParsedTokenConfig { + // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) + tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") + return tkn + }, "false", - func(t *testing.T) tableStoreApi { + func(t *testing.T) mockAzTableStoreApi { return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { t.Helper() resp := aztables.GetEntityResponse{Value: []byte(`{"value":false}`)} return resp, nil }) }, - conf, }, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + // token, _ := config.NewToken(tt.token(), *tt.config) - impl, err := NewAzTableStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { t.Fatal("failed to init aztablestore") } - impl.svc = tt.mockClient(t) + impl.WithSvc(tt.mockClient(t)) - got, err := impl.Token() + got, err := impl.Value() if err != nil { t.Fatalf(testutils.TestPhrase, err.Error(), nil) } @@ -186,55 +214,71 @@ func Test_azstorage_with_value_property(t *testing.T) { } func Test_AzTableStore_Error(t *testing.T) { - t.Parallel() tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect error - mockClient func(t *testing.T) tableStoreApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockAzTableStoreApi }{ - "errored on token parsing to partiationKey": {"AZTABLESTORE#/test-vault/token/1|somekey", ErrIncorrectlyStructuredToken, func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, nil - }) - }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - }, - "errored on service method call": {"AZTABLESTORE#/test-account/table/token/ok", ErrRetrieveFailed, func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, fmt.Errorf("network error") - }) + "errored on token parsing to partiationKey": { + func() *config.ParsedTokenConfig { + // "AZTABLESTORE#/test-vault/token/1|somekey" + tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, store.ErrIncorrectlyStructuredToken, func(t *testing.T) mockAzTableStoreApi { + return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + t.Helper() + resp := aztables.GetEntityResponse{} + return resp, nil + }) + }, }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + "errored on service method call": { + func() *config.ParsedTokenConfig { + // "AZTABLESTORE#/test-account/table/token/ok", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-account/table/token/ok") + return tkn + }, + store.ErrRetrieveFailed, + func(t *testing.T) mockAzTableStoreApi { + return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + t.Helper() + resp := aztables.GetEntityResponse{} + return resp, fmt.Errorf("network error") + }) + }, }, - "empty": {"AZTABLESTORE#/test-vault/token/1|somekey", ErrIncorrectlyStructuredToken, func(t *testing.T) tableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, nil - }) - }, - config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + "empty": { + func() *config.ParsedTokenConfig { + // "AZTABLESTORE#/test-vault/token/1|somekey", + tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/test-vault/token/1|somekey") + return tkn + }, + store.ErrIncorrectlyStructuredToken, func(t *testing.T) mockAzTableStoreApi { + return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { + t.Helper() + resp := aztables.GetEntityResponse{} + return resp, nil + }) + }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - - impl, err := NewAzTableStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { t.Fatal("failed to init aztablestore") } - impl.svc = tt.mockClient(t) - if _, err := impl.Token(); !errors.Is(err, tt.expect) { + impl.WithSvc(tt.mockClient(t)) + if _, err := impl.Value(); !errors.Is(err, tt.expect) { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) } }) @@ -244,66 +288,66 @@ func Test_AzTableStore_Error(t *testing.T) { func Test_fail_AzTable_Client_init(t *testing.T) { // this is basically a wrap around test for the url.Parse method in the stdlib // as that is what the client uses under the hood - token, _ := config.NewParsedTokenConfig("AZTABLESTORE:///%25%65%6e%301-._~/") } - if !errors.Is(err, ErrClientInitialization) { - t.Fatalf(testutils.TestPhraseWithContext, "aztables client init", err.Error(), ErrClientInitialization.Error()) + if !errors.Is(err, store.ErrClientInitialization) { + t.Fatalf(testutils.TestPhraseWithContext, "aztables client init", err.Error(), store.ErrClientInitialization.Error()) } } func Test_azSplitTokenTableStore(t *testing.T) { - t.Parallel() tests := []struct { name string token string - expect azServiceHelper + expect store.AzServiceHelper }{ { name: "simple_with_preceding_slash", token: "/test-account/tablename/somejsontest", - expect: azServiceHelper{ - serviceUri: "https://test-account.table.core.windows.net/tablename", - token: "somejsontest", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-account.table.core.windows.net/tablename", + Token: "somejsontest", }, }, { name: "missing_initial_slash", token: "test-account/tablename/somejsontest", - expect: azServiceHelper{ - serviceUri: "https://test-account.table.core.windows.net/tablename", - token: "somejsontest", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-account.table.core.windows.net/tablename", + Token: "somejsontest", }, }, { name: "missing_initial_slash_multislash_secretname", token: "test-account/tablename/some/json/test", - expect: azServiceHelper{ - serviceUri: "https://test-account.table.core.windows.net/tablename", - token: "some/json/test", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-account.table.core.windows.net/tablename", + Token: "some/json/test", }, }, { name: "with_initial_slash_multislash_secretname", token: "test-account/tablename//some/json/test", - expect: azServiceHelper{ - serviceUri: "https://test-account.table.core.windows.net/tablename", - token: "/some/json/test", + expect: store.AzServiceHelper{ + ServiceUri: "https://test-account.table.core.windows.net/tablename", + Token: "/some/json/test", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := azServiceFromToken(tt.token, "https://%s.table.core.windows.net/%s", 2) - if got.token != tt.expect.token { - t.Errorf(testutils.TestPhrase, tt.expect.token, got.token) + got := store.AzServiceFromToken(tt.token, "https://%s.table.core.windows.net/%s", 2) + if got.Token != tt.expect.Token { + t.Errorf(testutils.TestPhrase, tt.expect.Token, got.Token) } - if got.serviceUri != tt.expect.serviceUri { - t.Errorf(testutils.TestPhrase, tt.expect.serviceUri, got.serviceUri) + if got.ServiceUri != tt.expect.ServiceUri { + t.Errorf(testutils.TestPhrase, tt.expect.ServiceUri, got.ServiceUri) } }) } diff --git a/internal/store/gcpsecrets.go b/internal/store/gcpsecrets.go index 1df7199..07c43e2 100644 --- a/internal/store/gcpsecrets.go +++ b/internal/store/gcpsecrets.go @@ -6,8 +6,8 @@ import ( gcpsecrets "cloud.google.com/go/secretmanager/apiv1" gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/googleapis/gax-go/v2" ) @@ -42,6 +42,10 @@ func NewGcpSecrets(ctx context.Context, logger log.ILogger) (*GcpSecrets, error) }, nil } +func (s *GcpSecrets) WithSvc(svc gcpSecretsApi) { + s.svc = svc +} + func (imp *GcpSecrets) SetToken(token *config.ParsedTokenConfig) { storeConf := &GcpSecretsConfig{} _ = token.ParseMetadata(storeConf) @@ -49,7 +53,7 @@ func (imp *GcpSecrets) SetToken(token *config.ParsedTokenConfig) { imp.config = storeConf } -func (imp *GcpSecrets) Token() (string, error) { +func (imp *GcpSecrets) Value() (string, error) { // Close client currently as new one would be created per iteration defer func() { _ = imp.close() diff --git a/internal/store/gcpsecrets_test.go b/internal/store/gcpsecrets_test.go index 54bdf7b..5f859ba 100644 --- a/internal/store/gcpsecrets_test.go +++ b/internal/store/gcpsecrets_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -9,9 +9,10 @@ import ( "testing" gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/googleapis/gax-go/v2" ) @@ -21,6 +22,10 @@ func (m mockGcpSecretsApi) AccessSecretVersion(ctx context.Context, req *gcpsecr return m(ctx, req, opts...) } +func (m mockGcpSecretsApi) Close() error { + return nil +} + var TEST_GCP_CREDS = []byte(`{ "type": "service_account", "project_id": "xxxxx", @@ -74,49 +79,76 @@ func gcpSecretsGetChecker(t *testing.T, req *gcpsecretspb.AccessSecretVersionReq } func Test_GetGcpSecretVarHappy(t *testing.T) { - // t.Parallel() tests := map[string]struct { - token string + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) gcpSecretsApi - config *config.GenVarsConfig + mockClient func(t *testing.T) mockGcpSecretsApi }{ - "success": {"GCPSECRETS#/token/1", "someValue", func(t *testing.T) gcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{ - Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, - }, nil - }) - }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + "success": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "someValue", func(t *testing.T) mockGcpSecretsApi { + return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { + gcpSecretsGetChecker(t, req) + return &gcpsecretspb.AccessSecretVersionResponse{ + Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, + }, nil + }) + }, }, - "success with version": {"GCPSECRETS#/token/1[version=123]", "someValue", func(t *testing.T) gcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{ - Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, - }, nil - }) - }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + "success with version": { + func() *config.ParsedTokenConfig { + // "GCPSECRETS#/token/1[version=123]" + tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("version=123") + return tkn + }, "someValue", func(t *testing.T) mockGcpSecretsApi { + return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { + gcpSecretsGetChecker(t, req) + return &gcpsecretspb.AccessSecretVersionResponse{ + Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, + }, nil + }) + }, }, - "error": {"GCPSECRETS#/token/1", "unable to retrieve secret", func(t *testing.T) gcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return nil, fmt.Errorf("unable to retrieve secret") - }) - }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + "error": { + func() *config.ParsedTokenConfig { + // "GCPSECRETS#/token/1" + tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "unable to retrieve secret", func(t *testing.T) mockGcpSecretsApi { + return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { + gcpSecretsGetChecker(t, req) + return nil, fmt.Errorf("unable to retrieve secret") + }) + }, }, "found but empty": { - "GCPSECRETS#/token/1", + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "", - func(t *testing.T) gcpSecretsApi { + func(t *testing.T) mockGcpSecretsApi { return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { gcpSecretsGetChecker(t, req) return &gcpsecretspb.AccessSecretVersionResponse{}, nil }) }, - config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), }, } for name, tt := range tests { @@ -126,18 +158,17 @@ func Test_GetGcpSecretVarHappy(t *testing.T) { defer fixture.delete(fixture.name) os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fixture.name) - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - impl, err := NewGcpSecrets(context.TODO(), log.New(io.Discard)) + impl, err := store.NewGcpSecrets(context.TODO(), log.New(io.Discard)) if err != nil { t.Errorf(testutils.TestPhrase, err.Error(), nil) } - impl.svc = tt.mockClient(t) - impl.close = func() error { return nil } - impl.SetToken(token) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + + impl.SetToken(tt.token()) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { diff --git a/internal/store/hashivault.go b/internal/store/hashivault.go index 048039b..558c9b2 100644 --- a/internal/store/hashivault.go +++ b/internal/store/hashivault.go @@ -8,17 +8,17 @@ import ( "strconv" "strings" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" vault "github.com/hashicorp/vault/api" auth "github.com/hashicorp/vault/api/auth/aws" ) -// vaultHelper provides a broken up string -type vaultHelper struct { - path string - token string +// HashiVaultHelper provides a broken up string +type HashiVaultHelper struct { + Path string + Token string } type hashiVaultApi interface { @@ -52,8 +52,8 @@ func NewVaultStore(ctx context.Context, token *config.ParsedTokenConfig, logger } config := vault.DefaultConfig() - vt := splitToken(token.StoreToken()) - imp.strippedToken = vt.token + vt := SplitHashiVaultToken(token.StoreToken()) + imp.strippedToken = vt.Token client, err := vault.NewClient(config) if err != nil { return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) @@ -66,10 +66,14 @@ func NewVaultStore(ctx context.Context, token *config.ParsedTokenConfig, logger } client = awsclient } - imp.svc = client.KVv2(vt.path) + imp.svc = client.KVv2(vt.Path) return imp, nil } +func (s *VaultStore) WithSvc(svc hashiVaultApi) { + s.svc = svc +} + // newVaultStoreWithAWSAuthIAM returns an initialised client with AWSIAMAuth // EC2 auth type is not supported currently func newVaultStoreWithAWSAuthIAM(client *vault.Client, role string) (*vault.Client, error) { @@ -107,7 +111,7 @@ func (imp *VaultStore) SetToken(token *config.ParsedTokenConfig) {} // getTokenValue implements the underlying techonology // token retrieval and returns a stringified version // of the secret -func (imp *VaultStore) Token() (string, error) { +func (imp *VaultStore) Value() (string, error) { imp.logger.Info("%s", "Concrete implementation HashiVault") imp.logger.Info("Getting Secret: %s", imp.token) @@ -145,14 +149,14 @@ func (imp *VaultStore) getSecret(ctx context.Context, token string, version stri return imp.svc.Get(ctx, token) } -func splitToken(token string) vaultHelper { - vh := vaultHelper{} +func SplitHashiVaultToken(token string) HashiVaultHelper { + vh := HashiVaultHelper{} // split token to extract the mount path s := strings.Split(strings.TrimPrefix(token, "/"), "___") // grab token and trim prefix if slash - vh.token = strings.TrimPrefix(strings.Join(s[1:], ""), "/") + vh.Token = strings.TrimPrefix(strings.Join(s[1:], ""), "/") // assign mount path as extracted from input token - vh.path = s[0] + vh.Path = s[0] return vh } diff --git a/internal/store/hashivault_test.go b/internal/store/hashivault_test.go index 71d010a..8c9aed8 100644 --- a/internal/store/hashivault_test.go +++ b/internal/store/hashivault_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -10,30 +10,65 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" vault "github.com/hashicorp/vault/api" ) func TestMountPathExtract(t *testing.T) { ttests := map[string]struct { - token string - tokenSeparator string - keySeparator string - expect string + token func() *config.ParsedTokenConfig + expect string }{ - "without leading slash": {"VAULT://secret___/demo/configmanager", "://", "|", "secret"}, - "with leading slash": {"VAULT:///secret___/demo/configmanager", "://", "|", "secret"}, - "with underscore in path name": {"VAULT://_secret___/demo/configmanager", "://", "|", "_secret"}, - "with double underscore in path name": {"VAULT://__secret___/demo/configmanager", "://", "|", "__secret"}, - "with multiple paths in mountpath": {"VAULT://secret/bar/path___/demo/configmanager", "://", "|", "secret/bar/path"}, + "without leading slash": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/demo/configmanager" + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret"}, + "with leading slash": { + func() *config.ParsedTokenConfig { + // "VAULT:///secret___/demo/configmanager", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret"}, + "with underscore in path name": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("_secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "_secret"}, + "with double underscore in path name": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("__secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "__secret"}, + "with multiple paths in mountpath": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret/bar/path___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret/bar/path"}, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - token, _ := config.NewParsedTokenConfig(tt.token, *config.NewConfig().WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) - got := splitToken(token.StoreToken()) - if got.path != tt.expect { + got := store.SplitHashiVaultToken(tt.token().StoreToken()) + if got.Path != tt.expect { t.Errorf("got %q, expected %q", got, tt.expect) } }) @@ -54,17 +89,23 @@ func (m mockVaultApi) GetVersion(ctx context.Context, secretPath string, version } func TestVaultScenarios(t *testing.T) { - t.Parallel() ttests := map[string]struct { - token string - conf *config.GenVarsConfig + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) hashiVaultApi + mockClient func(t *testing.T) mockVaultApi setupEnv func() func() }{ - "happy return": {"VAULT://secret___/foo", config.NewConfig(), `{"foo":"test2130-9sd-0ds"}`, - func(t *testing.T) hashiVaultApi { + "happy return": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `{"foo":"test2130-9sd-0ds"}`, + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -84,8 +125,17 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "incorrect json": {"VAULT://secret___/foo", config.NewConfig(), `json: unsupported type: func() error`, - func(t *testing.T) hashiVaultApi { + "incorrect json": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/foo", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `json: unsupported type: func() error`, + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -106,10 +156,15 @@ func TestVaultScenarios(t *testing.T) { }, }, "another return": { - "VAULT://secret/engine1___/some/other/foo2", - config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret/engine1___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, `{"foo1":"test2130-9sd-0ds","foo2":"dsfsdf3454456"}`, - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -130,8 +185,17 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "not found": {"VAULT://secret___/foo", config.NewConfig(), `secret not found`, - func(t *testing.T) hashiVaultApi { + "not found": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/foo", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `secret not found`, + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -149,8 +213,17 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "403": {"VAULT://secret___/some/other/foo2", config.NewConfig(), `client 403`, - func(t *testing.T) hashiVaultApi { + "403": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `client 403`, + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -168,18 +241,29 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "found but empty": {"VAULT://secret___/some/other/foo2", config.NewConfig(), `{}`, func(t *testing.T) hashiVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - return &vault.KVSecret{Data: m}, nil - } - return mv - }, + "found but empty": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + // config.NewConfig(), + `{}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + return &vault.KVSecret{Data: m}, nil + } + return mv + }, func() func() { os.Setenv("VAULT_TOKEN", "129378y1231283") return func() { @@ -187,17 +271,26 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "found but nil returned": {"VAULT://secret___/some/other/foo2", config.NewConfig(), "", func(t *testing.T) hashiVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return &vault.KVSecret{Data: nil}, nil - } - return mv - }, + "found but nil returned": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "", + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return &vault.KVSecret{Data: nil}, nil + } + return mv + }, func() func() { os.Setenv("VAULT_TOKEN", "129378y1231283") return func() { @@ -205,19 +298,29 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "version provided correctly": {"VAULT://secret___/some/other/foo2[version=1]", config.NewConfig(), `{"foo2":"dsfsdf3454456"}`, func(t *testing.T) hashiVaultApi { - mv := mockVaultApi{} - mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - m["foo2"] = "dsfsdf3454456" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, + "version provided correctly": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2[version=1]", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1") + return tkn + }, + `{"foo2":"dsfsdf3454456"}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + m["foo2"] = "dsfsdf3454456" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, func() func() { os.Setenv("VAULT_TOKEN", "129378y1231283") return func() { @@ -225,17 +328,27 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "version provided but unable to parse": {"VAULT://secret___/some/other/foo2[version=1a]", config.NewConfig(), "unable to parse version into an integer: strconv.Atoi: parsing \"1a\": invalid syntax", func(t *testing.T) hashiVaultApi { - mv := mockVaultApi{} - mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return nil, nil - } - return mv - }, + "version provided but unable to parse": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2[version=1a]", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1a") + return tkn + }, + "unable to parse version into an integer: strconv.Atoi: parsing \"1a\": invalid syntax", + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return nil, nil + } + return mv + }, func() func() { os.Setenv("VAULT_TOKEN", "129378y1231283") return func() { @@ -244,11 +357,16 @@ func TestVaultScenarios(t *testing.T) { }, }, "vault rate limit incorrect": { - "VAULT://secret___/some/other/foo2", - config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, `error encountered setting up default configuration: VAULT_RATE_LIMIT was provided but incorrectly formatted failed to initialize the client`, - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -273,9 +391,8 @@ failed to initialize the client`, t.Run(name, func(t *testing.T) { tearDown := tt.setupEnv() defer tearDown() - token, _ := config.NewParsedTokenConfig(tt.token, *tt.conf) - impl, err := NewVaultStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { if err.Error() != tt.expect { t.Fatalf("failed to init hashivault, %v", err.Error()) @@ -283,8 +400,8 @@ failed to initialize the client`, return } - impl.svc = tt.mockClient(t) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -300,17 +417,22 @@ failed to initialize the client`, func TestAwsIamAuth(t *testing.T) { ttests := map[string]struct { - token string - conf *config.GenVarsConfig + token func() *config.ParsedTokenConfig expect string - mockClient func(t *testing.T) hashiVaultApi + mockClient func(t *testing.T) mockVaultApi mockHanlder func(t *testing.T) http.Handler setupEnv func(addr string) func() }{ "aws_iam auth no role specified": { - "VAULT://secret___/some/other/foo2[version:1]", config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1") + return tkn + }, "role provided is empty, EC2 auth not supported", - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -336,14 +458,20 @@ func TestAwsIamAuth(t *testing.T) { }, }, "aws_iam auth incorrectly formatted request": { - "VAULT://secret___/some/other/foo2[version=1,iam_role=not_a_role]", config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1,iam_role=not_a_role") + return tkn + }, `unable to login to AWS auth method: unable to log in to auth method: unable to log in with AWS auth: Error making API request. URL: PUT %s/v1/auth/aws/login Code: 400. Raw Message: incorrect values supplied. failed to initialize the client`, - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -377,16 +505,23 @@ incorrect values supplied. failed to initialize the client`, }, }, "aws_iam auth success": { - "VAULT://secret___/some/other/foo2[iam_role=arn:aws:iam::1111111:role/i-orchestration]", config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") + return tkn + }, + // `{"foo2":"dsfsdf3454456"}`, - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() if secretPath != "some/other/foo2" { t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) } - m := make(map[string]interface{}) + m := make(map[string]any) m["foo2"] = "dsfsdf3454456" return &vault.KVSecret{Data: m}, nil } @@ -414,9 +549,15 @@ incorrect values supplied. failed to initialize the client`, }, }, "aws_iam auth no token returned": { - "VAULT://secret___/some/other/foo2[iam_role=arn:aws:iam::1111111:role/i-orchestration]", config.NewConfig(), + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") + return tkn + }, `unable to login to AWS auth method: response did not return ClientToken, client token not set. failed to initialize the client`, - func(t *testing.T) hashiVaultApi { + func(t *testing.T) mockVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -432,7 +573,6 @@ incorrect values supplied. failed to initialize the client`, func(t *testing.T) http.Handler { mux := http.NewServeMux() mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write([]byte(`{"auth":{}}`)) }) @@ -458,9 +598,7 @@ incorrect values supplied. failed to initialize the client`, ts := httptest.NewServer(tt.mockHanlder(t)) tearDown := tt.setupEnv(ts.URL) defer tearDown() - token, _ := config.NewParsedTokenConfig(tt.token, *tt.conf) - - impl, err := NewVaultStore(context.TODO(), token, log.New(io.Discard)) + impl, err := store.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) if err != nil { // WHAT A CRAP way to do this... if err.Error() != strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0] { @@ -470,8 +608,8 @@ incorrect values supplied. failed to initialize the client`, return } - impl.svc = tt.mockClient(t) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/internal/store/paramstore.go b/internal/store/paramstore.go index 72b43a8..aa45ace 100644 --- a/internal/store/paramstore.go +++ b/internal/store/paramstore.go @@ -3,8 +3,8 @@ package store import ( "context" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/aws/aws-sdk-go-v2/aws" awsConf "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ssm" @@ -41,6 +41,10 @@ func NewParamStore(ctx context.Context, logger log.ILogger) (*ParamStore, error) }, nil } +func (s *ParamStore) WithSvc(svc paramStoreApi) { + s.svc = svc +} + func (imp *ParamStore) SetToken(token *config.ParsedTokenConfig) { storeConf := &ParamStrConfig{} _ = token.ParseMetadata(storeConf) @@ -48,7 +52,7 @@ func (imp *ParamStore) SetToken(token *config.ParsedTokenConfig) { imp.config = storeConf } -func (imp *ParamStore) Token() (string, error) { +func (imp *ParamStore) Value() (string, error) { imp.logger.Info("%s", "Concrete implementation ParameterStore") imp.logger.Info("ParamStore Token: %s", imp.token.String()) diff --git a/internal/store/paramstore_test.go b/internal/store/paramstore_test.go index 19c027a..8fc11d4 100644 --- a/internal/store/paramstore_test.go +++ b/internal/store/paramstore_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -7,18 +7,14 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" ) -// var ( -// tsuccessParam = "someVal" -// tsuccessObj map[string]string = map[string]string{"AWSPARAMSTR#/token/1": "someVal"} -// ) - type mockParamApi func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) func (m mockParamApi) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { @@ -44,76 +40,104 @@ func awsParamtStoreCommonGetChecker(t *testing.T, params *ssm.GetParameterInput) } func Test_GetParamStore(t *testing.T) { - t.Parallel() - var ( tsuccessParam = "someVal" // tsuccessObj map[string]string = map[string]string{"AWSPARAMSTR#/token/1": "someVal"} ) tests := map[string]struct { - token string - keySeparator string - tokenSeparator string - expect string - mockClient func(t *testing.T) paramStoreApi - config *config.GenVarsConfig + token func() *config.ParsedTokenConfig + expect string + mockClient func(t *testing.T) mockParamApi }{ - "successVal": {"AWSPARAMSTR#/token/1", "|", "#", tsuccessParam, func(t *testing.T) paramStoreApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: &tsuccessParam}, - }, nil - }) - }, config.NewConfig(), + "successVal": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/demo/configmanager" + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + tsuccessParam, func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: &tsuccessParam}, + }, nil + }) + }, }, - "successVal with keyseparator": {"AWSPARAMSTR#/token/1|somekey", "|", "#", tsuccessParam, func(t *testing.T) paramStoreApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) + "successVal with keyseparator": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1|somekey", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, + tsuccessParam, func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) - if strings.Contains(*params.Name, "|somekey") { - t.Errorf("incorrectly stripped key separator") - } + if strings.Contains(*params.Name, "|somekey") { + t.Errorf("incorrectly stripped key separator") + } - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: &tsuccessParam}, - }, nil - }) - }, config.NewConfig(), + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: &tsuccessParam}, + }, nil + }) + }, }, - "errored": {"AWSPARAMSTR#/token/1", "|", "#", "unable to retrieve", func(t *testing.T) paramStoreApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return nil, fmt.Errorf("unable to retrieve") - }) - }, config.NewConfig(), + "errored": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "unable to retrieve", func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return nil, fmt.Errorf("unable to retrieve") + }) + }, }, - "nil to empty": {"AWSPARAMSTR#/token/1", "|", "#", "", func(t *testing.T) paramStoreApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: nil}, - }, nil - }) - }, config.NewConfig(), + "nil to empty": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "", func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: nil}, + }, nil + }) + }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) - - impl, err := NewParamStore(context.TODO(), log.New(io.Discard)) + impl, err := store.NewParamStore(context.TODO(), log.New(io.Discard)) if err != nil { t.Errorf(testutils.TestPhrase, err.Error(), nil) } - impl.svc = tt.mockClient(t) - impl.SetToken(token) - got, err := impl.Token() + impl.WithSvc(tt.mockClient(t)) + impl.SetToken(tt.token()) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/internal/store/secretsmanager.go b/internal/store/secretsmanager.go index 6b0f7a2..6744d8a 100644 --- a/internal/store/secretsmanager.go +++ b/internal/store/secretsmanager.go @@ -3,8 +3,8 @@ package store import ( "context" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/aws/aws-sdk-go-v2/aws" awsconf "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" @@ -42,6 +42,10 @@ func NewSecretsMgr(ctx context.Context, logger log.ILogger) (*SecretsMgr, error) } +func (s *SecretsMgr) WithSvc(svc secretsMgrApi) { + s.svc = svc +} + func (imp *SecretsMgr) SetToken(token *config.ParsedTokenConfig) { storeConf := &SecretsMgrConfig{} if err := token.ParseMetadata(storeConf); err != nil { @@ -51,7 +55,7 @@ func (imp *SecretsMgr) SetToken(token *config.ParsedTokenConfig) { imp.config = storeConf } -func (imp *SecretsMgr) Token() (string, error) { +func (imp *SecretsMgr) Value() (string, error) { imp.logger.Info("Concrete implementation SecretsManager") imp.logger.Debug("SecretsManager Token: %s", imp.token.String()) diff --git a/internal/store/secretsmanager_test.go b/internal/store/secretsmanager_test.go index 3b29dd2..870bb75 100644 --- a/internal/store/secretsmanager_test.go +++ b/internal/store/secretsmanager_test.go @@ -1,4 +1,4 @@ -package store +package store_test import ( "context" @@ -7,9 +7,10 @@ import ( "strings" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" ) @@ -34,77 +35,111 @@ func awsSecretsMgrGetChecker(t *testing.T, params *secretsmanager.GetSecretValue } func Test_GetSecretMgr(t *testing.T) { - t.Parallel() tsuccessSecret := "dsgkbdsf" tests := map[string]struct { - token string - keySeparator string - tokenSeparator string - expect string - mockClient func(t *testing.T) secretsMgrApi - config *config.GenVarsConfig + token func() *config.ParsedTokenConfig + expect string + mockClient func(t *testing.T) mockSecretsApi }{ - "success": {"AWSSECRETS#/token/1", "|", "#", tsuccessSecret, func(t *testing.T) secretsMgrApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: &tsuccessSecret, - }, nil - }) - }, config.NewConfig(), + "success": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, tsuccessSecret, func(t *testing.T) mockSecretsApi { + return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + t.Helper() + awsSecretsMgrGetChecker(t, params) + return &secretsmanager.GetSecretValueOutput{ + SecretString: &tsuccessSecret, + }, nil + }) + }, }, - "success with version": {"AWSSECRETS#/token/1[version=123]", "|", "#", tsuccessSecret, func(t *testing.T) secretsMgrApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: &tsuccessSecret, - }, nil - }) - }, config.NewConfig(), + "success with version": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("version=123") + return tkn + }, + tsuccessSecret, func(t *testing.T) mockSecretsApi { + return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + t.Helper() + awsSecretsMgrGetChecker(t, params) + return &secretsmanager.GetSecretValueOutput{ + SecretString: &tsuccessSecret, + }, nil + }) + }, }, - "success with binary": {"AWSSECRETS#/token/1", "|", "#", tsuccessSecret, func(t *testing.T) secretsMgrApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretBinary: []byte(tsuccessSecret), - }, nil - }) - }, config.NewConfig(), + "success with binary": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + tsuccessSecret, func(t *testing.T) mockSecretsApi { + return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + t.Helper() + awsSecretsMgrGetChecker(t, params) + return &secretsmanager.GetSecretValueOutput{ + SecretBinary: []byte(tsuccessSecret), + }, nil + }) + }, }, - "errored": {"AWSSECRETS#/token/1", "|", "#", "unable to retrieve secret", func(t *testing.T) secretsMgrApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return nil, fmt.Errorf("unable to retrieve secret") - }) - }, config.NewConfig(), + "errored": { + func() *config.ParsedTokenConfig { + // "AWSSECRETS#/token/1", "|", "#", + tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "unable to retrieve secret", func(t *testing.T) mockSecretsApi { + return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + t.Helper() + awsSecretsMgrGetChecker(t, params) + return nil, fmt.Errorf("unable to retrieve secret") + }) + }, }, - "ok but empty": {"AWSSECRETS#/token/1", "|", "#", "", func(t *testing.T) secretsMgrApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: nil, - }, nil - }) - }, config.NewConfig(), + "ok but empty": { + func() *config.ParsedTokenConfig { + // "AWSSECRETS#/token/1", "|", "#", + tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("version=123") + return tkn + }, + "", func(t *testing.T) mockSecretsApi { + return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { + t.Helper() + awsSecretsMgrGetChecker(t, params) + return &secretsmanager.GetSecretValueOutput{ + SecretString: nil, + }, nil + }) + }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { + impl, _ := store.NewSecretsMgr(context.TODO(), log.New(io.Discard)) + impl.WithSvc(tt.mockClient(t)) - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) - - impl, _ := NewSecretsMgr(context.TODO(), log.New(io.Discard)) - impl.svc = tt.mockClient(t) - - impl.SetToken(token) - got, err := impl.Token() + impl.SetToken(tt.token()) + got, err := impl.Value() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/internal/store/store.go b/internal/store/store.go index 42adf5b..eceeb45 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -3,7 +3,7 @@ package store import ( "errors" - "github.com/DevLabFoundry/configmanager/v2/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/config" ) const implementationNetworkErr string = "implementation %s error: %v for token: %s" @@ -20,6 +20,8 @@ var ( // // Defined on the package for easier re-use across the program type Strategy interface { - Token() (s string, e error) + // Value retrieves the underlying value for the token + Value() (s string, e error) + // SetToken SetToken(s *config.ParsedTokenConfig) } diff --git a/internal/strategy/strategy.go b/internal/strategy/strategy.go index a5b4981..ac19a30 100644 --- a/internal/strategy/strategy.go +++ b/internal/strategy/strategy.go @@ -1,6 +1,4 @@ -// Package strategy is a strategy pattern wrapper around the store implementations -// -// NOTE: this may be refactored out into the store package directly +// Package strategy is a factory method wrapper around the backing store implementations package strategy import ( @@ -9,9 +7,9 @@ import ( "fmt" "sync" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" ) var ErrTokenInvalid = errors.New("invalid token - cannot get prefix") @@ -22,46 +20,16 @@ type StrategyFunc func(ctx context.Context, token *config.ParsedTokenConfig) (st // StrategyFuncMap type StrategyFuncMap map[config.ImplementationPrefix]StrategyFunc -func defaultStrategyFuncMap(logger log.ILogger) map[config.ImplementationPrefix]StrategyFunc { - return map[config.ImplementationPrefix]StrategyFunc{ - config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewAzTableStore(ctx, token, logger) - }, - config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewAzAppConf(ctx, token, logger) - }, - config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewGcpSecrets(ctx, logger) - }, - config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewSecretsMgr(ctx, logger) - }, - config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewParamStore(ctx, logger) - }, - config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewKvScrtStore(ctx, token, logger) - }, - config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewVaultStore(ctx, token, logger) - }, - } -} - -type strategyFnMap struct { - mu sync.Mutex - funcMap StrategyFuncMap -} -type RetrieveStrategy struct { - implementation store.Strategy +type Strategy struct { config config.GenVarsConfig strategyFuncMap strategyFnMap } -type Opts func(*RetrieveStrategy) + +type Opts func(*Strategy) // New -func New(config config.GenVarsConfig, logger log.ILogger, opts ...Opts) *RetrieveStrategy { - rs := &RetrieveStrategy{ +func New(config config.GenVarsConfig, logger log.ILogger, opts ...Opts) *Strategy { + rs := &Strategy{ config: config, strategyFuncMap: strategyFnMap{mu: sync.Mutex{}, funcMap: defaultStrategyFuncMap(logger)}, } @@ -78,25 +46,36 @@ func New(config config.GenVarsConfig, logger log.ILogger, opts ...Opts) *Retriev // Mainly used for testing // NOTE: this may lead to eventual optional configurations by users func WithStrategyFuncMap(funcMap StrategyFuncMap) Opts { - return func(rs *RetrieveStrategy) { + return func(rs *Strategy) { + rs.strategyFuncMap.mu.Lock() + defer rs.strategyFuncMap.mu.Unlock() for prefix, implementation := range funcMap { - rs.strategyFuncMap.mu.Lock() - defer rs.strategyFuncMap.mu.Unlock() rs.strategyFuncMap.funcMap[config.ImplementationPrefix(prefix)] = implementation } } } -func (rs *RetrieveStrategy) setImplementation(strategy store.Strategy) { - rs.implementation = strategy -} +// GetImplementation is a factory method returning the concrete implementation for the retrieval of the token value +// i.e. facilitating the exchange of the supplied token for the underlying value +func (rs *Strategy) GetImplementation(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + if token == nil { + return nil, fmt.Errorf("unable to get prefix, %w", ErrTokenInvalid) + } -func (rs *RetrieveStrategy) setTokenVal(s *config.ParsedTokenConfig) { - rs.implementation.SetToken(s) + if store, found := rs.strategyFuncMap.funcMap[token.Prefix()]; found { + return store(ctx, token) + } + + return nil, fmt.Errorf("implementation not found for input string: %s", token) } -func (rs *RetrieveStrategy) getTokenValue() (string, error) { - return rs.implementation.Token() +func ExchangeToken(s store.Strategy, token *config.ParsedTokenConfig) *TokenResponse { + cr := &TokenResponse{} + cr.Err = nil + cr.key = token + s.SetToken(token) + cr.value, cr.Err = s.Value() + return cr } type TokenResponse struct { @@ -113,31 +92,33 @@ func (tr *TokenResponse) Value() string { return tr.value } -// retrieveSpecificCh wraps around the specific strategy implementation -// and publishes results to a channel -func (rs *RetrieveStrategy) RetrieveByToken(ctx context.Context, impl store.Strategy, tokenConf *config.ParsedTokenConfig) *TokenResponse { - cr := &TokenResponse{} - cr.Err = nil - cr.key = tokenConf - rs.setImplementation(impl) - rs.setTokenVal(tokenConf) - s, err := rs.getTokenValue() - if err != nil { - cr.Err = err - return cr +func defaultStrategyFuncMap(logger log.ILogger) StrategyFuncMap { + return map[config.ImplementationPrefix]StrategyFunc{ + config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewAzTableStore(ctx, token, logger) + }, + config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewAzAppConf(ctx, token, logger) + }, + config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewGcpSecrets(ctx, logger) + }, + config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewSecretsMgr(ctx, logger) + }, + config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewParamStore(ctx, logger) + }, + config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewKvScrtStore(ctx, token, logger) + }, + config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewVaultStore(ctx, token, logger) + }, } - cr.value = s - return cr } -func (rs *RetrieveStrategy) SelectImplementation(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - if token == nil { - return nil, fmt.Errorf("unable to get prefix, %w", ErrTokenInvalid) - } - - if store, found := rs.strategyFuncMap.funcMap[token.Prefix()]; found { - return store(ctx, token) - } - - return nil, fmt.Errorf("implementation not found for input string: %s", token) +type strategyFnMap struct { + mu sync.Mutex + funcMap StrategyFuncMap } diff --git a/internal/strategy/strategy_test.go b/internal/strategy/strategy_test.go index 93c4c6c..acae5e1 100644 --- a/internal/strategy/strategy_test.go +++ b/internal/strategy/strategy_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - "github.com/DevLabFoundry/configmanager/v2/internal/config" - log "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/store" - "github.com/DevLabFoundry/configmanager/v2/internal/strategy" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/internal/config" + log "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/strategy" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" "github.com/go-test/deep" ) @@ -23,7 +23,7 @@ type mockGenerate struct { func (m mockGenerate) SetToken(s *config.ParsedTokenConfig) { } -func (m mockGenerate) Token() (s string, e error) { +func (m mockGenerate) Value() (s string, e error) { return m.value, m.err } @@ -41,20 +41,21 @@ var TEST_GCP_CREDS = []byte(`{ }`) func Test_Strategy_Retrieve_succeeds(t *testing.T) { - t.Parallel() ttests := map[string]struct { - impl func(t *testing.T) store.Strategy - config *config.GenVarsConfig - token string - expect string + impl func(t *testing.T) store.Strategy + config *config.GenVarsConfig + token string + expect string + impPrefix config.ImplementationPrefix }{ "with mocked implementation AZTABLESTORAGE": { func(t *testing.T) store.Strategy { - return &mockGenerate{"AZTABLESTORE://mountPath/token", "bar", nil} + return &mockGenerate{"mountPath/token", "bar", nil} }, - config.NewConfig().WithOutputPath("stdout").WithTokenSeparator("://"), - "AZTABLESTORE://mountPath/token", + config.NewConfig().WithOutputPath("stdout"), + "mountPath/token", "bar", + config.AzTableStorePrefix, }, // "error in retrieval": { // func(t *testing.T) store.Strategy { @@ -68,24 +69,23 @@ func Test_Strategy_Retrieve_succeeds(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - rs := strategy.New(*tt.config, log.New(io.Discard)) - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - got := rs.RetrieveByToken(context.TODO(), tt.impl(t), token) + token, _ := config.NewToken(tt.impPrefix, *tt.config) + token.WithSanitizedToken(tt.token) + got := strategy.ExchangeToken(tt.impl(t), token) if got.Err != nil { t.Errorf(testutils.TestPhraseWithContext, "Token response errored", got.Err.Error(), tt.expect) } if got.Value() != tt.expect { t.Errorf(testutils.TestPhraseWithContext, "Value not correct", got.Value(), tt.expect) } - if got.Key().String() != tt.token { - t.Errorf(testutils.TestPhraseWithContext, "INcorrect Token returned in Key", got.Key().String(), tt.token) + if got.Key().StoreToken() != tt.token { + t.Errorf(testutils.TestPhraseWithContext, "Incorrect Token returned in Key", got.Key().StoreToken(), tt.token) } }) } } func Test_CustomStrategyFuncMap_add_own(t *testing.T) { - t.Parallel() ttests := map[string]struct { }{ @@ -95,7 +95,8 @@ func Test_CustomStrategyFuncMap_add_own(t *testing.T) { t.Run(name, func(t *testing.T) { called := 0 genVarsConf := config.NewConfig() - token, _ := config.NewParsedTokenConfig("AZTABLESTORE://mountPath/token", *genVarsConf) + token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig()) + token.WithSanitizedToken("mountPath/token") var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { m := &mockGenerate{"AZTABLESTORE://mountPath/token", "bar", nil} @@ -105,8 +106,8 @@ func Test_CustomStrategyFuncMap_add_own(t *testing.T) { s := strategy.New(*genVarsConf, log.New(io.Discard), strategy.WithStrategyFuncMap(strategy.StrategyFuncMap{config.AzTableStorePrefix: custFunc})) - store, _ := s.SelectImplementation(context.TODO(), token) - _ = s.RetrieveByToken(context.TODO(), store, token) + store, _ := s.GetImplementation(context.TODO(), token) + _ = strategy.ExchangeToken(store, token) if called != 1 { t.Errorf(testutils.TestPhraseWithContext, "custom func not called", called, 1) @@ -123,16 +124,18 @@ func Test_SelectImpl_With(t *testing.T) { config *config.GenVarsConfig expect func() store.Strategy expErr error + impPrefix config.ImplementationPrefix }{ "unknown": { func() func() { return func() { } }, - "UNKNOWN#foo/bar", + "foo/bar", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { return nil }, fmt.Errorf("implementation not found for input string: UNKNOWN#foo/bar"), + config.UnknownPrefix, }, "success AZTABLESTORE": { func() func() { @@ -141,14 +144,17 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "AZTABLESTORE#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { - token, _ := config.NewParsedTokenConfig("AZTABLESTORE#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("#")) + token.WithSanitizedToken("foo/bar1") + s, _ := store.NewAzTableStore(context.TODO(), token, log.New(io.Discard)) return s }, nil, + config.AzTableStorePrefix, }, "success AWSPARAMSTR": { func() func() { @@ -158,13 +164,14 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "AWSPARAMSTR#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { s, _ := store.NewParamStore(context.TODO(), log.New(io.Discard)) return s }, nil, + config.ParamStorePrefix, }, "success AWSSECRETS": { func() func() { @@ -174,13 +181,14 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "AWSSECRETS#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { s, _ := store.NewSecretsMgr(context.TODO(), log.New(io.Discard)) return s }, nil, + config.SecretMgrPrefix, }, "success AZKVSECRET": { func() func() { @@ -190,14 +198,16 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "AZKVSECRET#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { - token, _ := config.NewParsedTokenConfig("AZKVSECRET#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + token, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithTokenSeparator("#")) + token.WithSanitizedToken("foo/bar1") s, _ := store.NewKvScrtStore(context.TODO(), token, log.New(io.Discard)) return s }, nil, + config.AzKeyVaultSecretsPrefix, }, "success AZAPPCONF": { func() func() { @@ -205,14 +215,16 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "AZAPPCONF#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { - token, _ := config.NewParsedTokenConfig("AZAPPCONF#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + token, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithTokenSeparator("#")) + token.WithSanitizedToken("foo/bar1") s, _ := store.NewAzAppConf(context.TODO(), token, log.New(io.Discard)) return s }, nil, + config.AzAppConfigPrefix, }, "success VAULT": { func() func() { @@ -221,14 +233,16 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "VAULT#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { - token, _ := config.NewParsedTokenConfig("VAULT#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + token, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig().WithTokenSeparator("#")) + token.WithSanitizedToken("foo/bar1") s, _ := store.NewVaultStore(context.TODO(), token, log.New(io.Discard)) return s }, nil, + config.HashicorpVaultPrefix, }, "success GCPSECRETS": { func() func() { @@ -240,32 +254,15 @@ func Test_SelectImpl_With(t *testing.T) { os.Clearenv() } }, - "GCPSECRETS#foo/bar1", + "foo/bar1", config.NewConfig().WithTokenSeparator("#"), func() store.Strategy { s, _ := store.NewGcpSecrets(context.TODO(), log.New(io.Discard)) return s }, nil, + config.GcpSecretsPrefix, }, - // "default Error": { - // func() func() { - // os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - // os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - // return func() { - // os.Clearenv() - // } - // }, - // context.TODO(), - // UnknownPrefix, "AWSPARAMSTR://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), - // func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { - // imp, err := NewParamStore(ctx) - // if err != nil { - // t.Errorf(testutils.TestPhraseWithContext, "init impl error", err.Error(), nil) - // } - // return imp - // }, - // }, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { @@ -273,8 +270,9 @@ func Test_SelectImpl_With(t *testing.T) { defer tearDown() want := tt.expect() rs := strategy.New(*tt.config, log.New(io.Discard)) - token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) - got, err := rs.SelectImplementation(context.TODO(), token) + token, _ := config.NewToken(tt.impPrefix, *tt.config) + token.WithSanitizedToken(tt.token) + got, err := rs.GetImplementation(context.TODO(), token) if err != nil { if err.Error() != tt.expErr.Error() { diff --git a/pkg/.gitkeep b/pkg/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go deleted file mode 100644 index 4d8bd69..0000000 --- a/pkg/generator/generator.go +++ /dev/null @@ -1,266 +0,0 @@ -package generator - -import ( - "context" - "encoding/json" - "fmt" - "io" - "strconv" - "sync" - - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/strategy" - "github.com/spyzhov/ajson" -) - -// GenVars is the main struct holding the -// strategy patterns iface -// any initialised config if overridded with withers -// as well as the final outString and the initial rawMap -// which wil be passed in a loop into a goroutine to perform the -// relevant strategy network calls to the config store implementations -type GenVars struct { - Logger log.ILogger - strategy strategy.StrategyFuncMap - ctx context.Context - config config.GenVarsConfig - // rawMap is the internal object that holds the values - // of original token => retrieved value - decrypted in plain text - // with a mutex RW locker - rawMap tokenMapSafe //ParsedMap -} - -type Opts func(*GenVars) - -// NewGenerator returns a new instance of Generator -// with a default strategy pattern wil be overwritten -// during the first run of a found tokens map -func NewGenerator(ctx context.Context, opts ...Opts) *GenVars { - // defaultStrategy := NewDefatultStrategy() - return newGenVars(ctx, opts...) -} - -func newGenVars(ctx context.Context, opts ...Opts) *GenVars { - m := make(ParsedMap) - conf := config.NewConfig() - g := &GenVars{ - Logger: log.New(io.Discard), - rawMap: tokenMapSafe{ - tokenMap: m, - mu: &sync.Mutex{}, - }, - ctx: ctx, - // return using default config - config: *conf, - } - g.strategy = nil - - // now apply additional opts - for _, o := range opts { - o(g) - } - - return g -} - -// WithStrategyMap -// -// Adds addtional funcs for storageRetrieval used for testing only -func (c *GenVars) WithStrategyMap(sm strategy.StrategyFuncMap) *GenVars { - c.strategy = sm - return c -} - -// WithConfig uses custom config -func (c *GenVars) WithConfig(cfg *config.GenVarsConfig) *GenVars { - // backwards compatibility - if cfg != nil { - c.config = *cfg - } - return c -} - -// WithContext uses caller passed context -func (c *GenVars) WithContext(ctx context.Context) *GenVars { - c.ctx = ctx - return c -} - -// Config gets Config on the GenVars -func (c *GenVars) Config() *config.GenVarsConfig { - return &c.config -} - -// ParsedMap is the internal working object definition and -// the return type if results are not flushed to file -type ParsedMap map[string]any - -func (pm ParsedMap) MapKeys() (keys []string) { - for k := range pm { - keys = append(keys, k) - } - return -} - -type tokenMapSafe struct { - mu *sync.Mutex - tokenMap ParsedMap -} - -func (tms *tokenMapSafe) getTokenMap() ParsedMap { - tms.mu.Lock() - defer tms.mu.Unlock() - return tms.tokenMap -} - -func (tms *tokenMapSafe) addKeyVal(key *config.ParsedTokenConfig, val string) { - tms.mu.Lock() - defer tms.mu.Unlock() - // NOTE: still use the metadata in the key - // there could be different versions / labels for the same token and hence different values - // However the JSONpath look up - tms.tokenMap[key.String()] = keySeparatorLookup(key, val) -} - -type rawTokenMap struct { - mu sync.Mutex - tokenMap map[string]*config.ParsedTokenConfig -} - -func newRawTokenMap() *rawTokenMap { - return &rawTokenMap{mu: sync.Mutex{}, tokenMap: map[string]*config.ParsedTokenConfig{}} -} - -func (rtm *rawTokenMap) addToken(name string, parsedToken *config.ParsedTokenConfig) { - rtm.mu.Lock() - defer rtm.mu.Unlock() - rtm.tokenMap[name] = parsedToken -} - -func (rtm *rawTokenMap) mapOfToken() map[string]*config.ParsedTokenConfig { - rtm.mu.Lock() - defer rtm.mu.Unlock() - return rtm.tokenMap -} - -// Generate generates a k/v map of the tokens with their corresponding secret/paramstore values -// the standard pattern of a token should follow a path like string -func (c *GenVars) Generate(tokens []string) (ParsedMap, error) { - - rtm := newRawTokenMap() - for _, token := range tokens { - // TODO: normalize tokens here potentially - // merge any tokens that only differ in keys lookup inside the object - parsedToken, err := config.NewParsedTokenConfig(token, c.config) - if err != nil { - c.Logger.Info(err.Error()) - continue - } - rtm.addToken(token, parsedToken) - } - // pass in default initialised retrieveStrategy - // input should be - if err := c.generate(rtm); err != nil { - return nil, err - } - return c.rawMap.getTokenMap(), nil -} - -// generate checks if any tokens found -// initiates groutines with fixed size channel map -// to capture responses and errors -// generates ParsedMap which includes -func (c *GenVars) generate(rawMap *rawTokenMap) error { - rtm := rawMap.mapOfToken() - if len(rtm) < 1 { - c.Logger.Debug("no replaceable tokens found in input") - return nil - } - - tokenCount := len(rtm) - outCh := make(chan *strategy.TokenResponse, tokenCount) - - // TODO: initialise the singleton serviceContainer - // pass into each goroutine - for _, parsedToken := range rtm { - token := parsedToken // safe closure capture - // take value from config allocation on a per iteration basis - go func() { - s := strategy.New(c.config, c.Logger, strategy.WithStrategyFuncMap(c.strategy)) - storeStrategy, err := s.SelectImplementation(c.ctx, token) - if err != nil { - outCh <- &strategy.TokenResponse{Err: err} - return - } - outCh <- s.RetrieveByToken(c.ctx, storeStrategy, token) - }() - } - - // Fan-in: receive results with pure select - received := 0 - for received < tokenCount { - select { - case cr := <-outCh: - if cr == nil { - continue // defensive (shouldn't happen) - } - c.Logger.Debug("cro: %+v", cr) - if cr.Err != nil { - c.Logger.Debug("cr.err %v, for token: %s", cr.Err, cr.Key()) - } else { - c.rawMap.addKeyVal(cr.Key(), cr.Value()) - } - received++ - case <-c.ctx.Done(): - c.Logger.Debug("context done: %v", c.ctx.Err()) - return c.ctx.Err() // propagate context error (cancel/timeout) - } - } - return nil -} - -// IsParsed will try to parse the return found string into -// map[string]string -// If found it will convert that to a map with all keys uppercased -// and any characters -func IsParsed(v any, trm ParsedMap) bool { - str := fmt.Sprint(v) - err := json.Unmarshal([]byte(str), &trm) - return err == nil -} - -// keySeparatorLookup checks if the key contains -// keySeparator character -// If it does contain one then it tries to parse -func keySeparatorLookup(key *config.ParsedTokenConfig, val string) string { - // key has separator - k := key.LookupKeys() - if k == "" { - // c.logger.Info("no keyseparator found") - return val - } - - keys, err := ajson.JSONPath([]byte(val), fmt.Sprintf("$..%s", k)) - if err != nil { - // c.logger.Debug("unable to parse as json object %v", err.Error()) - return val - } - - if len(keys) == 1 { - v := keys[0] - if v.Type() == ajson.String { - str, err := strconv.Unquote(fmt.Sprintf("%v", v)) - if err != nil { - // c.logger.Debug("unable to unquote value: %v returning as is", v) - return fmt.Sprintf("%v", v) - } - return str - } - - return fmt.Sprintf("%v", v) - } - - // c.logger.Info("no value found in json using path expression") - return "" -} diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go deleted file mode 100644 index bfb91b9..0000000 --- a/pkg/generator/generator_test.go +++ /dev/null @@ -1,548 +0,0 @@ -package generator_test - -import ( - "bytes" - "context" - "fmt" - "testing" - - "github.com/DevLabFoundry/configmanager/v2/internal/config" - "github.com/DevLabFoundry/configmanager/v2/internal/log" - "github.com/DevLabFoundry/configmanager/v2/internal/store" - "github.com/DevLabFoundry/configmanager/v2/internal/strategy" - "github.com/DevLabFoundry/configmanager/v2/internal/testutils" - "github.com/DevLabFoundry/configmanager/v2/pkg/generator" -) - -type mockGenerate struct { - inToken, value string - err error -} - -func (m *mockGenerate) SetToken(s *config.ParsedTokenConfig) { -} -func (m *mockGenerate) Token() (s string, e error) { - return m.value, m.err -} - -func Test_Generate(t *testing.T) { - - t.Run("succeeds with funcMap", func(t *testing.T) { - var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"UNKNOWN://mountPath/token", "bar", nil} - return m, nil - } - - g := generator.NewGenerator(context.TODO(), func(gv *generator.GenVars) { - gv.Logger = log.New(&bytes.Buffer{}) - }) - g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) - got, err := g.Generate([]string{"UNKNOWN://mountPath/token"}) - - if err != nil { - t.Fatal("errored on generate") - } - if len(got) != 1 { - t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 1) - } - }) - - t.Run("errors in retrieval and logs it out", func(t *testing.T) { - var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"UNKNOWN://mountPath/token", "bar", fmt.Errorf("failed to get value")} - return m, nil - } - - g := generator.NewGenerator(context.TODO()) - g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) - got, err := g.Generate([]string{"UNKNOWN://mountPath/token"}) - - if err != nil { - t.Fatal("errored on generate") - } - if len(got) != 0 { - t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) - } - }) - - t.Run("retrieves values correctly from a keylookup inside", func(t *testing.T) { - var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"token-unused", `{"foo":"bar","key1":{"key2":"val"}}`, nil} - return m, nil - } - - g := generator.NewGenerator(context.TODO()) - g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) - got, err := g.Generate([]string{"UNKNOWN://mountPath/token|key1.key2"}) - - if err != nil { - t.Fatal("errored on generate") - } - if len(got) != 1 { - t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) - } - if got["UNKNOWN://mountPath/token|key1.key2"] != "val" { - t.Errorf(testutils.TestPhraseWithContext, "incorrect value returned in parsedMap", got["UNKNOWN://mountPath/token|key1.key2"], "val") - } - }) -} - -func Test_generate_withKeys_lookup(t *testing.T) { - ttests := map[string]struct { - custFunc strategy.StrategyFunc - token string - expectVal string - }{ - "retrieves string value correctly from a keylookup inside": { - custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":"val"}}`, nil} - return m, nil - }, - token: "UNKNOWN://mountPath/token|key1.key2", - expectVal: "val", - }, - "retrieves number value correctly from a keylookup inside": { - custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":123}}`, nil} - return m, nil - }, - token: "UNKNOWN://mountPath/token|key1.key2", - expectVal: "123", - }, - "retrieves nothing as keylookup is incorrect": { - custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"token", `{"foo":"bar","key1":{"key2":123}}`, nil} - return m, nil - }, - token: "UNKNOWN://mountPath/token|noprop", - expectVal: "", - }, - "retrieves value as is due to incorrectly stored json in backing store": { - custFunc: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"token", `foo":"bar","key1":{"key2":123}}`, nil} - return m, nil - }, - token: "UNKNOWN://mountPath/token|noprop", - expectVal: `foo":"bar","key1":{"key2":123}}`, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - g := generator.NewGenerator(context.TODO()) - g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: tt.custFunc}) - got, err := g.Generate([]string{tt.token}) - - if err != nil { - t.Fatal("errored on generate") - } - if len(got) != 1 { - t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) - } - if got[tt.token] != tt.expectVal { - t.Errorf(testutils.TestPhraseWithContext, "incorrect value returned in parsedMap", got[tt.token], tt.expectVal) - } - }) - } -} - -func Test_IsParsed(t *testing.T) { - ttests := map[string]struct { - val any - isParsed bool - }{ - "not parseable": { - `notparseable`, false, - }, - "one level parseable": { - `{"parseable":"foo"}`, true, - }, - "incorrect JSON": { - `parseable":"foo"}`, false, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - typ := generator.ParsedMap{} - got := generator.IsParsed(tt.val, typ) - if got != tt.isParsed { - t.Errorf(testutils.TestPhraseWithContext, "unexpected IsParsed", got, tt.isParsed) - } - }) - } -} - -// import ( -// "context" -// "fmt" -// "strings" -// "testing" - -// "github.com/DevLabFoundry/configmanager/v2/internal/testutils" -// ) - -// var ( -// customts = "___" -// customop = "/foo" -// standardop = "./app.env" -// standardts = "#" -// ) - -// type fixture struct { -// t *testing.T -// c *GenVars -// rs *retrieveStrategy -// } - -// func newFixture(t *testing.T) *fixture { -// f := &fixture{} -// f.t = t -// return f -// } - -// func (f *fixture) configGenVars(op, ts string) { -// conf := NewConfig().WithOutputPath(op).WithTokenSeparator(ts) -// gv := NewGenerator().WithConfig(conf) -// f.rs = newRetrieveStrategy(NewDefatultStrategy(), *conf) -// f.c = gv -// } - -// func TestGenVarsWithConfig(t *testing.T) { - -// f := newFixture(t) - -// f.configGenVars(customop, customts) -// if f.c.config.outpath != customop { -// f.t.Errorf(testutils.TestPhrase, f.c.config.outpath, customop) -// } -// if f.c.config.tokenSeparator != customts { -// f.t.Errorf(testutils.TestPhrase, f.c.config.tokenSeparator, customts) -// } -// } - -// func TestStripPrefixNormal(t *testing.T) { -// ttests := map[string]struct { -// prefix ImplementationPrefix -// token string -// keySeparator string -// tokenSeparator string -// f *fixture -// expect string -// }{ -// "standard azkv": {AzKeyVaultSecretsPrefix, "AZKVSECRET://vault1/secret2", "|", "://", newFixture(t), "vault1/secret2"}, -// "standard hashivault": {HashicorpVaultPrefix, "VAULT://vault1/secret2", "|", "://", newFixture(t), "vault1/secret2"}, -// "custom separator hashivault": {HashicorpVaultPrefix, "VAULT#vault1/secret2", "|", "#", newFixture(t), "vault1/secret2"}, -// } -// for name, tt := range ttests { -// t.Run(name, func(t *testing.T) { -// tt.f.configGenVars(tt.keySeparator, tt.tokenSeparator) -// got := tt.f.rs.stripPrefix(tt.token, tt.prefix) -// if got != tt.expect { -// t.Errorf(testutils.TestPhrase, got, tt.expect) -// } -// }) -// } -// } - -// func Test_stripPrefix(t *testing.T) { -// f := newFixture(t) -// f.configGenVars(standardop, standardts) -// tests := []struct { -// name string -// token string -// prefix ImplementationPrefix -// expect string -// }{ -// { -// name: "simple", -// token: fmt.Sprintf("%s#/test/123", SecretMgrPrefix), -// prefix: SecretMgrPrefix, -// expect: "/test/123", -// }, -// { -// name: "key appended", -// token: fmt.Sprintf("%s#/test/123|key", ParamStorePrefix), -// prefix: ParamStorePrefix, -// expect: "/test/123", -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got := f.rs.stripPrefix(tt.token, tt.prefix) -// if tt.expect != got { -// t.Errorf(testutils.TestPhrase, tt.expect, got) -// } -// }) -// } -// } - -// func Test_NormaliseMap(t *testing.T) { -// f := newFixture(t) -// f.configGenVars(standardop, standardts) -// tests := []struct { -// name string -// gv *GenVars -// input map[string]any -// expected string -// }{ -// { -// name: "foo->FOO", -// gv: f.c, -// input: map[string]any{"foo": "bar"}, -// expected: "FOO", -// }, -// { -// name: "num->NUM", -// gv: f.c, -// input: map[string]any{"num": 123}, -// expected: "NUM", -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got := f.c.envVarNormalize(tt.input) -// for k := range got { -// if k != tt.expected { -// t.Errorf(testutils.TestPhrase, tt.expected, k) -// } -// } -// }) -// } -// } - -// func Test_KeyLookup(t *testing.T) { -// f := newFixture(t) -// f.configGenVars(standardop, standardts) - -// tests := []struct { -// name string -// gv *GenVars -// val string -// key string -// expect string -// }{ -// { -// name: "lowercase key found in str val", -// gv: f.c, -// key: `something|key`, -// val: `{"key": "11235"}`, -// expect: "11235", -// }, -// { -// name: "lowercase key found in numeric val", -// gv: f.c, -// key: `something|key`, -// val: `{"key": 11235}`, -// expect: "11235", -// }, -// { -// name: "lowercase nested key found in numeric val", -// gv: f.c, -// key: `something|key.test`, -// val: `{"key":{"bar":"foo","test":12345}}`, -// expect: "12345", -// }, -// { -// name: "uppercase key found in val", -// gv: f.c, -// key: `something|KEY`, -// val: `{"KEY": "upposeres"}`, -// expect: "upposeres", -// }, -// { -// name: "uppercase nested key found in val", -// gv: f.c, -// key: `something|KEY.TEST`, -// val: `{"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// expect: "upposeres", -// }, -// { -// name: "no key found in val", -// gv: f.c, -// key: `something`, -// val: `{"key": "notfound"}`, -// expect: `{"key": "notfound"}`, -// }, -// { -// name: "nested key not found", -// gv: f.c, -// key: `something|KEY.KEY`, -// val: `{"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// expect: "", -// }, -// { -// name: "incorrect json", -// gv: f.c, -// key: "something|key", -// val: `"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// expect: `"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// }, -// { -// name: "no key provided", -// gv: f.c, -// key: "something", -// val: `{"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// expect: `{"KEY":{"BAR":"FOO","TEST":"upposeres"}}`, -// }, -// { -// name: "return json object", -// gv: f.c, -// key: "something|key.test", -// val: `{"key":{"bar":"foo","test": {"key": "default"}}}`, -// expect: `{"key": "default"}`, -// }, -// { -// name: "unescapable string", -// gv: f.c, -// key: "something|key.test", -// val: `{"key":{"bar":"foo","test":"\\\"upposeres\\\""}}`, -// expect: `\"upposeres\"`, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got := f.c.keySeparatorLookup(tt.key, tt.val) -// if got != tt.expect { -// t.Errorf(testutils.TestPhrase, got, tt.expect) -// } -// }) -// } -// } - -// type mockRetrieve struct //func(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) chanResp -// { -// r func(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) ChanResp -// s func(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) -// } - -// func (m mockRetrieve) RetrieveByToken(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) ChanResp { -// return m.r(ctx, impl, prefix, in) -// } -// func (m mockRetrieve) SelectImplementation(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) { -// return m.s(ctx, prefix, in, config) -// } - -// type mockImpl struct { -// token, value string -// err error -// } - -// func (m *mockImpl) tokenVal(rs *retrieveStrategy) (s string, e error) { -// return m.value, m.err -// } -// func (m *mockImpl) setTokenVal(s string) { -// m.token = s -// } - -// func Test_generate_rawmap_of_tokens_mapped_to_values(t *testing.T) { -// ttests := map[string]struct { -// rawMap func(t *testing.T) map[string]string -// rs func(t *testing.T) retrieveIface -// expectMap func() map[string]string -// }{ -// "success": { -// func(t *testing.T) map[string]string { -// rm := make(map[string]string) -// rm["foo"] = "bar" -// return rm -// }, -// func(t *testing.T) retrieveIface { -// return mockRetrieve{ -// r: func(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) ChanResp { -// return ChanResp{ -// err: nil, -// value: "bar", -// } -// }, -// s: func(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) { -// return &mockImpl{"foo", "bar", nil}, nil -// }} -// }, -// func() map[string]string { -// rm := make(map[string]string) -// rm["foo"] = "bar" -// return rm -// }, -// }, -// // as the method swallows errors at the moment this is not very useful -// "error in implementation": { -// func(t *testing.T) map[string]string { -// rm := make(map[string]string) -// rm["foo"] = "bar" -// return rm -// }, -// func(t *testing.T) retrieveIface { -// return mockRetrieve{ -// r: func(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) ChanResp { -// return ChanResp{ -// err: fmt.Errorf("unable to retrieve"), -// } -// }, -// s: func(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) { -// return &mockImpl{"foo", "bar", nil}, nil -// }} -// }, -// func() map[string]string { -// rm := make(map[string]string) -// return rm -// }, -// }, -// "error in imp selection": { -// func(t *testing.T) map[string]string { -// rm := make(map[string]string) -// rm["foo"] = "bar" -// return rm -// }, -// func(t *testing.T) retrieveIface { -// return mockRetrieve{ -// r: func(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) ChanResp { -// return ChanResp{ -// err: fmt.Errorf("unable to retrieve"), -// } -// }, -// s: func(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) { -// return nil, fmt.Errorf("implementation not found for input string: %s", in) -// }} -// }, -// func() map[string]string { -// rm := make(map[string]string) -// return rm -// }, -// }, -// } -// for name, tt := range ttests { -// t.Run(name, func(t *testing.T) { -// generator := newGenVars() -// generator.generate(tt.rawMap(t), tt.rs(t)) -// got := generator.RawMap() -// if len(got) != len(tt.expectMap()) { -// t.Errorf(testutils.TestPhraseWithContext, "generated raw map did not match", len(got), len(tt.expectMap())) -// } -// }) -// } -// } - -// func TestGenerate(t *testing.T) { -// ttests := map[string]struct { -// tokens func(t *testing.T) []string -// expectLength int -// }{ -// "success without correct prefix": { -// func(t *testing.T) []string { -// return []string{"WRONGIMPL://bar-vault/token1", "AZKVNOTSECRET://bar-vault/token1"} -// }, -// 0, -// }, -// } -// for name, tt := range ttests { -// t.Run(name, func(t *testing.T) { -// generator := newGenVars() -// pm, err := generator.Generate(tt.tokens(t)) -// if err != nil { -// t.Errorf(testutils.TestPhrase, err.Error(), nil) -// } -// if len(pm) < tt.expectLength { -// t.Errorf(testutils.TestPhrase, len(pm), tt.expectLength) -// } -// }) -// } -// } From 258473228356477d8d75d0cdbd67afad1665a951 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Mon, 24 Nov 2025 17:44:52 +0000 Subject: [PATCH 2/7] fix: experiment with WASI NOTE: it is a no go - but still valuable insight into sandboxing by the runtime --- cmd/configmanager/configmanager.go | 2 +- configmanager.go | 2 +- generator/generator.go | 32 ++-- generator/generator_test.go | 14 +- generator/generatorvars.go | 20 -- go.mod | 57 +++--- go.sum | 60 ++++++ internal/config/config.go | 47 ++++- internal/plugin/plugin.go | 16 ++ internal/plugin/tester/main.go | 46 +++++ internal/plugin/wasip1.go | 263 +++++++++++++++++++++++++++ internal/plugin/wasip1_test.go | 47 +++++ plugins/awsparams/main.go | 203 +++++++++++++++++++++ plugins/awsparams/paramstore.go | 67 +++++++ plugins/awsparams/paramstore_test.go | 152 ++++++++++++++++ plugins/awssecrets/main.go | 197 ++++++++++++++++++++ plugins/scaffolding.go | 30 +++ 17 files changed, 1173 insertions(+), 82 deletions(-) create mode 100644 internal/plugin/plugin.go create mode 100644 internal/plugin/tester/main.go create mode 100644 internal/plugin/wasip1.go create mode 100644 internal/plugin/wasip1_test.go create mode 100644 plugins/awsparams/main.go create mode 100644 plugins/awsparams/paramstore.go create mode 100644 plugins/awsparams/paramstore_test.go create mode 100644 plugins/awssecrets/main.go create mode 100644 plugins/scaffolding.go diff --git a/cmd/configmanager/configmanager.go b/cmd/configmanager/configmanager.go index 1eed6cb..383d229 100644 --- a/cmd/configmanager/configmanager.go +++ b/cmd/configmanager/configmanager.go @@ -73,7 +73,7 @@ func cmdutilsInit(rootCmd *Root, cmd *cobra.Command, path string) (*cmdutils.Cmd cm := configmanager.New(cmd.Context()) cm.Config.WithTokenSeparator(rootCmd.rootFlags.tokenSeparator).WithOutputPath(path).WithKeySeparator(rootCmd.rootFlags.keySeparator).WithEnvSubst(rootCmd.rootFlags.enableEnvSubst) - gnrtr := generator.NewGenerator(cmd.Context(), func(gv *generator.GenVars) { + gnrtr := generator.New(cmd.Context(), func(gv *generator.Generator) { if rootCmd.rootFlags.verbose { rootCmd.logger.SetLevel(log.DebugLvl) } diff --git a/configmanager.go b/configmanager.go index f3f6134..4da0d1b 100644 --- a/configmanager.go +++ b/configmanager.go @@ -46,7 +46,7 @@ type ConfigManager struct { func New(ctx context.Context) *ConfigManager { cm := &ConfigManager{} cm.Config = config.NewConfig() - cm.generator = generator.NewGenerator(ctx).WithConfig(cm.Config) + cm.generator = generator.New(ctx).WithConfig(cm.Config) cm.logger = log.New(io.Discard) return cm } diff --git a/generator/generator.go b/generator/generator.go index 237c6a6..8dcef19 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -17,32 +17,32 @@ import ( "github.com/DevLabFoundry/configmanager/v3/internal/strategy" ) -// GenVars is the main struct holding the +// Generator is the main struct holding the // strategy patterns iface // any initialised config if overridded with withers // as well as the final outString and the initial rawMap // which wil be passed in a loop into a goroutine to perform the // relevant strategy network calls to the config store implementations -type GenVars struct { +type Generator struct { Logger log.ILogger strategy strategy.StrategyFuncMap ctx context.Context config config.GenVarsConfig } -type Opts func(*GenVars) +type Opts func(*Generator) -// NewGenerator returns a new instance of Generator +// New returns a new instance of Generator // with a default strategy pattern wil be overwritten // during the first run of a found tokens map -func NewGenerator(ctx context.Context, opts ...Opts) *GenVars { +func New(ctx context.Context, opts ...Opts) *Generator { // defaultStrategy := NewDefatultStrategy() - return newGenVars(ctx, opts...) + return new(ctx, opts...) } -func newGenVars(ctx context.Context, opts ...Opts) *GenVars { +func new(ctx context.Context, opts ...Opts) *Generator { conf := config.NewConfig() - g := &GenVars{ + g := &Generator{ Logger: log.New(io.Discard), ctx: ctx, // return using default config @@ -61,13 +61,13 @@ func newGenVars(ctx context.Context, opts ...Opts) *GenVars { // WithStrategyMap // // Adds addtional funcs for storageRetrieval used for testing only -func (c *GenVars) WithStrategyMap(sm strategy.StrategyFuncMap) *GenVars { +func (c *Generator) WithStrategyMap(sm strategy.StrategyFuncMap) *Generator { c.strategy = sm return c } // WithConfig uses custom config -func (c *GenVars) WithConfig(cfg *config.GenVarsConfig) *GenVars { +func (c *Generator) WithConfig(cfg *config.GenVarsConfig) *Generator { // backwards compatibility if cfg != nil { c.config = *cfg @@ -76,13 +76,13 @@ func (c *GenVars) WithConfig(cfg *config.GenVarsConfig) *GenVars { } // WithContext uses caller passed context -func (c *GenVars) WithContext(ctx context.Context) *GenVars { +func (c *Generator) WithContext(ctx context.Context) *Generator { c.ctx = ctx return c } // Config gets Config on the GenVars -func (c *GenVars) Config() *config.GenVarsConfig { +func (c *Generator) Config() *config.GenVarsConfig { return &c.config } @@ -90,7 +90,7 @@ func (c *GenVars) Config() *config.GenVarsConfig { // the standard pattern of a token should follow a path like string // // Called only from a slice of tokens -func (c *GenVars) Generate(tokens []string) (ReplacedToken, error) { +func (c *Generator) Generate(tokens []string) (ReplacedToken, error) { ntm, err := c.DiscoverTokens(strings.Join(tokens, "\n")) if err != nil { @@ -112,7 +112,7 @@ var ErrTokenDiscovery = errors.New("failed to discover tokens") // the standard pattern of a token should follow a path like string // // Called only from a slice of tokens -func (c *GenVars) DiscoverTokens(text string) (NormalizedTokenSafe, error) { +func (c *Generator) DiscoverTokens(text string) (NormalizedTokenSafe, error) { rtm := NewRawTokenConfig() @@ -144,7 +144,7 @@ func IsParsed(v any, trm ReplacedToken) bool { // Captures the response/error in TokenResponse struct // It then denormalizes the NormalizedTokenSafe back to a ReplacedToken map // which stores the values for each token to be returned to the caller -func (c *GenVars) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { +func (c *Generator) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { if len(ntm.normalizedTokenMap) < 1 { c.Logger.Debug("no replaceable tokens found in input") return nil, nil @@ -232,7 +232,7 @@ func (n NormalizedTokenSafe) GetMap() map[string]*NormalizedToken { return n.normalizedTokenMap } -func (c *GenVars) NormalizeRawToken(rtm *RawTokenConfig) NormalizedTokenSafe { +func (c *Generator) NormalizeRawToken(rtm *RawTokenConfig) NormalizedTokenSafe { ntm := NormalizedTokenSafe{mu: &sync.Mutex{}, normalizedTokenMap: make(map[string]*NormalizedToken)} for _, r := range rtm.RawTokenMap() { diff --git a/generator/generator_test.go b/generator/generator_test.go index e48e546..d391d86 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -34,7 +34,7 @@ func TestGenerate(t *testing.T) { return m, nil } - g := generator.NewGenerator(context.TODO(), func(gv *generator.GenVars) { + g := generator.New(context.TODO(), func(gv *generator.Generator) { gv.Logger = log.New(&bytes.Buffer{}) }) g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) @@ -54,7 +54,7 @@ func TestGenerate(t *testing.T) { return m, nil } - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) got, err := g.Generate([]string{"AWSPARAMSTR://mountPath/token"}) @@ -72,7 +72,7 @@ func TestGenerate(t *testing.T) { return m, nil } - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: custFunc}) got, err := g.Generate([]string{"AWSPARAMSTR://mountPath/token|key1.key2"}) @@ -129,7 +129,7 @@ func TestGenerate_withKeys_lookup(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) g.WithStrategyMap(strategy.StrategyFuncMap{config.ParamStorePrefix: tt.custFunc}) got, err := g.Generate([]string{tt.token}) @@ -175,7 +175,7 @@ func Test_IsParsed(t *testing.T) { func TestGenVars_NormalizeRawToken(t *testing.T) { t.Run("multiple tokens", func(t *testing.T) { - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) input := `GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj GCPSECRETS:///djsfsdkjvfjkhfdvibdfinjdsfnjvdsflj|a @@ -298,7 +298,7 @@ func Test_ConfigManager_DiscoverTokens(t *testing.T) { for name, tt := range ttests { t.Run(name, func(t *testing.T) { config.VarPrefix = map[config.ImplementationPrefix]bool{"AWSPARAMSTR": true} - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) g.Config().WithTokenSeparator(tt.separator) gdt, err := g.DiscoverTokens(tt.input) if err != nil { @@ -319,7 +319,7 @@ func Test_ConfigManager_DiscoverTokens(t *testing.T) { } func Test_Generate_EnsureRaceFree(t *testing.T) { - g := generator.NewGenerator(context.TODO()) + g := generator.New(context.TODO()) input := ` fg diff --git a/generator/generatorvars.go b/generator/generatorvars.go index 79a56ae..d2ac986 100644 --- a/generator/generatorvars.go +++ b/generator/generatorvars.go @@ -43,26 +43,6 @@ func (rtm *RawTokenConfig) RawTokenMap() map[string]*config.ParsedTokenConfig { return rtm.tokenMap } -// type tokenMapSafe struct { -// mu *sync.Mutex -// tokenMap ReplacedToken -// } - -// func (tms *tokenMapSafe) getTokenMap() ReplacedToken { -// tms.mu.Lock() -// defer tms.mu.Unlock() -// return tms.tokenMap -// } - -// func (tms *tokenMapSafe) addKeyVal(key *config.ParsedTokenConfig, val string) { -// tms.mu.Lock() -// defer tms.mu.Unlock() -// // NOTE: still use the metadata in the key -// // there could be different versions / labels for the same token and hence different values -// // However the JSONpath look up -// tms.tokenMap[key.String()] = keySeparatorLookup(key, val) -// } - // keySeparatorLookup checks if the key contains // keySeparator character // If it does contain one then it tries to parse diff --git a/go.mod b/go.mod index 1d37d5a..bf22cb0 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ module github.com/DevLabFoundry/configmanager/v3 -go 1.25.3 +go 1.25.4 require ( cloud.google.com/go/secretmanager v1.16.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 - github.com/aws/aws-sdk-go-v2 v1.39.6 - github.com/aws/aws-sdk-go-v2/config v1.31.17 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.11 - github.com/aws/aws-sdk-go-v2/service/ssm v1.66.4 + github.com/aws/aws-sdk-go-v2 v1.40.0 + github.com/aws/aws-sdk-go-v2/config v1.32.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 + github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 github.com/go-test/deep v1.1.1 github.com/googleapis/gax-go/v2 v2.15.0 github.com/hashicorp/vault/api v1.22.0 @@ -22,6 +22,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect + require ( cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -32,16 +34,16 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/a8m/envsubst v1.4.3 github.com/aws/aws-sdk-go v1.55.8 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect github.com/aws/smithy-go v1.23.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/fatih/color v1.18.0 // indirect @@ -76,23 +78,24 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/tetratelabs/wazero v1.10.1 go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.255.0 // indirect - google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/api v0.256.0 // indirect + google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index f40c7f4..a8b3951 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= @@ -12,14 +13,20 @@ cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYz cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 h1:uU4FujKFQAz31AbWOO3INV9qfIanHeIUSsGhRlcJJmg= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0/go.mod h1:qr3M3Oy6V98VR0c5tCHKUpaeJTRQh6KYzJewRtFWqfc= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0 h1:mXlQ+2C8A4KpXTIIYYxgFYqSivjGTBQidq/b0xxZLuk= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0/go.mod h1:K//Ck7MUa+r9jpV69WLeWnnju5WJx5120AFsEzvumII= +github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 h1:j0hhYS006eJ54vusoap0f2NVZ1YY3QnaAEnLM68f0SQ= +github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1/go.mod h1:AdtInaXmK8eYmbjezRWgLz+Qs46nc9Up9GWGwteWNfw= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= @@ -37,46 +44,75 @@ github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= +github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= +github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA= +github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw= github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M= +github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.11 h1:DouhxUREBjfnNJFp1yNn/p1Gk5pzr1YNixcIOIudI2g= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.11/go.mod h1:QgVIY03/XoQs2iFr0MbQuQ/Tf1RwlkOvuySWMh1wph4= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= github.com/aws/aws-sdk-go-v2/service/ssm v1.66.4 h1:UmkF0ipNy0Ps6csJl/ZRJ3K+DWe9q0A7LT3xfxoHbgg= github.com/aws/aws-sdk-go-v2/service/ssm v1.66.4/go.mod h1:uNHuYAQazkHqpD+hVomA2+eDSuKJzerno7Fnha6N6/Y= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 h1:ofiQvKwka2E3T8FXBsU1iWj7Yvk2wd1p4ZCdS6qGiKQ= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3/go.mod h1:+nlWvcgDPQ56mChEBzTC0puAMck+4onOFaHg5cE+Lgg= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -200,6 +236,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= +github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -219,13 +257,21 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -236,23 +282,37 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4= google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 h1:MgBTzgUJFAmp2PlyqKJecSpZpjFxkYL3nDUIeH/6Q30= google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101/go.mod h1:bbWg36d7wp3knc0hIlmJAnW5R/CQ2rzpEVb72eH4ex4= +google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY= +google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI= google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM= google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index d1d05ed..a1898fc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "strings" + + "github.com/DevLabFoundry/configmanager/v3/plugins" ) const ( @@ -171,6 +173,18 @@ func (ptc *ParsedTokenConfig) WithSanitizedToken(v string) { } func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { + // empty map will be parsed as `{}` still resulting in a valid json + // and successful unmarshalling but default value pointer struct + if err := json.Unmarshal(fmt.Appendf(nil, t.parseMetadata()), metadataTyp); err != nil { + // It would very hard to test this since + // we are forcing the key and value to be strings + // return non-filled pointer + return err + } + return nil +} + +func (t *ParsedTokenConfig) parseMetadata() string { // crude json like builder from key/val tags // since we are only ever dealing with a string input // extracted from the token there is little chance panic would occur here @@ -182,16 +196,7 @@ func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { metaMap = append(metaMap, fmt.Sprintf(`"%s":"%s"`, mapKeyVal[0], mapKeyVal[1])) } } - - // empty map will be parsed as `{}` still resulting in a valid json - // and successful unmarshalling but default value pointer struct - if err := json.Unmarshal(fmt.Appendf(nil, `{%s}`, strings.Join(metaMap, ",")), metadataTyp); err != nil { - // It would very hard to test this since - // we are forcing the key and value to be strings - // return non-filled pointer - return err - } - return nil + return fmt.Sprintf(`{%s}`, strings.Join(metaMap, ",")) } // StoreToken returns the sanitized token without: @@ -243,3 +248,25 @@ func (t *ParsedTokenConfig) Prefix() ImplementationPrefix { func (t *ParsedTokenConfig) TokenSeparator() string { return t.tokenSeparator } + +func (t *ParsedTokenConfig) JSONMessagExchange() (*plugins.MessagExchange, error) { + md := map[string]any{} + if err := json.Unmarshal([]byte(t.parseMetadata()), &md); err != nil { + return nil, err + } + + jme := &plugins.MessagExchange{ + Token: t.StoreToken(), + Metadata: md, + } + + return jme, nil +} + +func (t *ParsedTokenConfig) JSONMessagExchangeBytes() ([]byte, error) { + j, err := t.JSONMessagExchange() + if err != nil { + return nil, err + } + return json.Marshal(j) +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 0000000..5eb80c7 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,16 @@ +package plugin + +import "github.com/DevLabFoundry/configmanager/v3/internal/config" + +// Plugin is responsible for managing plugins within configmanager +// +// It includes the following methods +// - fetch plugins from known sources +// - maintains a list of tokens answerable by a specified pluginEngine +type Plugin struct { + Implementations config.ImplementationPrefix + SourcePath string + Version string + fallbackPaths []string + engineInstance *Engine +} diff --git a/internal/plugin/tester/main.go b/internal/plugin/tester/main.go new file mode 100644 index 0000000..b17e481 --- /dev/null +++ b/internal/plugin/tester/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/plugin" +) + +func main() { + inputReader, err := os.Open("/Users/dusannitschneider/git/dnitsch/configmanager/plugins/awsparams/awsparams.wasm") + if err != nil { + log.Fatal(fmt.Errorf("open plugin.wasm: %w", err)) + } + + ctx := context.Background() + + // Load the compiled WASI plugin. + engine, err := plugin.NewEngine(ctx, inputReader) + if err != nil { + log.Fatal(err) + } + defer engine.Close(ctx) + + inst, err := engine.NewApiInstance(ctx) + if err != nil { + log.Fatal(err) + } + defer inst.Close(ctx) + + // os.Setenv("AWS_PROFILE", "anabode_terraform_dev") + // os.Setenv("AWS_REGION", "eu-west-1") + t1, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + t1.WithSanitizedToken("/int-test/pocketbase/admin-pwd") + val, err := inst.TokenValue(ctx, t1) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(val)) + + os.Exit(0) +} diff --git a/internal/plugin/wasip1.go b/internal/plugin/wasip1.go new file mode 100644 index 0000000..268371f --- /dev/null +++ b/internal/plugin/wasip1.go @@ -0,0 +1,263 @@ +// Package plugin +// provides reactor style module +// we can explore the plugin provided host module +package plugin + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/plugins" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" +) + +var ( + ErrMissingMethod = errors.New("missing method on the wasiLib instance") + ErrAllocMemForParam = errors.New("failed to allocate memory for property") + ErrAllocateOutPtrZeroLen = errors.New("allocate returned 0 for output pointer") + ErrMemoryReadFailed = errors.New("mem.Read(out) failed") + ErrEmptyToken = errors.New("token must not be empty") +) + +// ==================== +// Engine & ApiInstance +// ==================== + +type Engine struct { + r wazero.Runtime + compiledModule wazero.CompiledModule +} + +// NewEngine compiles the WASI module once for the lifetime of the program. +func NewEngine(ctx context.Context, ps io.ReadCloser) (*Engine, error) { + r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig()) + if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { + _ = r.Close(ctx) + return nil, fmt.Errorf("instantiate WASI: %w", err) + } + + defer ps.Close() + wasiLib, err := io.ReadAll(ps) + if err != nil { + _ = r.Close(ctx) + return nil, fmt.Errorf("read plugin: %w", err) + } + + cm, err := r.CompileModule(ctx, wasiLib) + if err != nil { + _ = r.Close(ctx) + return nil, fmt.Errorf("compile module: %w", err) + } + + return &Engine{ + r: r, + compiledModule: cm, + }, nil +} + +// Close shuts down the runtime. +func (e *Engine) Close(ctx context.Context) error { + return e.r.Close(ctx) +} + +type ApiInstance struct { + mod api.Module + mem api.Memory + // exported alloc helpers + allocate api.Function + deallocate api.Function + // exported business function + tokenValue api.Function + // scratch output buffers + outPtr uint32 + outCap uint32 + outLenPtr uint32 // 4-byte cell for required length +} + +// NewApiInstance instantiates a fresh module instance. +func (e *Engine) NewApiInstance(ctx context.Context) (*ApiInstance, error) { + mod, err := e.r.InstantiateModule(ctx, e.compiledModule, wazero.NewModuleConfig().WithStartFunctions("_initialize")) + if err != nil { + return nil, fmt.Errorf("instantiate module: %w", err) + } + + inst := &ApiInstance{ + mod: mod, + mem: mod.Memory(), + allocate: mod.ExportedFunction("allocate"), + deallocate: mod.ExportedFunction("deallocate"), + tokenValue: mod.ExportedFunction("strategy_token_value"), + } + + for name, exported := range map[string]api.Function{ + "allocate": inst.allocate, + "deallocate": inst.deallocate, + "strategy_token_value": inst.tokenValue, + } { + if exported == nil { + return nil, fmt.Errorf("%w, method %q not found on exports", ErrMissingMethod, name) + } + } + + return inst, nil +} + +// Close instance (optional). +func (i *ApiInstance) Close(ctx context.Context) { + i.freeScratch(ctx) + _ = i.mod.Close(ctx) +} + +// put allocates module memory and writes bytes into it. +// returns (ptr, size). caller must deallocate(ptr, size). +func (i *ApiInstance) put(ctx context.Context, b []byte) (uint32, uint32, error) { + if len(b) == 0 { + return 0, 0, ErrEmptyToken + } + + res, err := i.allocate.Call(ctx, uint64(len(b))) + if err != nil { + return 0, 0, fmt.Errorf("allocate: %w", err) + } + ptr := uint32(res[0]) + if ptr == 0 { + return 0, 0, fmt.Errorf("allocate returned 0: %w", ErrAllocMemForParam) + } + + if ok := i.mem.Write(ptr, b); !ok { + _, _ = i.deallocate.Call(ctx, uint64(ptr), uint64(len(b))) + return 0, 0, fmt.Errorf("mem.Write failed: %w", ErrAllocMemForParam) + } + + return ptr, uint32(len(b)), nil +} + +// ensureOut ensures the scratch output buffer has at least `need` bytes. +// allocates outLenPtr (4 bytes) once. +func (i *ApiInstance) ensureOut(ctx context.Context, need uint32) error { + // outLenPtr is a 4-byte cell for required length + if i.outLenPtr == 0 { + res, err := i.allocate.Call(ctx, 4) + if err != nil { + return fmt.Errorf("allocate outLenPtr: %w", err) + } + i.outLenPtr = uint32(res[0]) + if i.outLenPtr == 0 { + return ErrAllocateOutPtrZeroLen + } + } + + if need <= i.outCap { + return nil + } + + // grow if needed - free old and alloc new + if i.outPtr != 0 { + _, _ = i.deallocate.Call(ctx, uint64(i.outPtr), uint64(i.outCap)) + i.outPtr, i.outCap = 0, 0 + } + + res, err := i.allocate.Call(ctx, uint64(need)) + if err != nil { + return fmt.Errorf("allocate outPtr: %w", err) + } + i.outPtr, i.outCap = uint32(res[0]), need + if i.outPtr == 0 { + return ErrAllocateOutPtrZeroLen + } + return nil +} + +// freeScratch frees the reusable output buffers (call once per instance). +func (i *ApiInstance) freeScratch(ctx context.Context) { + if i.outPtr != 0 { + _, _ = i.deallocate.Call(ctx, uint64(i.outPtr), uint64(i.outCap)) + i.outPtr, i.outCap = 0, 0 + } + if i.outLenPtr != 0 { + _, _ = i.deallocate.Call(ctx, uint64(i.outLenPtr), 4) + i.outLenPtr = 0 + } +} + +// TokenValue is the nice host-side API: string in, []byte out. +func (i *ApiInstance) TokenValue(ctx context.Context, token *config.ParsedTokenConfig) ([]byte, error) { + if token.StoreToken() == "" { + return nil, ErrEmptyToken + } + tokenBytes, err := token.JSONMessagExchangeBytes() + tokenPtr, tokenLen, err := i.put(ctx, tokenBytes) + if err != nil { + return nil, fmt.Errorf("put input: %w", err) + } + defer i.deallocate.Call(ctx, uint64(tokenPtr), uint64(tokenLen)) + + // start with a smallish buffer; plugin will ask for more if needed + if err := i.ensureOut(ctx, 64); err != nil { + return nil, fmt.Errorf("ensureOut: %w", err) + } + + call := func() (int32, uint32, error) { + res, err := i.tokenValue.Call( + ctx, + uint64(tokenPtr), uint64(tokenLen), // sanitizedToken + uint64(i.outPtr), uint64(i.outCap), // outPtr, outCap + uint64(i.outLenPtr), // outLenPtr + ) + if err != nil { + return 0, 0, fmt.Errorf("call strategy_token_value: %w", err) + } + + lenBytes, ok := i.mem.Read(i.outLenPtr, 4) + if !ok { + return int32(res[0]), 0, ErrMemoryReadFailed + } + + need := binary.LittleEndian.Uint32(lenBytes) + return int32(res[0]), need, nil + } + + rc, need, err := call() + if err != nil { + return nil, err + } + + if rc == plugins.ERR_BUF_TOO_SMALL { + if err := i.ensureOut(ctx, need); err != nil { + return nil, fmt.Errorf("ensureOut resize: %w", err) + } + rc, need, err = call() + if err != nil { + return nil, err + } + } + + if rc != plugins.OK { + switch rc { + case plugins.ERR_INVALID_UTF8: + return nil, errors.New("token value: invalid UTF-8 in input") + case plugins.ERR_EMPTY_INPUT: + return nil, ErrEmptyToken + case plugins.ERR_BUF_TOO_SMALL: + return nil, fmt.Errorf("token value: buffer too small even after resize (need=%d)", need) + default: + return nil, fmt.Errorf("token value: unknown error code %d", rc) + } + } + + out, ok := i.mem.Read(i.outPtr, need) + if !ok { + return nil, ErrMemoryReadFailed + } + + // Detach from wasm memory. + result := make([]byte, need) + copy(result, out) + return result, nil +} diff --git a/internal/plugin/wasip1_test.go b/internal/plugin/wasip1_test.go new file mode 100644 index 0000000..5442eb1 --- /dev/null +++ b/internal/plugin/wasip1_test.go @@ -0,0 +1,47 @@ +package plugin_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/plugin" +) + +func Test_FullFlow(t *testing.T) { + inputReader, err := os.Open("/Users/dusannitschneider/git/dnitsch/configmanager/plugins/awsparams/awsparams.wasm") + if err != nil { + t.Fatal(fmt.Errorf("open plugin.wasm: %w", err)) + } + ctx := context.Background() + + // Load the compiled WASI plugin. + engine, err := plugin.NewEngine(ctx, inputReader) + if err != nil { + t.Fatal(err) + } + defer engine.Close(ctx) + + inst, err := engine.NewApiInstance(ctx) + if err != nil { + t.Fatal(err) + } + defer inst.Close(ctx) + + os.Setenv("AWS_PROFILE", "anabode_terraform_dev") + os.Setenv("AWS_REGION", "eu-west-1") + t1, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + t1.WithSanitizedToken("/int-test/pocketbase/admin-pwd") + val, err := inst.TokenValue(ctx, t1) + if err != nil { + t.Fatal(err) + } + fmt.Printf("TokenValue(\"foo\") => %q\n", string(val)) + + // Zero-length test (should error) + t2, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + _, err = inst.TokenValue(ctx, t2) + fmt.Printf("TokenValue(\"\") error: %v\n", err) +} diff --git a/plugins/awsparams/main.go b/plugins/awsparams/main.go new file mode 100644 index 0000000..4bd4ec6 --- /dev/null +++ b/plugins/awsparams/main.go @@ -0,0 +1,203 @@ +package main + +import ( + "context" + "encoding/binary" + "encoding/json" + "sync" + "unicode/utf8" + "unsafe" + + "github.com/DevLabFoundry/configmanager/v3/plugins" +) + +// ==================== +// Bump allocator +// ==================== + +const heapSize = 64 * 1024 // 64 KiB arena; tune as needed + +type bumpAllocator struct { + mu sync.Mutex + heap []byte + used uint32 +} + +var alloc = bumpAllocator{ + heap: make([]byte, heapSize), +} + +// round allocation up to 8 bytes for basic alignment. +func roundUp(n uint32) uint32 { + const align = 8 + return (n + align - 1) &^ (align - 1) +} + +//go:wasmexport allocate +func Allocate(size uint32) uint32 { + if size == 0 { + return 0 + } + size = roundUp(size) + + alloc.mu.Lock() + defer alloc.mu.Unlock() + + if alloc.used+size > uint32(len(alloc.heap)) { + // Out of memory in our arena. + return 0 + } + + offset := alloc.used + alloc.used += size + + // Return pointer into linear memory for &heap[offset]. + return uint32(uintptr(unsafe.Pointer(&alloc.heap[offset]))) +} + +//go:wasmexport deallocate +func Deallocate(ptr, size uint32) { + // For a simple bump allocator, deallocate is a no-op. + // Memory is reclaimed when the module instance is destroyed. + _ = ptr + _ = size +} + +type Hdr struct { + Data uintptr + Len int + Cap int +} + +// ==================== +// Helpers +// ==================== + +// bytesFromPtrLen reinterprets a (ptr,len) pair in wasm linear memory +// as a Go []byte without copying. +func bytesFromPtrLen(ptr, length uint32) []byte { + if length == 0 { + return nil + } + + hdr := Hdr{ + Data: uintptr(ptr), + Len: int(length), + Cap: int(length), + } + + return *(*[]byte)(unsafe.Pointer(&hdr)) +} + +// ==================== +// strategy_token_value +// ==================== +// +// ABI: +// +// strategy_token_value( +// inPtr, inLen, outPtr, outCap, outLenPtr uint32, +// ) int32 +// +// Host contract: +// - Input bytes are at (inPtr, inLen) +// - Output buffer is [outPtr : outPtr+outCap) +// - outLenPtr points to 4 bytes where we write the required length +// +// Behaviour: +// - If input length == 0 => ERR_EMPTY_INPUT +// - If invalid UTF-8 => ERR_INVALID_UTF8 +// - Always write required length to *outLenPtr (little-endian) +// - If required > outCap => ERR_BUF_TOO_SMALL +// - Else copy into caller buffer and return OK +// +//go:wasmexport strategy_token_value +func StrategyTokenValue(tokenPtr, tokenLen, outPtr, outCap, outLenPtr uint32) int32 { + defer func() { + // Make sure panics don't leak as traps. + if r := recover(); r != nil { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + } + }() + + if tokenLen == 0 { + // Mark required length as 0 and signal error. + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + return plugins.ERR_EMPTY_INPUT + } + + tokenBytes := bytesFromPtrLen(tokenPtr, tokenLen) + if !utf8.Valid(tokenBytes) { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) + } + } + return plugins.ERR_INVALID_UTF8 + } + + // --- Business logic (replace with your real token strategy) --- + // unmarshal string into an object + token := &plugins.MessagExchange{} + if err := json.Unmarshal(tokenBytes, token); err != nil { + return plugins.ERR_FAILED_UNMARSHAL_MESSAGE + } + + // logger := log.New(os.Stdout) + // logger.SetLevel(log.DebugLvl) + + store, err := NewParamStore(context.Background()) + if err != nil { + return plugins.ERR_INIT_STORE + } + + outStr, err := store.Value(token) + if err != nil { + return plugins.ERR_FAILED_VALUE_RETRIEVAL + } + + outBytes := []byte(outStr) + // -------------------------------------------------------------- + // BEGIN RETURN Allocation + // -------------------------------------------------------------- + required := uint32(len(outBytes)) + + // Always write required length. + if outLenPtr != 0 { + lenCell := bytesFromPtrLen(outLenPtr, 4) + if len(lenCell) != 4 { + return plugins.ERR_INTERNAL + } + binary.LittleEndian.PutUint32(lenCell, required) + } + + if required > outCap { + return plugins.ERR_BUF_TOO_SMALL + } + + if required == 0 { + return plugins.OK + } + + outSlice := bytesFromPtrLen(outPtr, outCap) + if uint32(len(outSlice)) < required { + return plugins.ERR_INTERNAL + } + + copy(outSlice, outBytes) + return plugins.OK +} + +// main is required for wasip1 +// scaffolds the `_initialize` method +func main() {} + +// GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awsparams.wasm diff --git a/plugins/awsparams/paramstore.go b/plugins/awsparams/paramstore.go new file mode 100644 index 0000000..9001e53 --- /dev/null +++ b/plugins/awsparams/paramstore.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/plugins" + "github.com/aws/aws-sdk-go-v2/aws" + awsConf "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" +) + +type paramStoreApi interface { + GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) +} + +type ParamStore struct { + svc paramStoreApi + ctx context.Context + config *ParamStrConfig + token *config.ParsedTokenConfig +} + +type ParamStrConfig struct { + // reserved for potential future use +} + +func NewParamStore(ctx context.Context) (*ParamStore, error) { + cfg, err := awsConf.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + c := ssm.NewFromConfig(cfg) + + return &ParamStore{ + svc: c, + ctx: ctx, + }, nil +} + +func (s *ParamStore) WithSvc(svc paramStoreApi) { + s.svc = svc +} + +func (imp *ParamStore) Value(token *plugins.MessagExchange) (string, error) { + // imp.logger.Info("%s", "Concrete implementation ParameterStore") + // imp.logger.Info("ParamStore Token: %s", token.Token) + + input := &ssm.GetParameterInput{ + Name: aws.String(token.Token), + WithDecryption: aws.Bool(true), + } + ctx, cancel := context.WithCancel(imp.ctx) + defer cancel() + + result, err := imp.svc.GetParameter(ctx, input) + if err != nil { + // imp.logger.Error(plugins.ImplementationNetworkErr, config.ParamStorePrefix, err, token) + return "", err + } + + if result.Parameter.Value != nil { + return *result.Parameter.Value, nil + } + // imp.logger.Error("value retrieved but empty for token: %v", imp.token) + return "", nil +} diff --git a/plugins/awsparams/paramstore_test.go b/plugins/awsparams/paramstore_test.go new file mode 100644 index 0000000..84a0d23 --- /dev/null +++ b/plugins/awsparams/paramstore_test.go @@ -0,0 +1,152 @@ +package main_test + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/store" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" +) + +type mockParamApi func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) + +func (m mockParamApi) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + return m(ctx, params, optFns...) +} + +func awsParamtStoreCommonGetChecker(t *testing.T, params *ssm.GetParameterInput) { + if params.Name == nil { + t.Fatal("expect name to not be nil") + } + + if strings.Contains(*params.Name, "#") { + t.Errorf("incorrectly stripped token separator") + } + + if strings.Contains(*params.Name, string(config.ParamStorePrefix)) { + t.Errorf("incorrectly stripped prefix") + } + + if !*params.WithDecryption { + t.Fatal("expect WithDecryption to not be false") + } +} + +func Test_GetParamStore(t *testing.T) { + var ( + tsuccessParam = "someVal" + // tsuccessObj map[string]string = map[string]string{"AWSPARAMSTR#/token/1": "someVal"} + ) + tests := map[string]struct { + token func() *config.ParsedTokenConfig + expect string + mockClient func(t *testing.T) mockParamApi + }{ + "successVal": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/demo/configmanager" + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + tsuccessParam, func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: &tsuccessParam}, + }, nil + }) + }, + }, + "successVal with keyseparator": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1|somekey", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("somekey") + tkn.WithMetadata("") + return tkn + }, + tsuccessParam, func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + + if strings.Contains(*params.Name, "|somekey") { + t.Errorf("incorrectly stripped key separator") + } + + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: &tsuccessParam}, + }, nil + }) + }, + }, + "errored": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "unable to retrieve", func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return nil, fmt.Errorf("unable to retrieve") + }) + }, + }, + "nil to empty": { + func() *config.ParsedTokenConfig { + // "AWSPARAMSTR#/token/1", + tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/token/1") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "", func(t *testing.T) mockParamApi { + return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { + t.Helper() + awsParamtStoreCommonGetChecker(t, params) + return &ssm.GetParameterOutput{ + Parameter: &types.Parameter{Value: nil}, + }, nil + }) + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + impl, err := store.NewParamStore(context.TODO(), log.New(io.Discard)) + if err != nil { + t.Errorf(testutils.TestPhrase, err.Error(), nil) + } + impl.WithSvc(tt.mockClient(t)) + impl.SetToken(tt.token()) + got, err := impl.Value() + if err != nil { + if err.Error() != tt.expect { + t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) + } + return + } + if got != tt.expect { + t.Errorf(testutils.TestPhrase, got, tt.expect) + } + }) + } +} diff --git a/plugins/awssecrets/main.go b/plugins/awssecrets/main.go new file mode 100644 index 0000000..8a03b12 --- /dev/null +++ b/plugins/awssecrets/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "encoding/binary" + "strings" + "sync" + "unicode/utf8" + "unsafe" + + "github.com/DevLabFoundry/configmanager/v3/plugins" +) + +// ==================== +// Bump allocator +// ==================== + +const heapSize = 64 * 1024 // 64 KiB arena; tune as needed + +type bumpAllocator struct { + mu sync.Mutex + heap []byte + used uint32 +} + +var alloc = bumpAllocator{ + heap: make([]byte, heapSize), +} + +// round allocation up to 8 bytes for basic alignment. +func roundUp(n uint32) uint32 { + const align = 8 + return (n + align - 1) &^ (align - 1) +} + +//go:wasmexport allocate +func Allocate(size uint32) uint32 { + if size == 0 { + return 0 + } + size = roundUp(size) + + alloc.mu.Lock() + defer alloc.mu.Unlock() + + if alloc.used+size > uint32(len(alloc.heap)) { + // Out of memory in our arena. + return 0 + } + + offset := alloc.used + alloc.used += size + + // Return pointer into linear memory for &heap[offset]. + return uint32(uintptr(unsafe.Pointer(&alloc.heap[offset]))) +} + +//go:wasmexport deallocate +func Deallocate(ptr, size uint32) { + // For a simple bump allocator, deallocate is a no-op. + // Memory is reclaimed when the module instance is destroyed. + _ = ptr + _ = size +} + +type Hdr struct { + Data uintptr + Len int + Cap int +} + +// ==================== +// Helpers +// ==================== + +// bytesFromPtrLen reinterprets a (ptr,len) pair in wasm linear memory +// as a Go []byte without copying. +func bytesFromPtrLen(ptr, length uint32) []byte { + if length == 0 { + return nil + } + + hdr := Hdr{ + Data: uintptr(ptr), + Len: int(length), + Cap: int(length), + } + + return *(*[]byte)(unsafe.Pointer(&hdr)) +} + +// ==================== +// strategy_token_value +// ==================== +// +// ABI: +// +// strategy_token_value( +// inPtr, inLen, outPtr, outCap, outLenPtr uint32, +// ) int32 +// +// Host contract: +// - Input bytes are at (inPtr, inLen) +// - Output buffer is [outPtr : outPtr+outCap) +// - outLenPtr points to 4 bytes where we write the required length +// +// Behaviour: +// - If input length == 0 => ERR_EMPTY_INPUT +// - If invalid UTF-8 => ERR_INVALID_UTF8 +// - Always write required length to *outLenPtr (little-endian) +// - If required > outCap => ERR_BUF_TOO_SMALL +// - Else copy into caller buffer and return OK +// +//go:wasmexport strategy_token_value +func StrategyTokenValue(tokenPtr, tokenLen, metadataStrPtr, metadataStrLen, outPtr, outCap, outLenPtr uint32) int32 { + defer func() { + // Make sure panics don't leak as traps. + if r := recover(); r != nil { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + } + }() + + if tokenLen == 0 { + // Mark required length as 0 and signal error. + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + return plugins.ERR_EMPTY_INPUT + } + + tokenBytes := bytesFromPtrLen(tokenPtr, tokenLen) + if !utf8.Valid(tokenBytes) { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) + } + } + return plugins.ERR_INVALID_UTF8 + } + + if metadataStrLen > 0 { + metadataStrBytes := bytesFromPtrLen(metadataStrPtr, metadataStrLen) + if !utf8.Valid(metadataStrBytes) { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(metadataStrPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) + } + } + return plugins.ERR_INVALID_UTF8 + } + } + + // --- Business logic (replace with your real token strategy) --- + + inStr := string(tokenBytes) + outStr := "TOKEN_VALUE:" + strings.ToUpper(inStr) + outBytes := []byte(outStr) + // -------------------------------------------------------------- + + required := uint32(len(outBytes)) + + // Always write required length. + if outLenPtr != 0 { + lenCell := bytesFromPtrLen(outLenPtr, 4) + if len(lenCell) != 4 { + return plugins.ERR_INTERNAL + } + binary.LittleEndian.PutUint32(lenCell, required) + } + + if required > outCap { + return plugins.ERR_BUF_TOO_SMALL + } + + if required == 0 { + return plugins.OK + } + + outSlice := bytesFromPtrLen(outPtr, outCap) + if uint32(len(outSlice)) < required { + return plugins.ERR_INTERNAL + } + + copy(outSlice, outBytes) + return plugins.OK +} + +// main is required for wasip1 +// scaffolds the `_instantiate` method +func main() {} + +// GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awssecrets.wasm diff --git a/plugins/scaffolding.go b/plugins/scaffolding.go new file mode 100644 index 0000000..f4bdce0 --- /dev/null +++ b/plugins/scaffolding.go @@ -0,0 +1,30 @@ +package plugins + +import "errors" + +// Error codes shared with the host. +const ( + OK int32 = 0 + ERR_BUF_TOO_SMALL int32 = -1 + ERR_INVALID_UTF8 int32 = -2 + ERR_EMPTY_INPUT int32 = -3 + ERR_INTERNAL int32 = -4 + ERR_FAILED_UNMARSHAL_MESSAGE int32 = -5 + ERR_INIT_STORE int32 = -6 + ERR_FAILED_VALUE_RETRIEVAL int32 = -7 +) + +const ImplementationNetworkErr string = "implementation %s error: %v for token: %s" + +var ( + ErrRetrieveFailed = errors.New("failed to retrieve config item") + ErrClientInitialization = errors.New("failed to initialize the client") + ErrEmptyResponse = errors.New("value retrieved but empty for token") + ErrServiceCallFailed = errors.New("failed to complete the service call") +) + +type MessagExchange struct { + Token string `json:"token"` + Metadata map[string]any `json:"metadata,omitempty"` + Version string `json:"version"` +} From 6b8689c05de0d39fe45f6e69ac61cba73840308c Mon Sep 17 00:00:00 2001 From: dnitsch Date: Mon, 24 Nov 2025 17:45:55 +0000 Subject: [PATCH 3/7] fix: release container --- .github/workflows/release_container.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_container.yml b/.github/workflows/release_container.yml index 9a1c45d..3ac8556 100644 --- a/.github/workflows/release_container.yml +++ b/.github/workflows/release_container.yml @@ -2,7 +2,7 @@ name: Publish Container on: workflow_run: - workflows: ['Lint and Test'] + workflows: ['CI'] types: - completed branches: From f0582ec9a4e8cdd5cab93c5ea655410ec9030733 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Wed, 26 Nov 2025 09:29:15 +0000 Subject: [PATCH 4/7] fix: interim goplugin from hashicorp --- go.mod | 8 ++++- go.sum | 6 ++++ internal/plugin/plugin.go | 62 ++++++++++++++++++++++++++++++++++++- internal/plugin/provider.go | 62 +++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 internal/plugin/provider.go diff --git a/go.mod b/go.mod index bf22cb0..a8074af 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,12 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect +require ( + github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/oklog/run v1.1.0 // indirect +) require ( cloud.google.com/go/auth v0.17.0 // indirect @@ -59,6 +64,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect diff --git a/go.sum b/go.sum index a8b3951..f7638e0 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -174,6 +176,8 @@ github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicH github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/hashicorp/vault/api/auth/aws v0.11.0 h1:lWdUxrzvPotg6idNr62al4w97BgI9xTDdzMCTViNH2s= github.com/hashicorp/vault/api/auth/aws v0.11.0/go.mod h1:PWqdH/xqaudapmnnGP9ip2xbxT/kRW2qEgpqiQff6Gc= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= @@ -205,6 +209,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 5eb80c7..24c2b48 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -1,6 +1,11 @@ package plugin -import "github.com/DevLabFoundry/configmanager/v3/internal/config" +import ( + "net/rpc" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/hashicorp/go-plugin" +) // Plugin is responsible for managing plugins within configmanager // @@ -14,3 +19,58 @@ type Plugin struct { fallbackPaths []string engineInstance *Engine } + +// ValueProvider is the interface that we're exposing as a plugin. +type ValueProvider interface { + Value(token string, metadata string) (string, error) +} + +// Here is an implementation that talks over RPC +type StorePluginRPC struct{ client *rpc.Client } + +func (g *StorePluginRPC) Greet() string { + var resp string + err := g.client.Call("Plugin.Greet", new(interface{}), &resp) + if err != nil { + // You usually want your interfaces to return errors. If they don't, + // there isn't much other choice here. + panic(err) + } + + return resp +} + +// Here is the RPC server that GreeterRPC talks to, conforming to +// the requirements of net/rpc +type GreeterRPCServer struct { + // This is the real implementation + Impl ValueProvider +} + +func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error { + *resp = s.Impl.Value() + return nil +} + +// This is the implementation of plugin.Plugin so we can serve/consume this +// +// This has two methods: Server must return an RPC server for this plugin +// type. We construct a GreeterRPCServer for this. +// +// Client must return an implementation of our interface that communicates +// over an RPC client. We return GreeterRPC for this. +// +// Ignore MuxBroker. That is used to create more multiplexed streams on our +// plugin connection and is a more advanced use case. +type GreeterPlugin struct { + // Impl Injection + Impl ValueProvider +} + +func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &GreeterRPCServer{Impl: p.Impl}, nil +} + +func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &StorePluginRPC{client: c}, nil +} diff --git a/internal/plugin/provider.go b/internal/plugin/provider.go new file mode 100644 index 0000000..9a9e236 --- /dev/null +++ b/internal/plugin/provider.go @@ -0,0 +1,62 @@ +package plugin + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin/examples/basic/shared" +) + +// handshakeConfigs are used to just do a basic handshake between +// a plugin and host. If the handshake fails, a user friendly error is shown. +// This prevents users from executing bad plugins or executing a plugin +// directory. It is a UX feature, not a security feature. +var handshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "BASIC_PLUGIN", + MagicCookieValue: "hello", +} + +// pluginMap is the map of plugins we can dispense. +var pluginMap = map[string]plugin.Plugin{ + "greeter": &shared.GreeterPlugin{}, +} + +func Init() { + // Create an hclog.Logger + logger := hclog.New(&hclog.LoggerOptions{ + Name: "plugin", + Output: os.Stdout, + Level: hclog.Debug, + }) + + // We're a host! Start by launching the plugin process. + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: handshakeConfig, + Plugins: pluginMap, + Cmd: exec.Command("./plugin/greeter"), + Logger: logger, + }) + defer client.Kill() + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + log.Fatal(err) + } + + // Request the plugin + raw, err := rpcClient.Dispense("greeter") + if err != nil { + log.Fatal(err) + } + + // We should have a Greeter now! This feels like a normal interface + // implementation but is in fact over an RPC connection. + greeter := raw.(shared.Greeter) + fmt.Println(greeter.Greet()) +} From 4f7aeccfd1494de348187a7edc89fb03f774dcb9 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 29 Nov 2025 11:44:48 +0000 Subject: [PATCH 5/7] fix: interim commit shared lib needs more work --- .gitignore | 1 + buf.gen.yaml | 15 ++ buf.yaml | 9 ++ eirctl.yaml | 34 ++++- go.mod | 32 ++-- go.sum | 219 +++++++++++++++------------ plugins/grpc.go | 35 +++++ plugins/interface.go | 54 +++++++ plugins/proto/token_store.pb.go | 183 ++++++++++++++++++++++ plugins/proto/token_store.proto | 16 ++ plugins/proto/token_store_grpc.pb.go | 107 +++++++++++++ 11 files changed, 588 insertions(+), 117 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 plugins/grpc.go create mode 100644 plugins/interface.go create mode 100644 plugins/proto/token_store.pb.go create mode 100644 plugins/proto/token_store.proto create mode 100644 plugins/proto/token_store_grpc.pb.go diff --git a/.gitignore b/.gitignore index 573d3c6..81bfbd1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ dist # local testers and local/ +.bin diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..eb64d58 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,15 @@ +version: v2 +# this ensures we can store the .proto and generated files in the same directory +clean: false +plugins: + - remote: buf.build/protocolbuffers/go + out: plugins/proto + opt: + - paths=source_relative + - remote: buf.build/grpc/go:v1.3.0 + out: plugins/proto + opt: + - paths=source_relative + - require_unimplemented_servers=false +inputs: + - directory: plugins/proto diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..d055ccd --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: ./plugins/proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/eirctl.yaml b/eirctl.yaml index f9e4f9c..7c64937 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -10,10 +10,24 @@ contexts: bash: container: name: mirror.gcr.io/bash:5.0.18-alpine3.22 + buf: + container: + name: docker.io/bufbuild/buf:1.61 + pull_timeout: 0 + entrypoint: /usr/bin/env + + go1xalpine: + container: + name: mirror.gcr.io/golang:1.25-alpine + envfile: + exclude: + - GO + - CXX + - CGO pipelines: unit:test: - - pipeline: test:unit + - pipeline: test:unit env: ROOT_PKG_NAME: github.com/DevLabFoundry @@ -32,6 +46,11 @@ pipelines: - task: go:build:binary depends_on: clean + proto:build: + - task: proto:install + - task: proto:generate + depends_on: proto:install + tasks: show:coverage: description: Opens the current coverage viewer for the the configmanager utility. @@ -93,7 +112,7 @@ tasks: sonar:coverage:prep: context: bash command: - - | + - | sed -i 's|github.com/DevLabFoundry/configmanager/v2/||g' .coverage/out echo "Coverage file first 20 lines after conversion:" head -20 .coverage/out @@ -112,4 +131,15 @@ tasks: - VERSION - REVISION + # currently unused + proto:install: + context: go1xalpine + command: + - GOPATH=$PWD/local/go go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 + proto:generate: + context: buf + command: + # - PATH=$PATH:$PWD/local/go/bin buf generate + # getting all plugins from the remote registry + - buf generate diff --git a/go.mod b/go.mod index a8074af..4049a5c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/DevLabFoundry/configmanager/v3 go 1.25.4 require ( - cloud.google.com/go/secretmanager v1.16.0 + cloud.google.com/go/secretmanager v1.11.4 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 @@ -14,7 +14,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 github.com/go-test/deep v1.1.1 - github.com/googleapis/gax-go/v2 v2.15.0 + github.com/googleapis/gax-go/v2 v2.12.0 github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/api/auth/aws v0.11.0 github.com/spf13/cobra v1.10.1 @@ -24,16 +24,16 @@ require ( require ( github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/oklog/run v1.1.0 // indirect + go.opencensus.io v0.24.0 // indirect ) require ( - cloud.google.com/go/auth v0.17.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/iam v1.1.5 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect @@ -57,14 +57,14 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect + github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.7.0 + github.com/hashicorp/go-plugin v1.6.2 github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect @@ -86,7 +86,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/tetratelabs/wazero v1.10.1 go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect @@ -98,10 +98,10 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.256.0 // indirect - google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/api v0.155.0 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/grpc v1.61.2 + google.golang.org/protobuf v1.31.0 ) diff --git a/go.sum b/go.sum index f7638e0..3008268 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,20 @@ -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYzilxVyT+k= -cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= +cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 h1:uU4FujKFQAz31AbWOO3INV9qfIanHeIUSsGhRlcJJmg= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0/go.mod h1:qr3M3Oy6V98VR0c5tCHKUpaeJTRQh6KYzJewRtFWqfc= -github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0 h1:mXlQ+2C8A4KpXTIIYYxgFYqSivjGTBQidq/b0xxZLuk= -github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0/go.mod h1:K//Ck7MUa+r9jpV69WLeWnnju5WJx5120AFsEzvumII= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 h1:j0hhYS006eJ54vusoap0f2NVZ1YY3QnaAEnLM68f0SQ= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1/go.mod h1:AdtInaXmK8eYmbjezRWgLz+Qs46nc9Up9GWGwteWNfw= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -37,84 +27,64 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= -github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA= github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M= github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.11 h1:DouhxUREBjfnNJFp1yNn/p1Gk5pzr1YNixcIOIudI2g= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.11/go.mod h1:QgVIY03/XoQs2iFr0MbQuQ/Tf1RwlkOvuySWMh1wph4= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= -github.com/aws/aws-sdk-go-v2/service/ssm v1.66.4 h1:UmkF0ipNy0Ps6csJl/ZRJ3K+DWe9q0A7LT3xfxoHbgg= -github.com/aws/aws-sdk-go-v2/service/ssm v1.66.4/go.mod h1:uNHuYAQazkHqpD+hVomA2+eDSuKJzerno7Fnha6N6/Y= github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 h1:ofiQvKwka2E3T8FXBsU1iWj7Yvk2wd1p4ZCdS6qGiKQ= github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3/go.mod h1:+nlWvcgDPQ56mChEBzTC0puAMck+4onOFaHg5cE+Lgg= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +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/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -132,18 +102,41 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= 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= @@ -154,8 +147,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= -github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -180,6 +173,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 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= @@ -215,10 +210,9 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -239,15 +233,18 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -261,66 +258,88 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689Cbtr go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +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/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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4= -google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 h1:MgBTzgUJFAmp2PlyqKJecSpZpjFxkYL3nDUIeH/6Q30= -google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101/go.mod h1:bbWg36d7wp3knc0hIlmJAnW5R/CQ2rzpEVb72eH4ex4= -google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY= -google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI= -google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM= -google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +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/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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.61.2 h1:TzJay21lXCf7BiNFKl7mSskt5DlkKAumAYTs52SpJeo= +google.golang.org/grpc v1.61.2/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -332,3 +351,5 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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/plugins/grpc.go b/plugins/grpc.go new file mode 100644 index 0000000..9d7ffc5 --- /dev/null +++ b/plugins/grpc.go @@ -0,0 +1,35 @@ +package plugins + +import ( + "context" + + "github.com/DevLabFoundry/configmanager/v3/plugins/proto" +) + +// GRPCClient is an implementation of KV that talks over RPC. +type GRPCClient struct{ client proto.TokenStoreClient } + +func (m *GRPCClient) Get(key string, metadata []byte) (string, error) { + resp, err := m.client.Value(context.Background(), &proto.TokenValueRequest{ + Token: key, + Metadata: metadata, + }) + if err != nil { + return "", err + } + + return resp.Value, nil +} + +// Here is the gRPC server that GRPCClient talks to. +type GRPCServer struct { + // This is the real implementation + Impl TokenStore +} + +func (m *GRPCServer) Get( + ctx context.Context, + req *proto.TokenValueRequest) (*proto.TokenValueResponse, error) { + v, err := m.Impl.Get(req.) + return &proto.GetResponse{Value: v}, err +} diff --git a/plugins/interface.go b/plugins/interface.go new file mode 100644 index 0000000..ee33294 --- /dev/null +++ b/plugins/interface.go @@ -0,0 +1,54 @@ +package plugins + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin/examples/grpc/proto" +) + +// Handshake is a common handshake that is shared by plugin and host. +var Handshake = plugin.HandshakeConfig{ + // This isn't required when using VersionedPlugins + ProtocolVersion: 1, + MagicCookieKey: "BASIC_PLUGIN", + MagicCookieValue: "hello", +} + +// PluginMap is the map of plugins we can dispense. +var PluginMap = map[string]plugin.Plugin{ + "kv_grpc": &TokenStoreGRPCPlugin{}, + // "kv": &KVPlugin{}, +} + +// TokenStore is the interface that we're exposing as a plugin. +type TokenStore interface { + Get(token string, metadata []byte) (string, error) +} + +// This is the implementation of plugin.Plugin so we can serve/consume this. +type TokenStorePlugin struct { + // Concrete implementation, written in Go. This is only used for plugins + // that are written in Go. + Impl TokenStore +} + +// This is the implementation of plugin.GRPCPlugin so we can serve/consume this. +type TokenStoreGRPCPlugin struct { + // GRPCPlugin must still implement the Plugin interface + plugin.Plugin + // Concrete implementation, written in Go. This is only used for plugins + // that are written in Go. + Impl TokenStore +} + +func (p *TokenStoreGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl}) + return nil +} + +func (p *TokenStoreGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClient{client: proto(c)}, nil +} diff --git a/plugins/proto/token_store.pb.go b/plugins/proto/token_store.pb.go new file mode 100644 index 0000000..aa4eff0 --- /dev/null +++ b/plugins/proto/token_store.pb.go @@ -0,0 +1,183 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: token_store.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TokenValueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + Metadata []byte `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenValueRequest) Reset() { + *x = TokenValueRequest{} + mi := &file_token_store_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenValueRequest) ProtoMessage() {} + +func (x *TokenValueRequest) ProtoReflect() protoreflect.Message { + mi := &file_token_store_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenValueRequest.ProtoReflect.Descriptor instead. +func (*TokenValueRequest) Descriptor() ([]byte, []int) { + return file_token_store_proto_rawDescGZIP(), []int{0} +} + +func (x *TokenValueRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *TokenValueRequest) GetMetadata() []byte { + if x != nil { + return x.Metadata + } + return nil +} + +type TokenValueResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TokenValueResponse) Reset() { + *x = TokenValueResponse{} + mi := &file_token_store_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TokenValueResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TokenValueResponse) ProtoMessage() {} + +func (x *TokenValueResponse) ProtoReflect() protoreflect.Message { + mi := &file_token_store_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TokenValueResponse.ProtoReflect.Descriptor instead. +func (*TokenValueResponse) Descriptor() ([]byte, []int) { + return file_token_store_proto_rawDescGZIP(), []int{1} +} + +func (x *TokenValueResponse) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +var File_token_store_proto protoreflect.FileDescriptor + +const file_token_store_proto_rawDesc = "" + + "\n" + + "\x11token_store.proto\x12\x05proto\"E\n" + + "\x11TokenValueRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n" + + "\bmetadata\x18\x02 \x01(\fR\bmetadata\"*\n" + + "\x12TokenValueResponse\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value2J\n" + + "\n" + + "TokenStore\x12<\n" + + "\x05Value\x12\x18.proto.TokenValueRequest\x1a\x19.proto.TokenValueResponseB\tZ\a./protob\x06proto3" + +var ( + file_token_store_proto_rawDescOnce sync.Once + file_token_store_proto_rawDescData []byte +) + +func file_token_store_proto_rawDescGZIP() []byte { + file_token_store_proto_rawDescOnce.Do(func() { + file_token_store_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_token_store_proto_rawDesc), len(file_token_store_proto_rawDesc))) + }) + return file_token_store_proto_rawDescData +} + +var file_token_store_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_token_store_proto_goTypes = []any{ + (*TokenValueRequest)(nil), // 0: proto.TokenValueRequest + (*TokenValueResponse)(nil), // 1: proto.TokenValueResponse +} +var file_token_store_proto_depIdxs = []int32{ + 0, // 0: proto.TokenStore.Value:input_type -> proto.TokenValueRequest + 1, // 1: proto.TokenStore.Value:output_type -> proto.TokenValueResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_token_store_proto_init() } +func file_token_store_proto_init() { + if File_token_store_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_token_store_proto_rawDesc), len(file_token_store_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_token_store_proto_goTypes, + DependencyIndexes: file_token_store_proto_depIdxs, + MessageInfos: file_token_store_proto_msgTypes, + }.Build() + File_token_store_proto = out.File + file_token_store_proto_goTypes = nil + file_token_store_proto_depIdxs = nil +} diff --git a/plugins/proto/token_store.proto b/plugins/proto/token_store.proto new file mode 100644 index 0000000..a22eb02 --- /dev/null +++ b/plugins/proto/token_store.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; +package proto; +option go_package = "./proto"; + +message TokenValueRequest { + string token = 1; + bytes metadata = 2; +} + +message TokenValueResponse { + string value = 1; +} + +service TokenStore { + rpc Value(TokenValueRequest) returns (TokenValueResponse); +} diff --git a/plugins/proto/token_store_grpc.pb.go b/plugins/proto/token_store_grpc.pb.go new file mode 100644 index 0000000..6ea7faf --- /dev/null +++ b/plugins/proto/token_store_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: token_store.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + TokenStore_Value_FullMethodName = "/proto.TokenStore/Value" +) + +// TokenStoreClient is the client API for TokenStore service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TokenStoreClient interface { + Value(ctx context.Context, in *TokenValueRequest, opts ...grpc.CallOption) (*TokenValueResponse, error) +} + +type tokenStoreClient struct { + cc grpc.ClientConnInterface +} + +func NewTokenStoreClient(cc grpc.ClientConnInterface) TokenStoreClient { + return &tokenStoreClient{cc} +} + +func (c *tokenStoreClient) Value(ctx context.Context, in *TokenValueRequest, opts ...grpc.CallOption) (*TokenValueResponse, error) { + out := new(TokenValueResponse) + err := c.cc.Invoke(ctx, TokenStore_Value_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TokenStoreServer is the server API for TokenStore service. +// All implementations should embed UnimplementedTokenStoreServer +// for forward compatibility +type TokenStoreServer interface { + Value(context.Context, *TokenValueRequest) (*TokenValueResponse, error) +} + +// UnimplementedTokenStoreServer should be embedded to have forward compatible implementations. +type UnimplementedTokenStoreServer struct { +} + +func (UnimplementedTokenStoreServer) Value(context.Context, *TokenValueRequest) (*TokenValueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Value not implemented") +} + +// UnsafeTokenStoreServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TokenStoreServer will +// result in compilation errors. +type UnsafeTokenStoreServer interface { + mustEmbedUnimplementedTokenStoreServer() +} + +func RegisterTokenStoreServer(s grpc.ServiceRegistrar, srv TokenStoreServer) { + s.RegisterService(&TokenStore_ServiceDesc, srv) +} + +func _TokenStore_Value_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TokenValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TokenStoreServer).Value(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TokenStore_Value_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TokenStoreServer).Value(ctx, req.(*TokenValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TokenStore_ServiceDesc is the grpc.ServiceDesc for TokenStore service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TokenStore_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.TokenStore", + HandlerType: (*TokenStoreServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Value", + Handler: _TokenStore_Value_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "token_store.proto", +} From b1f304641297318a1e046b892135fa68a28971ed Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sun, 30 Nov 2025 10:42:37 +0000 Subject: [PATCH 6/7] fix: plugin architecture working NOTE: needs a lot more polishing --- internal/config/config.go | 50 +- internal/parser/parser_test.go | 179 +++-- internal/plugin/plugin.go | 76 --- internal/plugin/provider.go | 62 -- internal/plugin/tester/main.go | 46 -- internal/plugin/wasip1.go | 263 -------- internal/plugin/wasip1_test.go | 47 -- internal/store/azappconf.go | 112 ---- internal/store/azappconf_test.go | 208 ------ internal/store/azhelpers.go | 37 -- internal/store/azkeyvault.go | 94 --- internal/store/azkeyvault_test.go | 218 ------ internal/store/aztablestorage.go | 126 ---- internal/store/aztablestorage_test.go | 354 ---------- internal/store/gcpsecrets.go | 91 --- internal/store/gcpsecrets_test.go | 184 ------ internal/store/hashivault.go | 172 ----- internal/store/hashivault_test.go | 624 ------------------ internal/store/paramstore_test.go | 152 ----- internal/store/plugin.go | 64 ++ internal/store/plugin_test.go | 34 + internal/store/secretsmanager.go | 93 --- internal/store/secretsmanager_test.go | 154 ----- internal/store/store.go | 2 +- internal/strategy/strategy.go | 42 +- internal/strategy/strategy_test.go | 339 +++++----- plugins/awsparams/paramstore.go | 67 -- .../main.go => awsparamstr/README.md} | 17 +- .../awsparamstr/impl}/paramstore.go | 23 +- .../impl}/paramstore_test.go | 10 +- plugins/awsparamstr/main.go | 34 + plugins/awssecrets/main.go | 197 ------ plugins/grpc.go | 8 +- plugins/interface.go | 22 +- plugins/scaffolding.go | 6 - 35 files changed, 463 insertions(+), 3744 deletions(-) delete mode 100644 internal/plugin/plugin.go delete mode 100644 internal/plugin/provider.go delete mode 100644 internal/plugin/tester/main.go delete mode 100644 internal/plugin/wasip1.go delete mode 100644 internal/plugin/wasip1_test.go delete mode 100644 internal/store/azappconf.go delete mode 100644 internal/store/azappconf_test.go delete mode 100644 internal/store/azhelpers.go delete mode 100644 internal/store/azkeyvault.go delete mode 100644 internal/store/azkeyvault_test.go delete mode 100644 internal/store/aztablestorage.go delete mode 100644 internal/store/aztablestorage_test.go delete mode 100644 internal/store/gcpsecrets.go delete mode 100644 internal/store/gcpsecrets_test.go delete mode 100644 internal/store/hashivault.go delete mode 100644 internal/store/hashivault_test.go delete mode 100644 internal/store/paramstore_test.go create mode 100644 internal/store/plugin.go create mode 100644 internal/store/plugin_test.go delete mode 100644 internal/store/secretsmanager.go delete mode 100644 internal/store/secretsmanager_test.go delete mode 100644 plugins/awsparams/paramstore.go rename plugins/{awsparams/main.go => awsparamstr/README.md} (91%) rename {internal/store => plugins/awsparamstr/impl}/paramstore.go (69%) rename plugins/{awsparams => awsparamstr/impl}/paramstore_test.go (95%) create mode 100644 plugins/awsparamstr/main.go delete mode 100644 plugins/awssecrets/main.go diff --git a/internal/config/config.go b/internal/config/config.go index a1898fc..f7b3713 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "strings" - - "github.com/DevLabFoundry/configmanager/v3/plugins" ) const ( @@ -175,7 +173,7 @@ func (ptc *ParsedTokenConfig) WithSanitizedToken(v string) { func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { // empty map will be parsed as `{}` still resulting in a valid json // and successful unmarshalling but default value pointer struct - if err := json.Unmarshal(fmt.Appendf(nil, t.parseMetadata()), metadataTyp); err != nil { + if err := json.Unmarshal(fmt.Appendf(nil, "%s", t.parseMetadata()), metadataTyp); err != nil { // It would very hard to test this since // we are forcing the key and value to be strings // return non-filled pointer @@ -184,21 +182,6 @@ func (t *ParsedTokenConfig) ParseMetadata(metadataTyp any) error { return nil } -func (t *ParsedTokenConfig) parseMetadata() string { - // crude json like builder from key/val tags - // since we are only ever dealing with a string input - // extracted from the token there is little chance panic would occur here - // WATCH THIS SPACE "¯\_(ツ)_/¯" - metaMap := []string{} - for keyVal := range strings.SplitSeq(t.metadataStr, ",") { - mapKeyVal := strings.Split(keyVal, "=") - if len(mapKeyVal) == 2 { - metaMap = append(metaMap, fmt.Sprintf(`"%s":"%s"`, mapKeyVal[0], mapKeyVal[1])) - } - } - return fmt.Sprintf(`{%s}`, strings.Join(metaMap, ",")) -} - // StoreToken returns the sanitized token without: // - metadata // - keySeparator @@ -249,24 +232,17 @@ func (t *ParsedTokenConfig) TokenSeparator() string { return t.tokenSeparator } -func (t *ParsedTokenConfig) JSONMessagExchange() (*plugins.MessagExchange, error) { - md := map[string]any{} - if err := json.Unmarshal([]byte(t.parseMetadata()), &md); err != nil { - return nil, err - } - - jme := &plugins.MessagExchange{ - Token: t.StoreToken(), - Metadata: md, - } - - return jme, nil -} - -func (t *ParsedTokenConfig) JSONMessagExchangeBytes() ([]byte, error) { - j, err := t.JSONMessagExchange() - if err != nil { - return nil, err +func (t *ParsedTokenConfig) parseMetadata() string { + // crude json like builder from key/val tags + // since we are only ever dealing with a string input + // extracted from the token there is little chance panic would occur here + // WATCH THIS SPACE "¯\_(ツ)_/¯" + metaMap := []string{} + for keyVal := range strings.SplitSeq(t.metadataStr, ",") { + mapKeyVal := strings.Split(keyVal, "=") + if len(mapKeyVal) == 2 { + metaMap = append(metaMap, fmt.Sprintf(`"%s":"%s"`, mapKeyVal[0], mapKeyVal[1])) + } } - return json.Marshal(j) + return fmt.Sprintf(`{%s}`, strings.Join(metaMap, ",")) } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 2a97c04..9b93cf5 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -9,7 +9,6 @@ import ( "github.com/DevLabFoundry/configmanager/v3/internal/lexer" "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/DevLabFoundry/configmanager/v3/internal/parser" - "github.com/DevLabFoundry/configmanager/v3/internal/store" ) var lexerSource = lexer.Source{FileName: "bar", FullPath: "/foo/bar"} @@ -188,100 +187,100 @@ func Test_Parse_should_pass_with_metadata_end_tag(t *testing.T) { } } -func Test_Parse_ParseMetadata(t *testing.T) { +// func Test_Parse_ParseMetadata(t *testing.T) { - ttests := map[string]struct { - input string - typ *store.SecretsMgrConfig - }{ - "without keysPath": { - `AWSSECRETS:///foo[version=1.2.3]`, - &store.SecretsMgrConfig{}, - }, - "with keysPath": { - `AWSSECRETS:///foo|path.one[version=1.2.3]`, - &store.SecretsMgrConfig{}, - }, - "nestled in text": { - `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, - &store.SecretsMgrConfig{}, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - lexerSource.Input = tt.input - cfg := config.NewConfig() - l := lexer.New(lexerSource, *cfg) - p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) - parsed, errs := p.Parse() - if len(errs) > 0 { - t.Fatalf("%v", errs) - } +// ttests := map[string]struct { +// input string +// typ *store.SecretsMgrConfig +// }{ +// "without keysPath": { +// `AWSSECRETS:///foo[version=1.2.3]`, +// &store.SecretsMgrConfig{}, +// }, +// "with keysPath": { +// `AWSSECRETS:///foo|path.one[version=1.2.3]`, +// &store.SecretsMgrConfig{}, +// }, +// "nestled in text": { +// `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, +// &store.SecretsMgrConfig{}, +// }, +// } +// for name, tt := range ttests { +// t.Run(name, func(t *testing.T) { +// lexerSource.Input = tt.input +// cfg := config.NewConfig() +// l := lexer.New(lexerSource, *cfg) +// p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) +// parsed, errs := p.Parse() +// if len(errs) > 0 { +// t.Fatalf("%v", errs) +// } - for _, p := range parsed { - if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { - t.Fatal(err) - } - if tt.typ.Version != "1.2.3" { - t.Errorf("got %v wanted 1.2.3", tt.typ.Version) - } - } - }) - } -} +// for _, p := range parsed { +// if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { +// t.Fatal(err) +// } +// if tt.typ.Version != "1.2.3" { +// t.Errorf("got %v wanted 1.2.3", tt.typ.Version) +// } +// } +// }) +// } +// } -func Test_Parse_Path_Keys_WithParsedMetadat(t *testing.T) { +// func Test_Parse_Path_Keys_WithParsedMetadat(t *testing.T) { - ttests := map[string]struct { - input string - typ *store.SecretsMgrConfig - wantSanitizedPath string - wantKeyPath string - }{ - "without keysPath": { - `AWSSECRETS:///foo[version=1.2.3]`, - &store.SecretsMgrConfig{}, - "/foo", "", - }, - "with keysPath": { - `AWSSECRETS:///foo|path.one[version=1.2.3]`, - &store.SecretsMgrConfig{}, - "/foo", "path.one", - }, - "nestled in text": { - `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, - &store.SecretsMgrConfig{}, - "/path/queryparam", "p1", - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - lexerSource.Input = tt.input - cfg := config.NewConfig() - l := lexer.New(lexerSource, *cfg) - p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) - parsed, errs := p.Parse() - if len(errs) > 0 { - t.Fatalf("%v", errs) - } +// ttests := map[string]struct { +// input string +// typ *store.SecretsMgrConfig +// wantSanitizedPath string +// wantKeyPath string +// }{ +// "without keysPath": { +// `AWSSECRETS:///foo[version=1.2.3]`, +// &store.SecretsMgrConfig{}, +// "/foo", "", +// }, +// "with keysPath": { +// `AWSSECRETS:///foo|path.one[version=1.2.3]`, +// &store.SecretsMgrConfig{}, +// "/foo", "path.one", +// }, +// "nestled in text": { +// `someQ=AWSPARAMSTR:///path/queryparam|p1[version=1.2.3]&anotherQ`, +// &store.SecretsMgrConfig{}, +// "/path/queryparam", "p1", +// }, +// } +// for name, tt := range ttests { +// t.Run(name, func(t *testing.T) { +// lexerSource.Input = tt.input +// cfg := config.NewConfig() +// l := lexer.New(lexerSource, *cfg) +// p := parser.New(l, cfg).WithLogger(log.New(os.Stderr)) +// parsed, errs := p.Parse() +// if len(errs) > 0 { +// t.Fatalf("%v", errs) +// } - for _, p := range parsed { - if p.ParsedToken.StoreToken() != tt.wantSanitizedPath { - t.Errorf("got %s want %s", p.ParsedToken.StoreToken(), tt.wantSanitizedPath) - } - if p.ParsedToken.LookupKeys() != tt.wantKeyPath { - t.Errorf("got %s want %s", p.ParsedToken.LookupKeys(), tt.wantKeyPath) - } - if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { - t.Fatal(err) - } - if tt.typ.Version != "1.2.3" { - t.Errorf("got %v wanted 1.2.3", tt.typ.Version) - } - } - }) - } -} +// for _, p := range parsed { +// if p.ParsedToken.StoreToken() != tt.wantSanitizedPath { +// t.Errorf("got %s want %s", p.ParsedToken.StoreToken(), tt.wantSanitizedPath) +// } +// if p.ParsedToken.LookupKeys() != tt.wantKeyPath { +// t.Errorf("got %s want %s", p.ParsedToken.LookupKeys(), tt.wantKeyPath) +// } +// if err := p.ParsedToken.ParseMetadata(tt.typ); err != nil { +// t.Fatal(err) +// } +// if tt.typ.Version != "1.2.3" { +// t.Errorf("got %v wanted 1.2.3", tt.typ.Version) +// } +// } +// }) +// } +// } func testHelperGenDocBlock(t *testing.T, stmtBlock parser.ConfigManagerTokenBlock, tokenType config.ImplementationPrefix, tokenValue, keysLookupPath string) bool { t.Helper() diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go deleted file mode 100644 index 24c2b48..0000000 --- a/internal/plugin/plugin.go +++ /dev/null @@ -1,76 +0,0 @@ -package plugin - -import ( - "net/rpc" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/hashicorp/go-plugin" -) - -// Plugin is responsible for managing plugins within configmanager -// -// It includes the following methods -// - fetch plugins from known sources -// - maintains a list of tokens answerable by a specified pluginEngine -type Plugin struct { - Implementations config.ImplementationPrefix - SourcePath string - Version string - fallbackPaths []string - engineInstance *Engine -} - -// ValueProvider is the interface that we're exposing as a plugin. -type ValueProvider interface { - Value(token string, metadata string) (string, error) -} - -// Here is an implementation that talks over RPC -type StorePluginRPC struct{ client *rpc.Client } - -func (g *StorePluginRPC) Greet() string { - var resp string - err := g.client.Call("Plugin.Greet", new(interface{}), &resp) - if err != nil { - // You usually want your interfaces to return errors. If they don't, - // there isn't much other choice here. - panic(err) - } - - return resp -} - -// Here is the RPC server that GreeterRPC talks to, conforming to -// the requirements of net/rpc -type GreeterRPCServer struct { - // This is the real implementation - Impl ValueProvider -} - -func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error { - *resp = s.Impl.Value() - return nil -} - -// This is the implementation of plugin.Plugin so we can serve/consume this -// -// This has two methods: Server must return an RPC server for this plugin -// type. We construct a GreeterRPCServer for this. -// -// Client must return an implementation of our interface that communicates -// over an RPC client. We return GreeterRPC for this. -// -// Ignore MuxBroker. That is used to create more multiplexed streams on our -// plugin connection and is a more advanced use case. -type GreeterPlugin struct { - // Impl Injection - Impl ValueProvider -} - -func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) { - return &GreeterRPCServer{Impl: p.Impl}, nil -} - -func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { - return &StorePluginRPC{client: c}, nil -} diff --git a/internal/plugin/provider.go b/internal/plugin/provider.go deleted file mode 100644 index 9a9e236..0000000 --- a/internal/plugin/provider.go +++ /dev/null @@ -1,62 +0,0 @@ -package plugin - -import ( - "fmt" - "log" - "os" - "os/exec" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-plugin" - "github.com/hashicorp/go-plugin/examples/basic/shared" -) - -// handshakeConfigs are used to just do a basic handshake between -// a plugin and host. If the handshake fails, a user friendly error is shown. -// This prevents users from executing bad plugins or executing a plugin -// directory. It is a UX feature, not a security feature. -var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "BASIC_PLUGIN", - MagicCookieValue: "hello", -} - -// pluginMap is the map of plugins we can dispense. -var pluginMap = map[string]plugin.Plugin{ - "greeter": &shared.GreeterPlugin{}, -} - -func Init() { - // Create an hclog.Logger - logger := hclog.New(&hclog.LoggerOptions{ - Name: "plugin", - Output: os.Stdout, - Level: hclog.Debug, - }) - - // We're a host! Start by launching the plugin process. - client := plugin.NewClient(&plugin.ClientConfig{ - HandshakeConfig: handshakeConfig, - Plugins: pluginMap, - Cmd: exec.Command("./plugin/greeter"), - Logger: logger, - }) - defer client.Kill() - - // Connect via RPC - rpcClient, err := client.Client() - if err != nil { - log.Fatal(err) - } - - // Request the plugin - raw, err := rpcClient.Dispense("greeter") - if err != nil { - log.Fatal(err) - } - - // We should have a Greeter now! This feels like a normal interface - // implementation but is in fact over an RPC connection. - greeter := raw.(shared.Greeter) - fmt.Println(greeter.Greet()) -} diff --git a/internal/plugin/tester/main.go b/internal/plugin/tester/main.go deleted file mode 100644 index b17e481..0000000 --- a/internal/plugin/tester/main.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/plugin" -) - -func main() { - inputReader, err := os.Open("/Users/dusannitschneider/git/dnitsch/configmanager/plugins/awsparams/awsparams.wasm") - if err != nil { - log.Fatal(fmt.Errorf("open plugin.wasm: %w", err)) - } - - ctx := context.Background() - - // Load the compiled WASI plugin. - engine, err := plugin.NewEngine(ctx, inputReader) - if err != nil { - log.Fatal(err) - } - defer engine.Close(ctx) - - inst, err := engine.NewApiInstance(ctx) - if err != nil { - log.Fatal(err) - } - defer inst.Close(ctx) - - // os.Setenv("AWS_PROFILE", "anabode_terraform_dev") - // os.Setenv("AWS_REGION", "eu-west-1") - t1, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - t1.WithSanitizedToken("/int-test/pocketbase/admin-pwd") - val, err := inst.TokenValue(ctx, t1) - if err != nil { - log.Fatal(err) - } - - fmt.Println(string(val)) - - os.Exit(0) -} diff --git a/internal/plugin/wasip1.go b/internal/plugin/wasip1.go deleted file mode 100644 index 268371f..0000000 --- a/internal/plugin/wasip1.go +++ /dev/null @@ -1,263 +0,0 @@ -// Package plugin -// provides reactor style module -// we can explore the plugin provided host module -package plugin - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "io" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/plugins" - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" -) - -var ( - ErrMissingMethod = errors.New("missing method on the wasiLib instance") - ErrAllocMemForParam = errors.New("failed to allocate memory for property") - ErrAllocateOutPtrZeroLen = errors.New("allocate returned 0 for output pointer") - ErrMemoryReadFailed = errors.New("mem.Read(out) failed") - ErrEmptyToken = errors.New("token must not be empty") -) - -// ==================== -// Engine & ApiInstance -// ==================== - -type Engine struct { - r wazero.Runtime - compiledModule wazero.CompiledModule -} - -// NewEngine compiles the WASI module once for the lifetime of the program. -func NewEngine(ctx context.Context, ps io.ReadCloser) (*Engine, error) { - r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig()) - if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { - _ = r.Close(ctx) - return nil, fmt.Errorf("instantiate WASI: %w", err) - } - - defer ps.Close() - wasiLib, err := io.ReadAll(ps) - if err != nil { - _ = r.Close(ctx) - return nil, fmt.Errorf("read plugin: %w", err) - } - - cm, err := r.CompileModule(ctx, wasiLib) - if err != nil { - _ = r.Close(ctx) - return nil, fmt.Errorf("compile module: %w", err) - } - - return &Engine{ - r: r, - compiledModule: cm, - }, nil -} - -// Close shuts down the runtime. -func (e *Engine) Close(ctx context.Context) error { - return e.r.Close(ctx) -} - -type ApiInstance struct { - mod api.Module - mem api.Memory - // exported alloc helpers - allocate api.Function - deallocate api.Function - // exported business function - tokenValue api.Function - // scratch output buffers - outPtr uint32 - outCap uint32 - outLenPtr uint32 // 4-byte cell for required length -} - -// NewApiInstance instantiates a fresh module instance. -func (e *Engine) NewApiInstance(ctx context.Context) (*ApiInstance, error) { - mod, err := e.r.InstantiateModule(ctx, e.compiledModule, wazero.NewModuleConfig().WithStartFunctions("_initialize")) - if err != nil { - return nil, fmt.Errorf("instantiate module: %w", err) - } - - inst := &ApiInstance{ - mod: mod, - mem: mod.Memory(), - allocate: mod.ExportedFunction("allocate"), - deallocate: mod.ExportedFunction("deallocate"), - tokenValue: mod.ExportedFunction("strategy_token_value"), - } - - for name, exported := range map[string]api.Function{ - "allocate": inst.allocate, - "deallocate": inst.deallocate, - "strategy_token_value": inst.tokenValue, - } { - if exported == nil { - return nil, fmt.Errorf("%w, method %q not found on exports", ErrMissingMethod, name) - } - } - - return inst, nil -} - -// Close instance (optional). -func (i *ApiInstance) Close(ctx context.Context) { - i.freeScratch(ctx) - _ = i.mod.Close(ctx) -} - -// put allocates module memory and writes bytes into it. -// returns (ptr, size). caller must deallocate(ptr, size). -func (i *ApiInstance) put(ctx context.Context, b []byte) (uint32, uint32, error) { - if len(b) == 0 { - return 0, 0, ErrEmptyToken - } - - res, err := i.allocate.Call(ctx, uint64(len(b))) - if err != nil { - return 0, 0, fmt.Errorf("allocate: %w", err) - } - ptr := uint32(res[0]) - if ptr == 0 { - return 0, 0, fmt.Errorf("allocate returned 0: %w", ErrAllocMemForParam) - } - - if ok := i.mem.Write(ptr, b); !ok { - _, _ = i.deallocate.Call(ctx, uint64(ptr), uint64(len(b))) - return 0, 0, fmt.Errorf("mem.Write failed: %w", ErrAllocMemForParam) - } - - return ptr, uint32(len(b)), nil -} - -// ensureOut ensures the scratch output buffer has at least `need` bytes. -// allocates outLenPtr (4 bytes) once. -func (i *ApiInstance) ensureOut(ctx context.Context, need uint32) error { - // outLenPtr is a 4-byte cell for required length - if i.outLenPtr == 0 { - res, err := i.allocate.Call(ctx, 4) - if err != nil { - return fmt.Errorf("allocate outLenPtr: %w", err) - } - i.outLenPtr = uint32(res[0]) - if i.outLenPtr == 0 { - return ErrAllocateOutPtrZeroLen - } - } - - if need <= i.outCap { - return nil - } - - // grow if needed - free old and alloc new - if i.outPtr != 0 { - _, _ = i.deallocate.Call(ctx, uint64(i.outPtr), uint64(i.outCap)) - i.outPtr, i.outCap = 0, 0 - } - - res, err := i.allocate.Call(ctx, uint64(need)) - if err != nil { - return fmt.Errorf("allocate outPtr: %w", err) - } - i.outPtr, i.outCap = uint32(res[0]), need - if i.outPtr == 0 { - return ErrAllocateOutPtrZeroLen - } - return nil -} - -// freeScratch frees the reusable output buffers (call once per instance). -func (i *ApiInstance) freeScratch(ctx context.Context) { - if i.outPtr != 0 { - _, _ = i.deallocate.Call(ctx, uint64(i.outPtr), uint64(i.outCap)) - i.outPtr, i.outCap = 0, 0 - } - if i.outLenPtr != 0 { - _, _ = i.deallocate.Call(ctx, uint64(i.outLenPtr), 4) - i.outLenPtr = 0 - } -} - -// TokenValue is the nice host-side API: string in, []byte out. -func (i *ApiInstance) TokenValue(ctx context.Context, token *config.ParsedTokenConfig) ([]byte, error) { - if token.StoreToken() == "" { - return nil, ErrEmptyToken - } - tokenBytes, err := token.JSONMessagExchangeBytes() - tokenPtr, tokenLen, err := i.put(ctx, tokenBytes) - if err != nil { - return nil, fmt.Errorf("put input: %w", err) - } - defer i.deallocate.Call(ctx, uint64(tokenPtr), uint64(tokenLen)) - - // start with a smallish buffer; plugin will ask for more if needed - if err := i.ensureOut(ctx, 64); err != nil { - return nil, fmt.Errorf("ensureOut: %w", err) - } - - call := func() (int32, uint32, error) { - res, err := i.tokenValue.Call( - ctx, - uint64(tokenPtr), uint64(tokenLen), // sanitizedToken - uint64(i.outPtr), uint64(i.outCap), // outPtr, outCap - uint64(i.outLenPtr), // outLenPtr - ) - if err != nil { - return 0, 0, fmt.Errorf("call strategy_token_value: %w", err) - } - - lenBytes, ok := i.mem.Read(i.outLenPtr, 4) - if !ok { - return int32(res[0]), 0, ErrMemoryReadFailed - } - - need := binary.LittleEndian.Uint32(lenBytes) - return int32(res[0]), need, nil - } - - rc, need, err := call() - if err != nil { - return nil, err - } - - if rc == plugins.ERR_BUF_TOO_SMALL { - if err := i.ensureOut(ctx, need); err != nil { - return nil, fmt.Errorf("ensureOut resize: %w", err) - } - rc, need, err = call() - if err != nil { - return nil, err - } - } - - if rc != plugins.OK { - switch rc { - case plugins.ERR_INVALID_UTF8: - return nil, errors.New("token value: invalid UTF-8 in input") - case plugins.ERR_EMPTY_INPUT: - return nil, ErrEmptyToken - case plugins.ERR_BUF_TOO_SMALL: - return nil, fmt.Errorf("token value: buffer too small even after resize (need=%d)", need) - default: - return nil, fmt.Errorf("token value: unknown error code %d", rc) - } - } - - out, ok := i.mem.Read(i.outPtr, need) - if !ok { - return nil, ErrMemoryReadFailed - } - - // Detach from wasm memory. - result := make([]byte, need) - copy(result, out) - return result, nil -} diff --git a/internal/plugin/wasip1_test.go b/internal/plugin/wasip1_test.go deleted file mode 100644 index 5442eb1..0000000 --- a/internal/plugin/wasip1_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package plugin_test - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/plugin" -) - -func Test_FullFlow(t *testing.T) { - inputReader, err := os.Open("/Users/dusannitschneider/git/dnitsch/configmanager/plugins/awsparams/awsparams.wasm") - if err != nil { - t.Fatal(fmt.Errorf("open plugin.wasm: %w", err)) - } - ctx := context.Background() - - // Load the compiled WASI plugin. - engine, err := plugin.NewEngine(ctx, inputReader) - if err != nil { - t.Fatal(err) - } - defer engine.Close(ctx) - - inst, err := engine.NewApiInstance(ctx) - if err != nil { - t.Fatal(err) - } - defer inst.Close(ctx) - - os.Setenv("AWS_PROFILE", "anabode_terraform_dev") - os.Setenv("AWS_REGION", "eu-west-1") - t1, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - t1.WithSanitizedToken("/int-test/pocketbase/admin-pwd") - val, err := inst.TokenValue(ctx, t1) - if err != nil { - t.Fatal(err) - } - fmt.Printf("TokenValue(\"foo\") => %q\n", string(val)) - - // Zero-length test (should error) - t2, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - _, err = inst.TokenValue(ctx, t2) - fmt.Printf("TokenValue(\"\") error: %v\n", err) -} diff --git a/internal/store/azappconf.go b/internal/store/azappconf.go deleted file mode 100644 index c35f538..0000000 --- a/internal/store/azappconf.go +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Azure App Config implementation -**/ -package store - -import ( - "context" - "fmt" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" -) - -// appConfApi -// uses this package https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig -type appConfApi interface { - GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) -} - -type AzAppConf struct { - svc appConfApi - ctx context.Context - config *AzAppConfConfig - token *config.ParsedTokenConfig - strippedToken string - logger log.ILogger -} - -// AzAppConfConfig is the azure conf service specific config -// and it is parsed from the token metadata -type AzAppConfConfig struct { - Label string `json:"label"` - Etag *azcore.ETag `json:"etag"` - AcceptDateTime *time.Time `json:"acceptedDateTime"` -} - -// NewAzAppConf -func NewAzAppConf(ctx context.Context, token *config.ParsedTokenConfig, logger log.ILogger) (*AzAppConf, error) { - storeConf := &AzAppConfConfig{} - if err := token.ParseMetadata(storeConf); err != nil { - return nil, err - } - - backingStore := &AzAppConf{ - ctx: ctx, - config: storeConf, - token: token, - logger: logger, - } - srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.azconfig.io", 1) - backingStore.strippedToken = srvInit.Token - - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - logger.Error("failed to get credentials: %v", err) - return nil, err - } - - c, err := azappconfig.NewClient(srvInit.ServiceUri, cred, nil) - if err != nil { - logger.Error("failed to init the client: %v", err) - return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) - } - - backingStore.svc = c - return backingStore, nil - -} - -func (s *AzAppConf) WithSvc(svc appConfApi) { - s.svc = svc -} - -// setTokenVal sets the token -func (implmt *AzAppConf) SetToken(token *config.ParsedTokenConfig) {} - -// tokenVal in AZ App Config -// label can be specified -// From this point then normal rules of configmanager apply, -// including keySeperator and lookup. -func (imp *AzAppConf) Value() (string, error) { - imp.logger.Info("Concrete implementation AzAppConf") - imp.logger.Info("AzAppConf Token: %s", imp.token.String()) - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - opts := &azappconfig.GetSettingOptions{} - - // assign any metadatas from the token - if imp.config.Label != "" { - opts.Label = &imp.config.Label - } - - if imp.config.Etag != nil { - opts.OnlyIfChanged = imp.config.Etag - } - - s, err := imp.svc.GetSetting(ctx, imp.strippedToken, opts) - if err != nil { - imp.logger.Error(implementationNetworkErr, config.AzAppConfigPrefix, err, imp.strippedToken) - return "", fmt.Errorf("token: %s, error: %v. %w", imp.strippedToken, err, ErrRetrieveFailed) - } - if s.Value != nil { - return *s.Value, nil - } - imp.logger.Error("token: %v, %w", imp.token.String(), ErrEmptyResponse) - return "", nil -} diff --git a/internal/store/azappconf_test.go b/internal/store/azappconf_test.go deleted file mode 100644 index 17da526..0000000 --- a/internal/store/azappconf_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package store_test - -import ( - "bytes" - "context" - "errors" - "fmt" - "testing" - - "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - logger "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" -) - -func azAppConfCommonChecker(t *testing.T, key string, expectedKey string, expectLabel string, opts *azappconfig.GetSettingOptions) { - t.Helper() - if key != expectedKey { - t.Errorf(testutils.TestPhrase, key, expectedKey) - } - - if expectLabel != "" { - if opts == nil { - t.Errorf(testutils.TestPhrase, nil, expectLabel) - } - if *opts.Label != expectLabel { - t.Errorf(testutils.TestPhrase, opts.Label, expectLabel) - } - } -} - -type mockAzAppConfApi func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) - -func (m mockAzAppConfApi) GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - return m(ctx, key, options) -} - -func Test_AzAppConf_Success(t *testing.T) { - tsuccessParam := "somecvla" - - logr := logger.New(&bytes.Buffer{}) - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockAzAppConfApi - }{ - "successVal": { - func() *config.ParsedTokenConfig { - // "AZAPPCONF#/test-app-config-instance/table//token/1", - tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-app-config-instance/table//token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - tsuccessParam, - func(t *testing.T) mockAzAppConfApi { - return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - azAppConfCommonChecker(t, key, "table//token/1", "", options) - resp := azappconfig.GetSettingResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "successVal with :// token Separator": { - func() *config.ParsedTokenConfig { - // "AZAPPCONF:///test-app-config-instance/conf_key[label=dev]", - tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://")) - tkn.WithSanitizedToken("/test-app-config-instance/conf_key") - tkn.WithKeyPath("") - tkn.WithMetadata("label=dev") - return tkn - }, - tsuccessParam, - func(t *testing.T) mockAzAppConfApi { - return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - azAppConfCommonChecker(t, key, "conf_key", "dev", options) - resp := azappconfig.GetSettingResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "successVal with :// token Separator and etag specified": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-app-config-instance/conf_key") - tkn.WithKeyPath("") - tkn.WithMetadata("label=dev,etag=sometifdsssdsfdi_string01209222") - return tkn - }, - tsuccessParam, - func(t *testing.T) mockAzAppConfApi { - return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - azAppConfCommonChecker(t, key, "conf_key", "dev", options) - if !options.OnlyIfChanged.Equals("sometifdsssdsfdi_string01209222") { - t.Errorf(testutils.TestPhraseWithContext, "Etag not correctly set", options.OnlyIfChanged, "sometifdsssdsfdi_string01209222") - } - resp := azappconfig.GetSettingResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "successVal with keyseparator but no val returned": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-app-config-instance/try_to_find") - tkn.WithKeyPath("key_separator.lookup") - tkn.WithMetadata("") - return tkn - }, - "", - func(t *testing.T) mockAzAppConfApi { - return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - azAppConfCommonChecker(t, key, "try_to_find", "", options) - resp := azappconfig.GetSettingResponse{} - resp.Value = nil - return resp, nil - }) - }, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewAzAppConf(context.TODO(), tt.token(), logr) - if err != nil { - t.Errorf("failed to init AZAPPCONF") - } - - impl.WithSvc(tt.mockClient(t)) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} - -func Test_AzAppConf_Error(t *testing.T) { - logr := logger.New(&bytes.Buffer{}) - - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect error - mockClient func(t *testing.T) mockAzAppConfApi - }{ - "errored on service method call": { - func() *config.ParsedTokenConfig { - // "AZAPPCONF#/test-app-config-instance/table/token/ok", - tkn, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-app-config-instance/table/token/ok") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - store.ErrRetrieveFailed, - func(t *testing.T) mockAzAppConfApi { - return mockAzAppConfApi(func(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { - t.Helper() - resp := azappconfig.GetSettingResponse{} - return resp, fmt.Errorf("network error") - }) - }, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewAzAppConf(context.TODO(), tt.token(), logr) - if err != nil { - t.Fatal("failed to init AZAPPCONF") - } - impl.WithSvc(tt.mockClient(t)) - if _, err := impl.Value(); !errors.Is(err, tt.expect) { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - }) - } -} - -func Test_fail_AzAppConf_Client_init(t *testing.T) { - - logr := logger.New(&bytes.Buffer{}) - - // this is basically a wrap around test for the url.Parse method in the stdlib - // as that is what the client uses under the hood - token, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig()) - token.WithSanitizedToken("/%25%65%6e%301-._~/") - } - if !errors.Is(err, store.ErrClientInitialization) { - t.Fatalf(testutils.TestPhraseWithContext, "azappconf client init", err.Error(), store.ErrClientInitialization.Error()) - } -} diff --git a/internal/store/azhelpers.go b/internal/store/azhelpers.go deleted file mode 100644 index 29e66e8..0000000 --- a/internal/store/azhelpers.go +++ /dev/null @@ -1,37 +0,0 @@ -package store - -import ( - "fmt" - "strings" -) - -/* -Generic Azure Service Init Helpers -*/ - -// AzServiceHelper returns a service URI and the stripped token -type AzServiceHelper struct { - ServiceUri string - Token string -} - -// AzServiceFromToken for azure the first part of the token __must__ always be the -// identifier of the service e.g. the account name for tableStore or the Vault name for KVSecret or -// AppConfig instance -// take parameter specifies the number of elements to take from the start only -// -// e.g. a value of 2 for take will take first 2 elements from the slices -// -// For AppConfig or KeyVault we ONLY need the AppConfig instance or KeyVault instance name -func AzServiceFromToken(token string, formatUri string, take int) AzServiceHelper { - // ensure preceding slash is trimmed - stringToken := strings.Split(strings.TrimPrefix(token, "/"), "/") - splitToken := []any{} - // recast []string slice to an []any - for _, st := range stringToken { - splitToken = append(splitToken, st) - } - - uri := fmt.Sprintf(formatUri, splitToken[0:take]...) - return AzServiceHelper{ServiceUri: uri, Token: strings.Join(stringToken[take:], "/")} -} diff --git a/internal/store/azkeyvault.go b/internal/store/azkeyvault.go deleted file mode 100644 index 781b066..0000000 --- a/internal/store/azkeyvault.go +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Azure KeyVault implementation -**/ -package store - -import ( - "context" - - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" -) - -type kvApi interface { - GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) -} - -type KvScrtStore struct { - svc kvApi - ctx context.Context - logger log.ILogger - token *config.ParsedTokenConfig - config *AzKvConfig - strippedToken string -} - -// AzKvConfig takes any metadata from the token -// Version is the only -type AzKvConfig struct { - Version string `json:"version"` -} - -// NewKvScrtStore returns a KvScrtStore -// requires `AZURE_SUBSCRIPTION_ID` environment variable to be present to successfully work -func NewKvScrtStore(ctx context.Context, token *config.ParsedTokenConfig, logger log.ILogger) (*KvScrtStore, error) { - - storeConf := &AzKvConfig{} - _ = token.ParseMetadata(storeConf) - - backingStore := &KvScrtStore{ - ctx: ctx, - logger: logger, - config: storeConf, - token: token, - } - - srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.vault.azure.net", 1) - backingStore.strippedToken = srvInit.Token - - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - logger.Error("failed to get credentials: %v", err) - return nil, err - } - - c, err := azsecrets.NewClient(srvInit.ServiceUri, cred, nil) - if err != nil { - logger.Error("%v\n%w", err, ErrClientInitialization) - return nil, err - } - - backingStore.svc = c - return backingStore, nil - -} - -func (s *KvScrtStore) WithSvc(svc kvApi) { - s.svc = svc -} - -// setToken already happens in AzureKVClient in the constructor -func (implmt *KvScrtStore) SetToken(token *config.ParsedTokenConfig) {} - -func (imp *KvScrtStore) Value() (string, error) { - imp.logger.Info("Concrete implementation AzKeyVault Secret") - imp.logger.Info("AzKeyVault Token: %s", imp.token.String()) - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - // secretVersion as "" => latest - // imp.config.Version will default `""` if not specified - s, err := imp.svc.GetSecret(ctx, imp.strippedToken, imp.config.Version, nil) - if err != nil { - imp.logger.Error(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) - return "", err - } - if s.Value != nil { - return *s.Value, nil - } - imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} diff --git a/internal/store/azkeyvault_test.go b/internal/store/azkeyvault_test.go deleted file mode 100644 index 35b5c7d..0000000 --- a/internal/store/azkeyvault_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package store_test - -import ( - "context" - "fmt" - "io" - "strings" - "testing" - - "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" -) - -func Test_azSplitToken(t *testing.T) { - tests := []struct { - name string - token string - expect store.AzServiceHelper - }{ - { - name: "simple_with_preceding_slash", - token: "/test-vault/somejsontest", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-vault.vault.azure.net", - Token: "somejsontest", - }, - }, - { - name: "missing_initial_slash", - token: "test-vault/somejsontest", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-vault.vault.azure.net", - Token: "somejsontest", - }, - }, - { - name: "missing_initial_slash_multislash_secretname", - token: "test-vault/some/json/test", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-vault.vault.azure.net", - Token: "some/json/test", - }, - }, - { - name: "with_initial_slash_multislash_secretname", - token: "test-vault//some/json/test", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-vault.vault.azure.net", - Token: "/some/json/test", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := store.AzServiceFromToken(tt.token, "https://%s.vault.azure.net", 1) - if got.Token != tt.expect.Token { - t.Errorf(testutils.TestPhrase, tt.expect.Token, got.Token) - } - if got.ServiceUri != tt.expect.ServiceUri { - t.Errorf(testutils.TestPhrase, tt.expect.ServiceUri, got.ServiceUri) - } - }) - } -} - -func azKvCommonGetSecretChecker(t *testing.T, name, version, expectedName string) { - if name == "" { - t.Errorf("expect name to not be nil") - } - if name != expectedName { - t.Errorf(testutils.TestPhrase, name, expectedName) - } - - if strings.Contains(name, "#") { - t.Errorf("incorrectly stripped token separator") - } - - if strings.Contains(name, string(config.AzKeyVaultSecretsPrefix)) { - t.Errorf("incorrectly stripped prefix") - } - - if version != "" { - t.Fatal("expect version to be \"\" an empty string ") - } -} - -type mockAzKvSecretApi func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) - -func (m mockAzKvSecretApi) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - return m(ctx, name, version, options) -} - -func TestAzKeyVault(t *testing.T) { - tsuccessParam := "dssdfdweiuyh" - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockAzKvSecretApi - }{ - "successVal": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault//token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - tsuccessParam, func(t *testing.T) mockAzKvSecretApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "/token/1") - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "successVal with version": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault//token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("version:123") - return tkn - }, tsuccessParam, func(t *testing.T) mockAzKvSecretApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "/token/1") - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "successVal with keyseparator": { - func() *config.ParsedTokenConfig { - // "AZKVSECRET#/test-vault/token/1|somekey" - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault/token/1") - tkn.WithKeyPath("somekey") - tkn.WithMetadata("") - return tkn - }, tsuccessParam, func(t *testing.T) mockAzKvSecretApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - resp.Value = &tsuccessParam - return resp, nil - }) - }, - }, - "errored": { - func() *config.ParsedTokenConfig { - // "AZKVSECRET#/test-vault/token/1|somekey" - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault/token/1") - tkn.WithKeyPath("somekey") - tkn.WithMetadata("") - return tkn - }, - "unable to retrieve secret", - func(t *testing.T) mockAzKvSecretApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - return resp, fmt.Errorf("unable to retrieve secret") - }) - }, - }, - "empty": { - func() *config.ParsedTokenConfig { - // "AZKVSECRET#/test-vault/token/1|somekey" - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault/token/1") - tkn.WithKeyPath("somekey") - tkn.WithMetadata("") - return tkn - }, "", func(t *testing.T) mockAzKvSecretApi { - return mockAzKvSecretApi(func(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { - t.Helper() - azKvCommonGetSecretChecker(t, name, "", "token/1") - - resp := azsecrets.GetSecretResponse{} - return resp, nil - }) - }, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewKvScrtStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - t.Errorf("failed to init azkvstore") - } - - impl.WithSvc(tt.mockClient(t)) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} diff --git a/internal/store/aztablestorage.go b/internal/store/aztablestorage.go deleted file mode 100644 index eedef16..0000000 --- a/internal/store/aztablestorage.go +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Azure TableStore implementation -**/ -package store - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" -) - -var ErrIncorrectlyStructuredToken = errors.New("incorrectly structured token") - -// tableStoreApi -// uses this package https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/data/aztables -type tableStoreApi interface { - GetEntity(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) -} - -type AzTableStore struct { - svc tableStoreApi - ctx context.Context - logger log.ILogger - config *AzTableStrgConfig - token *config.ParsedTokenConfig - // token only without table indicators - // key only - strippedToken string -} - -type AzTableStrgConfig struct { - Format string `json:"format"` -} - -// NewAzTableStore -func NewAzTableStore(ctx context.Context, token *config.ParsedTokenConfig, logger log.ILogger) (*AzTableStore, error) { - - storeConf := &AzTableStrgConfig{} - _ = token.ParseMetadata(storeConf) - // initialToken := config.ParseMetadata(token, storeConf) - backingStore := &AzTableStore{ - ctx: ctx, - logger: logger, - config: storeConf, - token: token, - } - - srvInit := AzServiceFromToken(token.StoreToken(), "https://%s.table.core.windows.net/%s", 2) - backingStore.strippedToken = srvInit.Token - - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - logger.Error("failed to get credentials: %v", err) - return nil, err - } - - c, err := aztables.NewClient(srvInit.ServiceUri, cred, nil) - if err != nil { - logger.Error("failed to init the client: %v", err) - return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) - } - - backingStore.svc = c - return backingStore, nil -} - -func (s *AzTableStore) WithSvc(svc tableStoreApi) { - s.svc = svc -} - -// setToken already happens in the constructor -func (implmt *AzTableStore) SetToken(token *config.ParsedTokenConfig) {} - -// tokenVal in AZ table storage if an Entity contains the `value` property -// we attempt to extract it and return. -// -// From this point then normal rules of configmanager apply, -// including keySeperator and lookup. -func (imp *AzTableStore) Value() (string, error) { - imp.logger.Info("AzTableSTore Token: %s", imp.token.String()) - imp.logger.Info("Concrete implementation AzTableSTore") - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - // split the token for partition and rowKey - pKey, rKey, err := azTableStoreTokenSplitter(imp.strippedToken) - if err != nil { - return "", err - } - - s, err := imp.svc.GetEntity(ctx, pKey, rKey, &aztables.GetEntityOptions{}) - if err != nil { - imp.logger.Error(implementationNetworkErr, config.AzTableStorePrefix, err, imp.strippedToken) - return "", fmt.Errorf(implementationNetworkErr+" %w", config.AzTableStorePrefix, err, imp.token.StoreToken(), ErrRetrieveFailed) - } - if len(s.Value) > 0 { - // check for `value` property in entity - checkVal := make(map[string]interface{}) - _ = json.Unmarshal(s.Value, &checkVal) - if checkVal["value"] != nil { - return fmt.Sprintf("%v", checkVal["value"]), nil - } - return string(s.Value), nil - } - imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} - -func azTableStoreTokenSplitter(token string) (partitionKey, rowKey string, err error) { - splitToken := strings.Split(strings.TrimPrefix(token, "/"), "/") - if len(splitToken) < 2 { - return "", "", fmt.Errorf("token: %s - could not be correctly destructured to pluck the partition and row keys\n%w", token, ErrIncorrectlyStructuredToken) - } - partitionKey = splitToken[0] - rowKey = splitToken[1] - // naked return to save having to define another struct - return -} diff --git a/internal/store/aztablestorage_test.go b/internal/store/aztablestorage_test.go deleted file mode 100644 index 892ee9e..0000000 --- a/internal/store/aztablestorage_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package store_test - -import ( - "context" - "errors" - "fmt" - "io" - "strings" - "testing" - - "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" -) - -func azTableStoreCommonChecker(t *testing.T, partitionKey, rowKey, expectedPartitionKey, expectedRowKey string) { - t.Helper() - if partitionKey == "" { - t.Errorf("expect name to not be nil") - } - if partitionKey != expectedPartitionKey { - t.Errorf(testutils.TestPhrase, partitionKey, expectedPartitionKey) - } - - if strings.Contains(partitionKey, string(config.AzTableStorePrefix)) { - t.Errorf("incorrectly stripped prefix") - } - - if rowKey != expectedRowKey { - t.Errorf(testutils.TestPhrase, rowKey, expectedPartitionKey) - } -} - -type mockAzTableStoreApi func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) - -func (m mockAzTableStoreApi) GetEntity(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - return m(ctx, partitionKey, rowKey, options) -} - -func Test_AzTableStore_Success(t *testing.T) { - - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockAzTableStoreApi - }{ - "successVal": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE#/test-account/table//token/1" - tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-account/table//token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "tsuccessParam", func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - resp := aztables.GetEntityResponse{} - resp.Value = []byte("tsuccessParam") - return resp, nil - }) - }, - }, - // "successVal with :// token Separator": {"AZTABLESTORE:///test-account/table//token/1", "tsuccessParam", func(t *testing.T) tableStoreApi { - // return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - // t.Helper() - // azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - // resp := aztables.GetEntityResponse{} - // resp.Value = []byte("tsuccessParam") - // return resp, nil - // }) - // }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), - // }, - // "successVal with keyseparator but no val returned": {"AZTABLESTORE#/test-account/table/token/1|somekey", "", func(t *testing.T) tableStoreApi { - // return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - // t.Helper() - // azTableStoreCommonChecker(t, partitionKey, rowKey, "token", "1") - - // resp := aztables.GetEntityResponse{} - // resp.Value = nil - // return resp, nil - // }) - // }, - // config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), - // }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - t.Errorf("failed to init aztablestore") - } - - impl.WithSvc(tt.mockClient(t)) - - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} - -func Test_azstorage_with_value_property(t *testing.T) { - - conf := config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://") - ttests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockAzTableStoreApi - }{ - "return value property with json like object": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE:///test-account/table/partitionkey/rowKey|host", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) - tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") - tkn.WithKeyPath("host") - return tkn - }, - "map[bool:true host:foo port:1234]", - func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{Value: []byte(`{"value":{"host":"foo","port":1234,"bool":true}}`)} - return resp, nil - }) - }, - }, - "return value property with string only": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) - tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") - // tkn.WithKeyPath("host") - // tkn.WithMetadata("version:123]") - return tkn - }, - "foo.bar.com", - func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{Value: []byte(`{"value":"foo.bar.com"}`)} - return resp, nil - }) - }, - }, - "return value property with numeric only": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) - tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") - // tkn.WithKeyPath("host") - // tkn.WithMetadata("version:123]") - return tkn - }, - "1234", - func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{Value: []byte(`{"value":1234}`)} - return resp, nil - }) - }, - }, - "return value property with boolean only": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE:///test-account/table/partitionkey/rowKey", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *conf) - tkn.WithSanitizedToken("/test-account/table/partitionkey/rowKey") - return tkn - }, - "false", - func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{Value: []byte(`{"value":false}`)} - return resp, nil - }) - }, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - // token, _ := config.NewToken(tt.token(), *tt.config) - - impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - t.Fatal("failed to init aztablestore") - } - - impl.WithSvc(tt.mockClient(t)) - - got, err := impl.Value() - if err != nil { - t.Fatalf(testutils.TestPhrase, err.Error(), nil) - } - - if got != tt.expect { - t.Errorf(testutils.TestPhraseWithContext, "AZ Table storage with value property inside entity", fmt.Sprintf("%q", got), fmt.Sprintf("%q", tt.expect)) - } - }) - } -} - -func Test_AzTableStore_Error(t *testing.T) { - - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect error - mockClient func(t *testing.T) mockAzTableStoreApi - }{ - "errored on token parsing to partiationKey": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE#/test-vault/token/1|somekey" - tkn, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault/token/1") - tkn.WithKeyPath("somekey") - tkn.WithMetadata("") - return tkn - }, store.ErrIncorrectlyStructuredToken, func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, nil - }) - }, - }, - "errored on service method call": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE#/test-account/table/token/ok", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-account/table/token/ok") - return tkn - }, - store.ErrRetrieveFailed, - func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, fmt.Errorf("network error") - }) - }, - }, - - "empty": { - func() *config.ParsedTokenConfig { - // "AZTABLESTORE#/test-vault/token/1|somekey", - tkn, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/test-vault/token/1|somekey") - return tkn - }, - store.ErrIncorrectlyStructuredToken, func(t *testing.T) mockAzTableStoreApi { - return mockAzTableStoreApi(func(ctx context.Context, partitionKey string, rowKey string, options *aztables.GetEntityOptions) (aztables.GetEntityResponse, error) { - t.Helper() - resp := aztables.GetEntityResponse{} - return resp, nil - }) - }, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewAzTableStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - t.Fatal("failed to init aztablestore") - } - - impl.WithSvc(tt.mockClient(t)) - if _, err := impl.Value(); !errors.Is(err, tt.expect) { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - }) - } -} - -func Test_fail_AzTable_Client_init(t *testing.T) { - // this is basically a wrap around test for the url.Parse method in the stdlib - // as that is what the client uses under the hood - token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig()) - // "AZTABLESTORE:///%25%65%6e%301-._~/") - } - if !errors.Is(err, store.ErrClientInitialization) { - t.Fatalf(testutils.TestPhraseWithContext, "aztables client init", err.Error(), store.ErrClientInitialization.Error()) - } -} - -func Test_azSplitTokenTableStore(t *testing.T) { - - tests := []struct { - name string - token string - expect store.AzServiceHelper - }{ - { - name: "simple_with_preceding_slash", - token: "/test-account/tablename/somejsontest", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-account.table.core.windows.net/tablename", - Token: "somejsontest", - }, - }, - { - name: "missing_initial_slash", - token: "test-account/tablename/somejsontest", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-account.table.core.windows.net/tablename", - Token: "somejsontest", - }, - }, - { - name: "missing_initial_slash_multislash_secretname", - token: "test-account/tablename/some/json/test", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-account.table.core.windows.net/tablename", - Token: "some/json/test", - }, - }, - { - name: "with_initial_slash_multislash_secretname", - token: "test-account/tablename//some/json/test", - expect: store.AzServiceHelper{ - ServiceUri: "https://test-account.table.core.windows.net/tablename", - Token: "/some/json/test", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := store.AzServiceFromToken(tt.token, "https://%s.table.core.windows.net/%s", 2) - if got.Token != tt.expect.Token { - t.Errorf(testutils.TestPhrase, tt.expect.Token, got.Token) - } - if got.ServiceUri != tt.expect.ServiceUri { - t.Errorf(testutils.TestPhrase, tt.expect.ServiceUri, got.ServiceUri) - } - }) - } -} diff --git a/internal/store/gcpsecrets.go b/internal/store/gcpsecrets.go deleted file mode 100644 index 07c43e2..0000000 --- a/internal/store/gcpsecrets.go +++ /dev/null @@ -1,91 +0,0 @@ -package store - -import ( - "context" - "fmt" - - gcpsecrets "cloud.google.com/go/secretmanager/apiv1" - gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/googleapis/gax-go/v2" -) - -type gcpSecretsApi interface { - AccessSecretVersion(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) -} - -type GcpSecrets struct { - svc gcpSecretsApi - logger log.ILogger - ctx context.Context - config *GcpSecretsConfig - close func() error - token *config.ParsedTokenConfig -} - -type GcpSecretsConfig struct { - Version string `json:"version"` -} - -func NewGcpSecrets(ctx context.Context, logger log.ILogger) (*GcpSecrets, error) { - - c, err := gcpsecrets.NewClient(ctx) - if err != nil { - return nil, err - } - return &GcpSecrets{ - svc: c, - logger: logger, - ctx: ctx, - close: c.Close, - }, nil -} - -func (s *GcpSecrets) WithSvc(svc gcpSecretsApi) { - s.svc = svc -} - -func (imp *GcpSecrets) SetToken(token *config.ParsedTokenConfig) { - storeConf := &GcpSecretsConfig{} - _ = token.ParseMetadata(storeConf) - imp.token = token - imp.config = storeConf -} - -func (imp *GcpSecrets) Value() (string, error) { - // Close client currently as new one would be created per iteration - defer func() { - _ = imp.close() - }() - - imp.logger.Info("Concrete implementation GcpSecrets") - imp.logger.Info("GcpSecrets Token: %s", imp.token.String()) - - version := "latest" - if imp.config.Version != "" { - version = imp.config.Version - } - - imp.logger.Info("Getting Secret: %s @version: %s", imp.token, version) - - input := &gcpsecretspb.AccessSecretVersionRequest{ - Name: fmt.Sprintf("%s/versions/%s", imp.token.StoreToken(), version), - } - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - result, err := imp.svc.AccessSecretVersion(ctx, input) - - if err != nil { - imp.logger.Error(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) - return "", err - } - if result.Payload != nil { - return string(result.Payload.Data), nil - } - - imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} diff --git a/internal/store/gcpsecrets_test.go b/internal/store/gcpsecrets_test.go deleted file mode 100644 index 5f859ba..0000000 --- a/internal/store/gcpsecrets_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package store_test - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "testing" - - gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" - "github.com/googleapis/gax-go/v2" -) - -type mockGcpSecretsApi func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) - -func (m mockGcpSecretsApi) AccessSecretVersion(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - return m(ctx, req, opts...) -} - -func (m mockGcpSecretsApi) Close() error { - return nil -} - -var TEST_GCP_CREDS = []byte(`{ - "type": "service_account", - "project_id": "xxxxx", - "private_key_id": "yyyyyyyyyyyy", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDf842hcn5Nvp6e\n7yKARaCVIDfLXpKDhRwUOvHMzJ1ioRgQo/kbv1n4yHGCSUFyY6hKGj0HBjaGj5kE\n79H/6Y3dJNGhnsMnxBhHdo+3FI8QF0CHZh460NMZSAJ41UMQSBGssGVsNfyUzXGH\nLc45sIx/Twx3yr1k2GD3E8FlDcKlZqa3xGHf+aipg2X3NxbYi+Sz7Yed+SOMhNHl\ncX6E/TqG9n1aTyIwjMIHscCYarJqURkJxr24ukDroCeMxAfxYTdMvRU2e8pFEdoY\nrgUC88fYfaVI5txJ6j/ZKauKQX9Pa8tSyXJeGva3JYp4VC7V4IyoVviCUgEGWZDN\n6/i3zoF/AgMBAAECggEAcVBCcVYFIkE48SH+Svjv74SFtpj7eSB4vKO2hPFjEOyB\nyKmu+aMwWvjQtiNqwf46wIPWLR+vpxYxTpYpo1sBNMvUZfp2tEA8KKyMuw3j9ThO\npjO9R/UxWrFcztbZP/u3NbFrH/2Q95mbv9IlbnsuG5xbqqEig0wYg+uzBvaXbig3\n/Jr0vLT2BkRCBKQkYGjVZcHlHVLoF7/J8cghFgkV1PGvknOv6/q7qzn9L4TjQIet\nfhrhN8Z1vgFiSYtpjP6YQEUEPSHmCQeD3WzJcnASPpU2uCUwd/z65ltKPnn+rqMt\n6jt9R1S1Ju2ZSjv+kR5fIXzihdOzncyzDDm33c/QwQKBgQD2QDZuzLjTxnhsfGii\nKJDAts+Jqfs/6SeEJcJKtEngj4m7rgzyEjbKVp8qtRHIzglKRWAe62/qzzy2BkKi\nvAd4+ZzmG2SkgypGsKVfjGXVFixz2gtUdmBOmK/TnYsxNT9yTt+rX9IGqKK60q73\nOWl8VsliLIsfvSH7+bqi7sRcXQKBgQDo0VUebyQHoTAXPdzGy2ysrVPDiHcldH0Y\n/hvhQTZwxYaJr3HpOCGol2Xl6zyawuudEQsoQwJ3Li6yeb0YMGiWX77/t+qX3pSn\nkGuoftGaNDV7sLn9UV2y+InF8EL1CasrhG1k5RIuxyfV0w+QUo+E7LpVR5XkbJqT\n9QNKnDQXiwKBgQDvvEYCCqbp7e/xVhEbxbhfFdro4Cat6tRAz+3egrTlvXhO0jzi\nMp9Kz5f3oP5ma0gaGX5hu75icE1fvKqE+d+ghAqe7w5FJzkyRulJI0tEb2jphN7A\n5NoPypBqyZboWjmhlG4mzouPVf/POCuEnk028truDAWJ6by7Lj3oP+HFNQKBgQCc\n5BQ8QiFBkvnZb7LLtGIzq0n7RockEnAK25LmJRAOxs13E2fsBguIlR3x5qgckqY8\nXjPqmd2bet+1HhyzpEuWqkcIBGRum2wJz2T9UxjklbJE/D8Z2i8OYDZX0SUOA8n5\ntXASwduS8lqB2Y1vcHOO3AhlV6xHFnjEpCPnr4PbKQKBgAhQ9D9MPeuz+5yw3yHg\nkvULZRtud+uuaKrOayprN25RTxr9c0erxqnvM7KHeo6/urOXeEa7x2n21kAT0Nch\nkF2RtWBLZKXGZEVBtw1Fw0UKNh4IDgM26dwlzRfTVHCiw6M6dCiTNk9KkP2vlkim\n3QFDSSUp+eBTXA17WkDAQf7w\n-----END PRIVATE KEY-----\n", - "client_email": "foo@project.iam.gserviceaccount.com", - "client_id": "99999911111111", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bla" - }`) - -func fixtureInitMockClient(t *testing.T) struct { - name string - close func() error - delete func(name string) error -} { - - cf, err := os.CreateTemp("", "gcp-creds*") - if err != nil { - t.Fatalf(testutils.TestPhraseWithContext, "unable to set up creds file", err.Error(), nil) - } - if _, err := cf.Write(TEST_GCP_CREDS); err != nil { - t.Fatalf(testutils.TestPhraseWithContext, "unable to write mock creds into file", err.Error(), nil) - } - - resp := struct { - name string - close func() error - delete func(name string) error - }{ - name: cf.Name(), - close: cf.Close, - delete: os.Remove, - } - return resp -} - -func gcpSecretsGetChecker(t *testing.T, req *gcpsecretspb.AccessSecretVersionRequest) { - t.Helper() - if req.Name == "" { - t.Fatal("expect name to not be nil") - } - if strings.Contains(req.Name, "#") { - t.Errorf("incorrectly stripped token separator") - } - if strings.Contains(req.Name, string(config.GcpSecretsPrefix)) { - t.Errorf("incorrectly stripped prefix") - } -} - -func Test_GetGcpSecretVarHappy(t *testing.T) { - - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockGcpSecretsApi - }{ - "success": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "someValue", func(t *testing.T) mockGcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{ - Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, - }, nil - }) - }, - }, - "success with version": { - func() *config.ParsedTokenConfig { - // "GCPSECRETS#/token/1[version=123]" - tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("version=123") - return tkn - }, "someValue", func(t *testing.T) mockGcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{ - Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, - }, nil - }) - }, - }, - "error": { - func() *config.ParsedTokenConfig { - // "GCPSECRETS#/token/1" - tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "unable to retrieve secret", func(t *testing.T) mockGcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return nil, fmt.Errorf("unable to retrieve secret") - }) - }, - }, - "found but empty": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.GcpSecretsPrefix, *config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "", - func(t *testing.T) mockGcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{}, nil - }) - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - fixture := fixtureInitMockClient(t) - defer fixture.close() - defer fixture.delete(fixture.name) - - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fixture.name) - - impl, err := store.NewGcpSecrets(context.TODO(), log.New(io.Discard)) - - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) - } - - impl.WithSvc(tt.mockClient(t)) - - impl.SetToken(tt.token()) - got, err := impl.Value() - - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} diff --git a/internal/store/hashivault.go b/internal/store/hashivault.go deleted file mode 100644 index 558c9b2..0000000 --- a/internal/store/hashivault.go +++ /dev/null @@ -1,172 +0,0 @@ -package store - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strconv" - "strings" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - - vault "github.com/hashicorp/vault/api" - auth "github.com/hashicorp/vault/api/auth/aws" -) - -// HashiVaultHelper provides a broken up string -type HashiVaultHelper struct { - Path string - Token string -} - -type hashiVaultApi interface { - Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) - GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) -} - -type VaultStore struct { - svc hashiVaultApi - ctx context.Context - logger log.ILogger - config *VaultConfig - token *config.ParsedTokenConfig - strippedToken string -} - -// VaultConfig holds the parseable metadata struct -type VaultConfig struct { - Version string `json:"version"` - Role string `json:"iam_role"` -} - -func NewVaultStore(ctx context.Context, token *config.ParsedTokenConfig, logger log.ILogger) (*VaultStore, error) { - storeConf := &VaultConfig{} - _ = token.ParseMetadata(storeConf) - imp := &VaultStore{ - ctx: ctx, - logger: logger, - config: storeConf, - token: token, - } - - config := vault.DefaultConfig() - vt := SplitHashiVaultToken(token.StoreToken()) - imp.strippedToken = vt.Token - client, err := vault.NewClient(config) - if err != nil { - return nil, fmt.Errorf("%v\n%w", err, ErrClientInitialization) - } - - if strings.HasPrefix(os.Getenv("VAULT_TOKEN"), "aws_iam") { - awsclient, err := newVaultStoreWithAWSAuthIAM(client, storeConf.Role) - if err != nil { - return nil, err - } - client = awsclient - } - imp.svc = client.KVv2(vt.Path) - return imp, nil -} - -func (s *VaultStore) WithSvc(svc hashiVaultApi) { - s.svc = svc -} - -// newVaultStoreWithAWSAuthIAM returns an initialised client with AWSIAMAuth -// EC2 auth type is not supported currently -func newVaultStoreWithAWSAuthIAM(client *vault.Client, role string) (*vault.Client, error) { - if len(role) < 1 { - return nil, fmt.Errorf("role provided is empty, EC2 auth not supported") - } - awsAuth, err := auth.NewAWSAuth( - auth.WithRole(role), - ) - if err != nil { - return nil, fmt.Errorf("unable to initialize AWS auth method: %s. %w", err, ErrClientInitialization) - } - - authInfo, err := client.Auth().Login(context.Background(), awsAuth) - - if err != nil { - return nil, fmt.Errorf("unable to login to AWS auth method: %s. %w", err, ErrClientInitialization) - } - if authInfo == nil { - return nil, fmt.Errorf("no auth info was returned after login") - } - - return client, nil -} - -// setTokenVal -// imp.token is already set in the Vault constructor -// -// This happens inside the New func call -// due to the way the client needs to be -// initialised with a mountpath -// and mountpath is part of the token so it is set then -func (imp *VaultStore) SetToken(token *config.ParsedTokenConfig) {} - -// getTokenValue implements the underlying techonology -// token retrieval and returns a stringified version -// of the secret -func (imp *VaultStore) Value() (string, error) { - imp.logger.Info("%s", "Concrete implementation HashiVault") - imp.logger.Info("Getting Secret: %s", imp.token) - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - secret, err := imp.getSecret(ctx, imp.strippedToken, imp.config.Version) - if err != nil { - imp.logger.Error(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) - return "", err - } - - if secret.Data != nil { - resp, err := marshall(secret.Data) - if err != nil { - imp.logger.Error("marshalling error: %s", err.Error()) - return "", err - } - imp.logger.Debug("marhalled kvv2: %s", resp) - return resp, nil - } - - imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} - -func (imp *VaultStore) getSecret(ctx context.Context, token string, version string) (*vault.KVSecret, error) { - if version != "" { - v, err := strconv.Atoi(version) - if err != nil { - return nil, fmt.Errorf("unable to parse version into an integer: %s", err.Error()) - } - return imp.svc.GetVersion(ctx, token, v) - } - return imp.svc.Get(ctx, token) -} - -func SplitHashiVaultToken(token string) HashiVaultHelper { - vh := HashiVaultHelper{} - // split token to extract the mount path - s := strings.Split(strings.TrimPrefix(token, "/"), "___") - // grab token and trim prefix if slash - vh.Token = strings.TrimPrefix(strings.Join(s[1:], ""), "/") - // assign mount path as extracted from input token - vh.Path = s[0] - return vh -} - -// marshall converts map[string]any into a JSON -// object. Secrets should only be a single level -// deep. -func marshall(secret map[string]any) (string, error) { - b, err := json.Marshal(secret) - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/internal/store/hashivault_test.go b/internal/store/hashivault_test.go deleted file mode 100644 index 8c9aed8..0000000 --- a/internal/store/hashivault_test.go +++ /dev/null @@ -1,624 +0,0 @@ -package store_test - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" - vault "github.com/hashicorp/vault/api" -) - -func TestMountPathExtract(t *testing.T) { - ttests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - }{ - "without leading slash": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/demo/configmanager" - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/demo/configmanager") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "secret"}, - "with leading slash": { - func() *config.ParsedTokenConfig { - // "VAULT:///secret___/demo/configmanager", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/secret___/demo/configmanager") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "secret"}, - "with underscore in path name": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("_secret___/demo/configmanager") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "_secret"}, - "with double underscore in path name": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("__secret___/demo/configmanager") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "__secret"}, - "with multiple paths in mountpath": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret/bar/path___/demo/configmanager") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, "secret/bar/path"}, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - got := store.SplitHashiVaultToken(tt.token().StoreToken()) - if got.Path != tt.expect { - t.Errorf("got %q, expected %q", got, tt.expect) - } - }) - } -} - -type mockVaultApi struct { - g func(ctx context.Context, secretPath string) (*vault.KVSecret, error) - gv func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) -} - -func (m mockVaultApi) Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - return m.g(ctx, secretPath) -} - -func (m mockVaultApi) GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - return m.gv(ctx, secretPath, version) -} - -func TestVaultScenarios(t *testing.T) { - - ttests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockVaultApi - setupEnv func() func() - }{ - "happy return": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/foo") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `{"foo":"test2130-9sd-0ds"}`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "foo" { - t.Errorf("got %v; want %s", secretPath, `foo`) - } - m := make(map[string]interface{}) - m["foo"] = "test2130-9sd-0ds" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "incorrect json": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/foo", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/foo") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `json: unsupported type: func() error`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "foo" { - t.Errorf("got %v; want %s", secretPath, `foo`) - } - m := make(map[string]interface{}) - m["error"] = func() error { return fmt.Errorf("ddodod") } - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "another return": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret/engine1___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `{"foo1":"test2130-9sd-0ds","foo2":"dsfsdf3454456"}`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - m["foo1"] = "test2130-9sd-0ds" - m["foo2"] = "dsfsdf3454456" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "not found": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/foo", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/foo") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `secret not found`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "foo" { - t.Errorf("got %v; want %s", secretPath, `foo`) - } - return nil, fmt.Errorf("secret not found") - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "403": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/some/other/foo2", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `client 403`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) - } - return nil, fmt.Errorf("client 403") - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "found but empty": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/some/other/foo2", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - // config.NewConfig(), - `{}`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "found but nil returned": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "", - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return &vault.KVSecret{Data: nil}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "version provided correctly": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/some/other/foo2[version=1]", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("version=1") - return tkn - }, - `{"foo2":"dsfsdf3454456"}`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - m["foo2"] = "dsfsdf3454456" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "version provided but unable to parse": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/some/other/foo2[version=1a]", - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("version=1a") - return tkn - }, - "unable to parse version into an integer: strconv.Atoi: parsing \"1a\": invalid syntax", - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return nil, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "129378y1231283") - return func() { - os.Clearenv() - } - }, - }, - "vault rate limit incorrect": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - `error encountered setting up default configuration: VAULT_RATE_LIMIT was provided but incorrectly formatted -failed to initialize the client`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return &vault.KVSecret{Data: nil}, nil - } - return mv - }, - func() func() { - os.Setenv("VAULT_TOKEN", "") - os.Setenv("VAULT_RATE_LIMIT", "wrong") - return func() { - os.Clearenv() - } - }, - }, - } - - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - tearDown := tt.setupEnv() - defer tearDown() - - impl, err := store.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - if err.Error() != tt.expect { - t.Fatalf("failed to init hashivault, %v", err.Error()) - } - return - } - - impl.WithSvc(tt.mockClient(t)) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} - -func TestAwsIamAuth(t *testing.T) { - ttests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockVaultApi - mockHanlder func(t *testing.T) http.Handler - setupEnv func(addr string) func() - }{ - "aws_iam auth no role specified": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("version=1") - return tkn - }, - "role provided is empty, EC2 auth not supported", - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return &vault.KVSecret{Data: nil}, nil - } - return mv - }, - func(t *testing.T) http.Handler { - return nil - }, - func(_ string) func() { - os.Setenv("VAULT_TOKEN", "aws_iam") - os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") - os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") - os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") - os.Setenv("AWS_REGION", "eu-west-1") - return func() { - os.Clearenv() - } - }, - }, - "aws_iam auth incorrectly formatted request": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("version=1,iam_role=not_a_role") - return tkn - }, - `unable to login to AWS auth method: unable to log in to auth method: unable to log in with AWS auth: Error making API request. - -URL: PUT %s/v1/auth/aws/login -Code: 400. Raw Message: - -incorrect values supplied. failed to initialize the client`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - return &vault.KVSecret{Data: nil}, nil - } - return mv - }, - func(t *testing.T) http.Handler { - mux := http.NewServeMux() - mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(400) - w.Write([]byte(`incorrect values supplied`)) - }) - return mux - }, - func(addr string) func() { - os.Setenv("VAULT_TOKEN", "aws_iam") - os.Setenv("VAULT_ADDR", addr) - os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") - os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") - os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") - os.Setenv("AWS_REGION", "eu-west-1") - return func() { - os.Clearenv() - } - }, - }, - "aws_iam auth success": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") - return tkn - }, - // - `{"foo2":"dsfsdf3454456"}`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - m := make(map[string]any) - m["foo2"] = "dsfsdf3454456" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func(t *testing.T) http.Handler { - mux := http.NewServeMux() - mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(`{"auth":{"client_token": "fooresddfasdsasad"}}`)) - }) - return mux - }, - func(addr string) func() { - os.Setenv("VAULT_TOKEN", "aws_iam") - os.Setenv("VAULT_ADDR", addr) - os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") - os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") - os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") - os.Setenv("AWS_REGION", "eu-west-1") - return func() { - os.Clearenv() - } - }, - }, - "aws_iam auth no token returned": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("secret___/some/other/foo2") - tkn.WithKeyPath("") - tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") - return tkn - }, - `unable to login to AWS auth method: response did not return ClientToken, client token not set. failed to initialize the client`, - func(t *testing.T) mockVaultApi { - mv := mockVaultApi{} - mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { - t.Helper() - if secretPath != "some/other/foo2" { - t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) - } - m := make(map[string]interface{}) - m["foo2"] = "dsfsdf3454456" - return &vault.KVSecret{Data: m}, nil - } - return mv - }, - func(t *testing.T) http.Handler { - mux := http.NewServeMux() - mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(`{"auth":{}}`)) - }) - return mux - }, - func(addr string) func() { - os.Setenv("VAULT_TOKEN", "aws_iam") - os.Setenv("VAULT_ADDR", addr) - os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") - os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") - os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") - os.Setenv("AWS_REGION", "eu-west-1") - return func() { - os.Clearenv() - } - }, - }, - } - - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - // - ts := httptest.NewServer(tt.mockHanlder(t)) - tearDown := tt.setupEnv(ts.URL) - defer tearDown() - impl, err := store.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) - if err != nil { - // WHAT A CRAP way to do this... - if err.Error() != strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0] { - t.Errorf(testutils.TestPhraseWithContext, "aws iam auth", err.Error(), strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0]) - t.Fatalf("failed to init hashivault, %v", err.Error()) - } - return - } - - impl.WithSvc(tt.mockClient(t)) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} diff --git a/internal/store/paramstore_test.go b/internal/store/paramstore_test.go deleted file mode 100644 index 8fc11d4..0000000 --- a/internal/store/paramstore_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package store_test - -import ( - "context" - "fmt" - "io" - "strings" - "testing" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" - "github.com/aws/aws-sdk-go-v2/service/ssm" - "github.com/aws/aws-sdk-go-v2/service/ssm/types" -) - -type mockParamApi func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) - -func (m mockParamApi) GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - return m(ctx, params, optFns...) -} - -func awsParamtStoreCommonGetChecker(t *testing.T, params *ssm.GetParameterInput) { - if params.Name == nil { - t.Fatal("expect name to not be nil") - } - - if strings.Contains(*params.Name, "#") { - t.Errorf("incorrectly stripped token separator") - } - - if strings.Contains(*params.Name, string(config.ParamStorePrefix)) { - t.Errorf("incorrectly stripped prefix") - } - - if !*params.WithDecryption { - t.Fatal("expect WithDecryption to not be false") - } -} - -func Test_GetParamStore(t *testing.T) { - var ( - tsuccessParam = "someVal" - // tsuccessObj map[string]string = map[string]string{"AWSPARAMSTR#/token/1": "someVal"} - ) - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockParamApi - }{ - "successVal": { - func() *config.ParsedTokenConfig { - // "VAULT://secret___/demo/configmanager" - tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - tsuccessParam, func(t *testing.T) mockParamApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: &tsuccessParam}, - }, nil - }) - }, - }, - "successVal with keyseparator": { - func() *config.ParsedTokenConfig { - // "AWSPARAMSTR#/token/1|somekey", - tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("somekey") - tkn.WithMetadata("") - return tkn - }, - tsuccessParam, func(t *testing.T) mockParamApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - - if strings.Contains(*params.Name, "|somekey") { - t.Errorf("incorrectly stripped key separator") - } - - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: &tsuccessParam}, - }, nil - }) - }, - }, - "errored": { - func() *config.ParsedTokenConfig { - // "AWSPARAMSTR#/token/1", - tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "unable to retrieve", func(t *testing.T) mockParamApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return nil, fmt.Errorf("unable to retrieve") - }) - }, - }, - "nil to empty": { - func() *config.ParsedTokenConfig { - // "AWSPARAMSTR#/token/1", - tkn, _ := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "", func(t *testing.T) mockParamApi { - return mockParamApi(func(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) { - t.Helper() - awsParamtStoreCommonGetChecker(t, params) - return &ssm.GetParameterOutput{ - Parameter: &types.Parameter{Value: nil}, - }, nil - }) - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, err := store.NewParamStore(context.TODO(), log.New(io.Discard)) - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) - } - impl.WithSvc(tt.mockClient(t)) - impl.SetToken(tt.token()) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} diff --git a/internal/store/plugin.go b/internal/store/plugin.go new file mode 100644 index 0000000..ddfd739 --- /dev/null +++ b/internal/store/plugin.go @@ -0,0 +1,64 @@ +package store + +import ( + "context" + "os/exec" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/plugins" + "github.com/hashicorp/go-plugin" +) + +// Plugin is responsible for managing plugins within configmanager +// +// It includes the following methods +// - fetch plugins from known sources +// - maintains a list of tokens answerable by a specified pluginEngine +type Plugin struct { + Implementations config.ImplementationPrefix + SourcePath string + Version string + ClientCleanUp func() + tokenStore plugins.TokenStore +} + +// New Plugin gets called once per implementation +func New(ctx context.Context, path string, prefix config.ImplementationPrefix) (*Plugin, error) { + // We're a host. Start by launching the plugin process. + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: plugins.Handshake, + Plugins: plugin.PluginSet{"configmanager_token_store": &plugins.TokenStoreGRPCPlugin{}}, + Cmd: exec.Command(path), + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + client.Kill() + return nil, err + } + + // ensure the loaded plugin can dispense the required prefix implementation + raw, err := rpcClient.Dispense("configmanager_token_store") + if err != nil { + client.Kill() + return nil, err + } + + ts := raw.(plugins.TokenStore) + + p := &Plugin{ + ClientCleanUp: client.Kill, + tokenStore: ts, + } + return p, nil +} + +func (p *Plugin) GetValue(token *config.ParsedTokenConfig) (string, error) { + result, err := p.tokenStore.Value(token.StoreToken(), []byte(token.Metadata())) + if err != nil { + return "", err + } + return result, nil +} diff --git a/internal/store/plugin_test.go b/internal/store/plugin_test.go new file mode 100644 index 0000000..f646841 --- /dev/null +++ b/internal/store/plugin_test.go @@ -0,0 +1,34 @@ +package store_test + +import ( + "context" + "os" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/store" +) + +func TestPlugin_GetValue_integration(t *testing.T) { + // as the plugin is technically a subprocess + // setting env vars at this level will affect the loaded plugin + os.Setenv("AWS_REGION", "eu-west-1") + os.Setenv("AWS_PROFILE", "PROFILE_TO_USE") + np, err := store.New(context.TODO(), "../../plugins/awsparamstr/bin/awsparamstr", config.ParamStorePrefix) + if err != nil { + t.Fatal(err) + } + defer np.ClientCleanUp() + token, err := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) + if err != nil { + t.Fatal(err) + } + token.WithSanitizedToken("/int-test/pocketbase/admin-pwd") + got, err := np.GetValue(token) + if err != nil { + t.Fatal(err) + } + if got == "" { + t.Error("empty...") + } +} diff --git a/internal/store/secretsmanager.go b/internal/store/secretsmanager.go deleted file mode 100644 index 6744d8a..0000000 --- a/internal/store/secretsmanager.go +++ /dev/null @@ -1,93 +0,0 @@ -package store - -import ( - "context" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/aws/aws-sdk-go-v2/aws" - awsconf "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" -) - -type secretsMgrApi interface { - GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) -} - -type SecretsMgr struct { - svc secretsMgrApi - ctx context.Context - logger log.ILogger - config *SecretsMgrConfig - token *config.ParsedTokenConfig -} - -type SecretsMgrConfig struct { - Version string `json:"version"` -} - -func NewSecretsMgr(ctx context.Context, logger log.ILogger) (*SecretsMgr, error) { - cfg, err := awsconf.LoadDefaultConfig(ctx) - if err != nil { - logger.Error("unable to load SDK config, %v\n%w", err, ErrClientInitialization) - return nil, err - } - c := secretsmanager.NewFromConfig(cfg) - - return &SecretsMgr{ - svc: c, - logger: logger, - ctx: ctx, - }, nil - -} - -func (s *SecretsMgr) WithSvc(svc secretsMgrApi) { - s.svc = svc -} - -func (imp *SecretsMgr) SetToken(token *config.ParsedTokenConfig) { - storeConf := &SecretsMgrConfig{} - if err := token.ParseMetadata(storeConf); err != nil { - imp.logger.Error("parse token error %v", err) - } - imp.token = token - imp.config = storeConf -} - -func (imp *SecretsMgr) Value() (string, error) { - imp.logger.Info("Concrete implementation SecretsManager") - imp.logger.Debug("SecretsManager Token: %s", imp.token.String()) - - version := "AWSCURRENT" - if imp.config.Version != "" { - version = imp.config.Version - } - - imp.logger.Info("Getting Secret: %s @version: %s", imp.token, version) - - input := &secretsmanager.GetSecretValueInput{ - SecretId: aws.String(imp.token.StoreToken()), - VersionStage: aws.String(version), - } - - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - result, err := imp.svc.GetSecretValue(ctx, input) - if err != nil { - imp.logger.Error(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) - return "", err - } - - if result.SecretString != nil { - return *result.SecretString, nil - } - - if len(result.SecretBinary) > 0 { - return string(result.SecretBinary), nil - } - - imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} diff --git a/internal/store/secretsmanager_test.go b/internal/store/secretsmanager_test.go deleted file mode 100644 index 870bb75..0000000 --- a/internal/store/secretsmanager_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package store_test - -import ( - "context" - "fmt" - "io" - "strings" - "testing" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" -) - -type mockSecretsApi func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) - -func (m mockSecretsApi) GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - return m(ctx, params, optFns...) -} - -func awsSecretsMgrGetChecker(t *testing.T, params *secretsmanager.GetSecretValueInput) { - if params.VersionStage == nil { - t.Fatal("expect name to not be nil") - } - - if strings.Contains(*params.SecretId, "#") { - t.Errorf("incorrectly stripped token separator") - } - - if strings.Contains(*params.SecretId, string(config.SecretMgrPrefix)) { - t.Errorf("incorrectly stripped prefix") - } -} - -func Test_GetSecretMgr(t *testing.T) { - - tsuccessSecret := "dsgkbdsf" - - tests := map[string]struct { - token func() *config.ParsedTokenConfig - expect string - mockClient func(t *testing.T) mockSecretsApi - }{ - "success": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig()) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, tsuccessSecret, func(t *testing.T) mockSecretsApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: &tsuccessSecret, - }, nil - }) - }, - }, - "success with version": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("version=123") - return tkn - }, - tsuccessSecret, func(t *testing.T) mockSecretsApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: &tsuccessSecret, - }, nil - }) - }, - }, - "success with binary": { - func() *config.ParsedTokenConfig { - tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - tsuccessSecret, func(t *testing.T) mockSecretsApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretBinary: []byte(tsuccessSecret), - }, nil - }) - }, - }, - "errored": { - func() *config.ParsedTokenConfig { - // "AWSSECRETS#/token/1", "|", "#", - tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("") - return tkn - }, - "unable to retrieve secret", func(t *testing.T) mockSecretsApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return nil, fmt.Errorf("unable to retrieve secret") - }) - }, - }, - "ok but empty": { - func() *config.ParsedTokenConfig { - // "AWSSECRETS#/token/1", "|", "#", - tkn, _ := config.NewToken(config.SecretMgrPrefix, *config.NewConfig().WithTokenSeparator("#")) - tkn.WithSanitizedToken("/token/1") - tkn.WithKeyPath("") - tkn.WithMetadata("version=123") - return tkn - }, - "", func(t *testing.T) mockSecretsApi { - return mockSecretsApi(func(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error) { - t.Helper() - awsSecretsMgrGetChecker(t, params) - return &secretsmanager.GetSecretValueOutput{ - SecretString: nil, - }, nil - }) - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - impl, _ := store.NewSecretsMgr(context.TODO(), log.New(io.Discard)) - impl.WithSvc(tt.mockClient(t)) - - impl.SetToken(tt.token()) - got, err := impl.Value() - if err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - if got != tt.expect { - t.Errorf(testutils.TestPhrase, got, tt.expect) - } - }) - } -} diff --git a/internal/store/store.go b/internal/store/store.go index eceeb45..a4fdbc0 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -22,6 +22,6 @@ var ( type Strategy interface { // Value retrieves the underlying value for the token Value() (s string, e error) - // SetToken + // SetToken SetToken(s *config.ParsedTokenConfig) } diff --git a/internal/strategy/strategy.go b/internal/strategy/strategy.go index ac19a30..7aa8345 100644 --- a/internal/strategy/strategy.go +++ b/internal/strategy/strategy.go @@ -94,27 +94,27 @@ func (tr *TokenResponse) Value() string { func defaultStrategyFuncMap(logger log.ILogger) StrategyFuncMap { return map[config.ImplementationPrefix]StrategyFunc{ - config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewAzTableStore(ctx, token, logger) - }, - config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewAzAppConf(ctx, token, logger) - }, - config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewGcpSecrets(ctx, logger) - }, - config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewSecretsMgr(ctx, logger) - }, - config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewParamStore(ctx, logger) - }, - config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewKvScrtStore(ctx, token, logger) - }, - config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - return store.NewVaultStore(ctx, token, logger) - }, + // config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewAzTableStore(ctx, token, logger) + // }, + // config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewAzAppConf(ctx, token, logger) + // }, + // config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewGcpSecrets(ctx, logger) + // }, + // config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewSecretsMgr(ctx, logger) + // }, + // config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewParamStore(ctx, logger) + // }, + // config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewKvScrtStore(ctx, token, logger) + // }, + // config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + // return store.NewVaultStore(ctx, token, logger) + // }, } } diff --git a/internal/strategy/strategy_test.go b/internal/strategy/strategy_test.go index acae5e1..c5fe3f7 100644 --- a/internal/strategy/strategy_test.go +++ b/internal/strategy/strategy_test.go @@ -2,9 +2,7 @@ package strategy_test import ( "context" - "fmt" "io" - "os" "testing" "github.com/DevLabFoundry/configmanager/v3/internal/config" @@ -12,7 +10,6 @@ import ( "github.com/DevLabFoundry/configmanager/v3/internal/store" "github.com/DevLabFoundry/configmanager/v3/internal/strategy" "github.com/DevLabFoundry/configmanager/v3/internal/testutils" - "github.com/go-test/deep" ) type mockGenerate struct { @@ -116,175 +113,175 @@ func Test_CustomStrategyFuncMap_add_own(t *testing.T) { } } -func Test_SelectImpl_With(t *testing.T) { +// func Test_SelectImpl_With(t *testing.T) { - ttests := map[string]struct { - setUpTearDown func() func() - token string - config *config.GenVarsConfig - expect func() store.Strategy - expErr error - impPrefix config.ImplementationPrefix - }{ - "unknown": { - func() func() { - return func() { - } - }, - "foo/bar", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { return nil }, - fmt.Errorf("implementation not found for input string: UNKNOWN#foo/bar"), - config.UnknownPrefix, - }, - "success AZTABLESTORE": { - func() func() { - os.Setenv("AZURE_stuff", "foo") - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("#")) - token.WithSanitizedToken("foo/bar1") +// ttests := map[string]struct { +// setUpTearDown func() func() +// token string +// config *config.GenVarsConfig +// expect func() store.Strategy +// expErr error +// impPrefix config.ImplementationPrefix +// }{ +// "unknown": { +// func() func() { +// return func() { +// } +// }, +// "foo/bar", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { return nil }, +// fmt.Errorf("implementation not found for input string: UNKNOWN#foo/bar"), +// config.UnknownPrefix, +// }, +// "success AZTABLESTORE": { +// func() func() { +// os.Setenv("AZURE_stuff", "foo") +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig().WithTokenSeparator("#")) +// token.WithSanitizedToken("foo/bar1") - s, _ := store.NewAzTableStore(context.TODO(), token, log.New(io.Discard)) - return s - }, - nil, - config.AzTableStorePrefix, - }, - "success AWSPARAMSTR": { - func() func() { - os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - s, _ := store.NewParamStore(context.TODO(), log.New(io.Discard)) - return s - }, - nil, - config.ParamStorePrefix, - }, - "success AWSSECRETS": { - func() func() { - os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - s, _ := store.NewSecretsMgr(context.TODO(), log.New(io.Discard)) - return s - }, - nil, - config.SecretMgrPrefix, - }, - "success AZKVSECRET": { - func() func() { - os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - token, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithTokenSeparator("#")) - token.WithSanitizedToken("foo/bar1") - s, _ := store.NewKvScrtStore(context.TODO(), token, log.New(io.Discard)) - return s - }, - nil, - config.AzKeyVaultSecretsPrefix, - }, - "success AZAPPCONF": { - func() func() { - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - token, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithTokenSeparator("#")) - token.WithSanitizedToken("foo/bar1") - s, _ := store.NewAzAppConf(context.TODO(), token, log.New(io.Discard)) - return s - }, - nil, - config.AzAppConfigPrefix, - }, - "success VAULT": { - func() func() { - os.Setenv("VAULT_", "AAAAAAAAAAAAAAA") - return func() { - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - token, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig().WithTokenSeparator("#")) - token.WithSanitizedToken("foo/bar1") - s, _ := store.NewVaultStore(context.TODO(), token, log.New(io.Discard)) - return s - }, - nil, - config.HashicorpVaultPrefix, - }, - "success GCPSECRETS": { - func() func() { - cf, _ := os.CreateTemp(".", "*") - cf.Write(TEST_GCP_CREDS) - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", cf.Name()) - return func() { - os.Remove(cf.Name()) - os.Clearenv() - } - }, - "foo/bar1", - config.NewConfig().WithTokenSeparator("#"), - func() store.Strategy { - s, _ := store.NewGcpSecrets(context.TODO(), log.New(io.Discard)) - return s - }, - nil, - config.GcpSecretsPrefix, - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - tearDown := tt.setUpTearDown() - defer tearDown() - want := tt.expect() - rs := strategy.New(*tt.config, log.New(io.Discard)) - token, _ := config.NewToken(tt.impPrefix, *tt.config) - token.WithSanitizedToken(tt.token) - got, err := rs.GetImplementation(context.TODO(), token) +// s, _ := store.NewAzTableStore(context.TODO(), token, log.New(io.Discard)) +// return s +// }, +// nil, +// config.AzTableStorePrefix, +// }, +// "success AWSPARAMSTR": { +// func() func() { +// os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") +// os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// s, _ := store.NewParamStore(context.TODO(), log.New(io.Discard)) +// return s +// }, +// nil, +// config.ParamStorePrefix, +// }, +// "success AWSSECRETS": { +// func() func() { +// os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") +// os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// s, _ := store.NewSecretsMgr(context.TODO(), log.New(io.Discard)) +// return s +// }, +// nil, +// config.SecretMgrPrefix, +// }, +// "success AZKVSECRET": { +// func() func() { +// os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") +// os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// token, _ := config.NewToken(config.AzKeyVaultSecretsPrefix, *config.NewConfig().WithTokenSeparator("#")) +// token.WithSanitizedToken("foo/bar1") +// s, _ := store.NewKvScrtStore(context.TODO(), token, log.New(io.Discard)) +// return s +// }, +// nil, +// config.AzKeyVaultSecretsPrefix, +// }, +// "success AZAPPCONF": { +// func() func() { +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// token, _ := config.NewToken(config.AzAppConfigPrefix, *config.NewConfig().WithTokenSeparator("#")) +// token.WithSanitizedToken("foo/bar1") +// s, _ := store.NewAzAppConf(context.TODO(), token, log.New(io.Discard)) +// return s +// }, +// nil, +// config.AzAppConfigPrefix, +// }, +// "success VAULT": { +// func() func() { +// os.Setenv("VAULT_", "AAAAAAAAAAAAAAA") +// return func() { +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// token, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig().WithTokenSeparator("#")) +// token.WithSanitizedToken("foo/bar1") +// s, _ := store.NewVaultStore(context.TODO(), token, log.New(io.Discard)) +// return s +// }, +// nil, +// config.HashicorpVaultPrefix, +// }, +// "success GCPSECRETS": { +// func() func() { +// cf, _ := os.CreateTemp(".", "*") +// cf.Write(TEST_GCP_CREDS) +// os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", cf.Name()) +// return func() { +// os.Remove(cf.Name()) +// os.Clearenv() +// } +// }, +// "foo/bar1", +// config.NewConfig().WithTokenSeparator("#"), +// func() store.Strategy { +// s, _ := store.NewGcpSecrets(context.TODO(), log.New(io.Discard)) +// return s +// }, +// nil, +// config.GcpSecretsPrefix, +// }, +// } +// for name, tt := range ttests { +// t.Run(name, func(t *testing.T) { +// tearDown := tt.setUpTearDown() +// defer tearDown() +// want := tt.expect() +// rs := strategy.New(*tt.config, log.New(io.Discard)) +// token, _ := config.NewToken(tt.impPrefix, *tt.config) +// token.WithSanitizedToken(tt.token) +// got, err := rs.GetImplementation(context.TODO(), token) - if err != nil { - if err.Error() != tt.expErr.Error() { - t.Errorf(testutils.TestPhraseWithContext, "uncaught error", err.Error(), tt.expErr.Error()) - } - return - } +// if err != nil { +// if err.Error() != tt.expErr.Error() { +// t.Errorf(testutils.TestPhraseWithContext, "uncaught error", err.Error(), tt.expErr.Error()) +// } +// return +// } - diff := deep.Equal(got, want) - if diff != nil { - t.Errorf(testutils.TestPhraseWithContext, "reflection of initialised implentations", fmt.Sprintf("%q", got), fmt.Sprintf("%q", want)) - } - }) - } -} +// diff := deep.Equal(got, want) +// if diff != nil { +// t.Errorf(testutils.TestPhraseWithContext, "reflection of initialised implentations", fmt.Sprintf("%q", got), fmt.Sprintf("%q", want)) +// } +// }) +// } +// } diff --git a/plugins/awsparams/paramstore.go b/plugins/awsparams/paramstore.go deleted file mode 100644 index 9001e53..0000000 --- a/plugins/awsparams/paramstore.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "context" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/plugins" - "github.com/aws/aws-sdk-go-v2/aws" - awsConf "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/ssm" -) - -type paramStoreApi interface { - GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) -} - -type ParamStore struct { - svc paramStoreApi - ctx context.Context - config *ParamStrConfig - token *config.ParsedTokenConfig -} - -type ParamStrConfig struct { - // reserved for potential future use -} - -func NewParamStore(ctx context.Context) (*ParamStore, error) { - cfg, err := awsConf.LoadDefaultConfig(ctx) - if err != nil { - return nil, err - } - c := ssm.NewFromConfig(cfg) - - return &ParamStore{ - svc: c, - ctx: ctx, - }, nil -} - -func (s *ParamStore) WithSvc(svc paramStoreApi) { - s.svc = svc -} - -func (imp *ParamStore) Value(token *plugins.MessagExchange) (string, error) { - // imp.logger.Info("%s", "Concrete implementation ParameterStore") - // imp.logger.Info("ParamStore Token: %s", token.Token) - - input := &ssm.GetParameterInput{ - Name: aws.String(token.Token), - WithDecryption: aws.Bool(true), - } - ctx, cancel := context.WithCancel(imp.ctx) - defer cancel() - - result, err := imp.svc.GetParameter(ctx, input) - if err != nil { - // imp.logger.Error(plugins.ImplementationNetworkErr, config.ParamStorePrefix, err, token) - return "", err - } - - if result.Parameter.Value != nil { - return *result.Parameter.Value, nil - } - // imp.logger.Error("value retrieved but empty for token: %v", imp.token) - return "", nil -} diff --git a/plugins/awsparams/main.go b/plugins/awsparamstr/README.md similarity index 91% rename from plugins/awsparams/main.go rename to plugins/awsparamstr/README.md index 4bd4ec6..cc07402 100644 --- a/plugins/awsparams/main.go +++ b/plugins/awsparamstr/README.md @@ -1,5 +1,13 @@ -package main +# AWS PARAM STORE Plugin +This is the `awsparamstr` implementation plugin built using the gp-plugin architecture from hashicorp... + + +## Alternate architecture + +Explored an alternate architecture using WASIP1 + +```go import ( "context" "encoding/binary" @@ -199,5 +207,10 @@ func StrategyTokenValue(tokenPtr, tokenLen, outPtr, outCap, outLenPtr uint32) in // main is required for wasip1 // scaffolds the `_initialize` method func main() {} +``` + +### Build notes + +build using the `-buildmode=c-shared` which will convert the module to a reactor module -// GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awsparams.wasm +`GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awsparams.wasm` diff --git a/internal/store/paramstore.go b/plugins/awsparamstr/impl/paramstore.go similarity index 69% rename from internal/store/paramstore.go rename to plugins/awsparamstr/impl/paramstore.go index aa45ace..6255252 100644 --- a/internal/store/paramstore.go +++ b/plugins/awsparamstr/impl/paramstore.go @@ -1,10 +1,11 @@ -package store +package impl import ( "context" "github.com/DevLabFoundry/configmanager/v3/internal/config" "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/plugins" "github.com/aws/aws-sdk-go-v2/aws" awsConf "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ssm" @@ -17,9 +18,9 @@ type paramStoreApi interface { type ParamStore struct { svc paramStoreApi ctx context.Context - logger log.ILogger config *ParamStrConfig token *config.ParsedTokenConfig + logger log.ILogger } type ParamStrConfig struct { @@ -29,7 +30,6 @@ type ParamStrConfig struct { func NewParamStore(ctx context.Context, logger log.ILogger) (*ParamStore, error) { cfg, err := awsConf.LoadDefaultConfig(ctx) if err != nil { - logger.Error("unable to load SDK config, %v\n%w", err, ErrClientInitialization) return nil, err } c := ssm.NewFromConfig(cfg) @@ -45,19 +45,12 @@ func (s *ParamStore) WithSvc(svc paramStoreApi) { s.svc = svc } -func (imp *ParamStore) SetToken(token *config.ParsedTokenConfig) { - storeConf := &ParamStrConfig{} - _ = token.ParseMetadata(storeConf) - imp.token = token - imp.config = storeConf -} - -func (imp *ParamStore) Value() (string, error) { - imp.logger.Info("%s", "Concrete implementation ParameterStore") - imp.logger.Info("ParamStore Token: %s", imp.token.String()) +func (imp *ParamStore) Value(token string, metadata []byte) (string, error) { + imp.logger.Info("Concrete implementation ParameterStore") + imp.logger.Info("ParamStore Token: %s", token) input := &ssm.GetParameterInput{ - Name: aws.String(imp.token.StoreToken()), + Name: aws.String(token), WithDecryption: aws.Bool(true), } ctx, cancel := context.WithCancel(imp.ctx) @@ -65,7 +58,7 @@ func (imp *ParamStore) Value() (string, error) { result, err := imp.svc.GetParameter(ctx, input) if err != nil { - imp.logger.Error(implementationNetworkErr, config.ParamStorePrefix, err, imp.token.StoreToken()) + imp.logger.Error(plugins.ImplementationNetworkErr, config.ParamStorePrefix, err, token) return "", err } diff --git a/plugins/awsparams/paramstore_test.go b/plugins/awsparamstr/impl/paramstore_test.go similarity index 95% rename from plugins/awsparams/paramstore_test.go rename to plugins/awsparamstr/impl/paramstore_test.go index 84a0d23..cc64dfc 100644 --- a/plugins/awsparams/paramstore_test.go +++ b/plugins/awsparamstr/impl/paramstore_test.go @@ -1,4 +1,4 @@ -package main_test +package impl_test import ( "context" @@ -9,8 +9,8 @@ import ( "github.com/DevLabFoundry/configmanager/v3/internal/config" "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" "github.com/DevLabFoundry/configmanager/v3/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/plugins/awsparamstr/impl" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" ) @@ -131,13 +131,13 @@ func Test_GetParamStore(t *testing.T) { } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := store.NewParamStore(context.TODO(), log.New(io.Discard)) + impl, err := impl.NewParamStore(context.TODO(), log.New(io.Discard)) if err != nil { t.Errorf(testutils.TestPhrase, err.Error(), nil) } impl.WithSvc(tt.mockClient(t)) - impl.SetToken(tt.token()) - got, err := impl.Value() + + got, err := impl.Value(tt.token().StoreToken(), []byte{}) if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/plugins/awsparamstr/main.go b/plugins/awsparamstr/main.go new file mode 100644 index 0000000..c4b340f --- /dev/null +++ b/plugins/awsparamstr/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "os" + + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/plugins" + "github.com/DevLabFoundry/configmanager/v3/plugins/awsparamstr/impl" + "github.com/hashicorp/go-plugin" +) + +// Here is a real implementation of KV that writes to a local file with +// the key name and the contents are the value of the key. +type TokenStorePlugin struct{} + +func (ts TokenStorePlugin) Value(key string, metadata []byte) (string, error) { + srv, err := impl.NewParamStore(context.Background(), log.New(os.Stderr)) + if err != nil { + return "", err + } + return srv.Value(key, metadata) +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugins.Handshake, + Plugins: map[string]plugin.Plugin{ + "configmanager_token_store": &plugins.TokenStoreGRPCPlugin{Impl: &TokenStorePlugin{}}, + }, + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/awssecrets/main.go b/plugins/awssecrets/main.go deleted file mode 100644 index 8a03b12..0000000 --- a/plugins/awssecrets/main.go +++ /dev/null @@ -1,197 +0,0 @@ -package main - -import ( - "encoding/binary" - "strings" - "sync" - "unicode/utf8" - "unsafe" - - "github.com/DevLabFoundry/configmanager/v3/plugins" -) - -// ==================== -// Bump allocator -// ==================== - -const heapSize = 64 * 1024 // 64 KiB arena; tune as needed - -type bumpAllocator struct { - mu sync.Mutex - heap []byte - used uint32 -} - -var alloc = bumpAllocator{ - heap: make([]byte, heapSize), -} - -// round allocation up to 8 bytes for basic alignment. -func roundUp(n uint32) uint32 { - const align = 8 - return (n + align - 1) &^ (align - 1) -} - -//go:wasmexport allocate -func Allocate(size uint32) uint32 { - if size == 0 { - return 0 - } - size = roundUp(size) - - alloc.mu.Lock() - defer alloc.mu.Unlock() - - if alloc.used+size > uint32(len(alloc.heap)) { - // Out of memory in our arena. - return 0 - } - - offset := alloc.used - alloc.used += size - - // Return pointer into linear memory for &heap[offset]. - return uint32(uintptr(unsafe.Pointer(&alloc.heap[offset]))) -} - -//go:wasmexport deallocate -func Deallocate(ptr, size uint32) { - // For a simple bump allocator, deallocate is a no-op. - // Memory is reclaimed when the module instance is destroyed. - _ = ptr - _ = size -} - -type Hdr struct { - Data uintptr - Len int - Cap int -} - -// ==================== -// Helpers -// ==================== - -// bytesFromPtrLen reinterprets a (ptr,len) pair in wasm linear memory -// as a Go []byte without copying. -func bytesFromPtrLen(ptr, length uint32) []byte { - if length == 0 { - return nil - } - - hdr := Hdr{ - Data: uintptr(ptr), - Len: int(length), - Cap: int(length), - } - - return *(*[]byte)(unsafe.Pointer(&hdr)) -} - -// ==================== -// strategy_token_value -// ==================== -// -// ABI: -// -// strategy_token_value( -// inPtr, inLen, outPtr, outCap, outLenPtr uint32, -// ) int32 -// -// Host contract: -// - Input bytes are at (inPtr, inLen) -// - Output buffer is [outPtr : outPtr+outCap) -// - outLenPtr points to 4 bytes where we write the required length -// -// Behaviour: -// - If input length == 0 => ERR_EMPTY_INPUT -// - If invalid UTF-8 => ERR_INVALID_UTF8 -// - Always write required length to *outLenPtr (little-endian) -// - If required > outCap => ERR_BUF_TOO_SMALL -// - Else copy into caller buffer and return OK -// -//go:wasmexport strategy_token_value -func StrategyTokenValue(tokenPtr, tokenLen, metadataStrPtr, metadataStrLen, outPtr, outCap, outLenPtr uint32) int32 { - defer func() { - // Make sure panics don't leak as traps. - if r := recover(); r != nil { - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, 0) - } - } - } - }() - - if tokenLen == 0 { - // Mark required length as 0 and signal error. - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, 0) - } - } - return plugins.ERR_EMPTY_INPUT - } - - tokenBytes := bytesFromPtrLen(tokenPtr, tokenLen) - if !utf8.Valid(tokenBytes) { - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) - } - } - return plugins.ERR_INVALID_UTF8 - } - - if metadataStrLen > 0 { - metadataStrBytes := bytesFromPtrLen(metadataStrPtr, metadataStrLen) - if !utf8.Valid(metadataStrBytes) { - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(metadataStrPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) - } - } - return plugins.ERR_INVALID_UTF8 - } - } - - // --- Business logic (replace with your real token strategy) --- - - inStr := string(tokenBytes) - outStr := "TOKEN_VALUE:" + strings.ToUpper(inStr) - outBytes := []byte(outStr) - // -------------------------------------------------------------- - - required := uint32(len(outBytes)) - - // Always write required length. - if outLenPtr != 0 { - lenCell := bytesFromPtrLen(outLenPtr, 4) - if len(lenCell) != 4 { - return plugins.ERR_INTERNAL - } - binary.LittleEndian.PutUint32(lenCell, required) - } - - if required > outCap { - return plugins.ERR_BUF_TOO_SMALL - } - - if required == 0 { - return plugins.OK - } - - outSlice := bytesFromPtrLen(outPtr, outCap) - if uint32(len(outSlice)) < required { - return plugins.ERR_INTERNAL - } - - copy(outSlice, outBytes) - return plugins.OK -} - -// main is required for wasip1 -// scaffolds the `_instantiate` method -func main() {} - -// GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awssecrets.wasm diff --git a/plugins/grpc.go b/plugins/grpc.go index 9d7ffc5..5f9ebf0 100644 --- a/plugins/grpc.go +++ b/plugins/grpc.go @@ -9,7 +9,7 @@ import ( // GRPCClient is an implementation of KV that talks over RPC. type GRPCClient struct{ client proto.TokenStoreClient } -func (m *GRPCClient) Get(key string, metadata []byte) (string, error) { +func (m *GRPCClient) Value(key string, metadata []byte) (string, error) { resp, err := m.client.Value(context.Background(), &proto.TokenValueRequest{ Token: key, Metadata: metadata, @@ -27,9 +27,9 @@ type GRPCServer struct { Impl TokenStore } -func (m *GRPCServer) Get( +func (m *GRPCServer) Value( ctx context.Context, req *proto.TokenValueRequest) (*proto.TokenValueResponse, error) { - v, err := m.Impl.Get(req.) - return &proto.GetResponse{Value: v}, err + v, err := m.Impl.Value(req.Token, req.Metadata) + return &proto.TokenValueResponse{Value: v}, err } diff --git a/plugins/interface.go b/plugins/interface.go index ee33294..5a3cd1b 100644 --- a/plugins/interface.go +++ b/plugins/interface.go @@ -5,34 +5,26 @@ import ( "google.golang.org/grpc" + "github.com/DevLabFoundry/configmanager/v3/plugins/proto" "github.com/hashicorp/go-plugin" - "github.com/hashicorp/go-plugin/examples/grpc/proto" ) // Handshake is a common handshake that is shared by plugin and host. var Handshake = plugin.HandshakeConfig{ // This isn't required when using VersionedPlugins ProtocolVersion: 1, - MagicCookieKey: "BASIC_PLUGIN", + MagicCookieKey: "CONFIGMANAGER_PLUGIN", MagicCookieValue: "hello", } -// PluginMap is the map of plugins we can dispense. +// // PluginMap is the map of plugins we can dispense. var PluginMap = map[string]plugin.Plugin{ - "kv_grpc": &TokenStoreGRPCPlugin{}, - // "kv": &KVPlugin{}, + "configmanager_token_store": &TokenStoreGRPCPlugin{}, } // TokenStore is the interface that we're exposing as a plugin. type TokenStore interface { - Get(token string, metadata []byte) (string, error) -} - -// This is the implementation of plugin.Plugin so we can serve/consume this. -type TokenStorePlugin struct { - // Concrete implementation, written in Go. This is only used for plugins - // that are written in Go. - Impl TokenStore + Value(token string, metadata []byte) (string, error) } // This is the implementation of plugin.GRPCPlugin so we can serve/consume this. @@ -45,10 +37,10 @@ type TokenStoreGRPCPlugin struct { } func (p *TokenStoreGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { - proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl}) + proto.RegisterTokenStoreServer(s, &GRPCServer{Impl: p.Impl}) return nil } func (p *TokenStoreGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { - return &GRPCClient{client: proto(c)}, nil + return &GRPCClient{client: proto.NewTokenStoreClient(c)}, nil } diff --git a/plugins/scaffolding.go b/plugins/scaffolding.go index f4bdce0..f317496 100644 --- a/plugins/scaffolding.go +++ b/plugins/scaffolding.go @@ -22,9 +22,3 @@ var ( ErrEmptyResponse = errors.New("value retrieved but empty for token") ErrServiceCallFailed = errors.New("failed to complete the service call") ) - -type MessagExchange struct { - Token string `json:"token"` - Metadata map[string]any `json:"metadata,omitempty"` - Version string `json:"version"` -} From 6f0a3ba8df9170c594fb03694b2ba8923d53b3ce Mon Sep 17 00:00:00 2001 From: dnitsch Date: Mon, 8 Dec 2025 15:10:51 +0000 Subject: [PATCH 7/7] fix: add init as wip element --- .gitignore | 7 +- .trivyignore.yaml | 7 + Dockerfile | 6 + eirctl.yaml | 54 ++- generator/generator.go | 88 ++-- generator/generatorvars.go | 22 + go.mod | 87 ++-- go.sum | 252 +++-------- internal/store/plugin.go | 12 +- internal/store/plugin_test.go | 14 +- internal/store/store.go | 97 +++- internal/strategy/strategy.go | 250 ++++++----- internal/strategy/strategy_test.go | 200 ++++----- plugins/README.md | 240 ++++++++++ plugins/awsparamstr/README.md | 214 +-------- plugins/awssecrets/.gitkeep | 0 plugins/grpc.go | 3 +- plugins/vault/README.md | 3 + plugins/vault/impl/hashivault.go | 173 +++++++ plugins/vault/impl/hashivault_test.go | 624 ++++++++++++++++++++++++++ plugins/vault/main.go | 34 ++ 21 files changed, 1649 insertions(+), 738 deletions(-) create mode 100644 .trivyignore.yaml create mode 100644 plugins/README.md create mode 100644 plugins/awssecrets/.gitkeep create mode 100644 plugins/vault/README.md create mode 100644 plugins/vault/impl/hashivault.go create mode 100644 plugins/vault/impl/hashivault_test.go create mode 100644 plugins/vault/main.go diff --git a/.gitignore b/.gitignore index 81bfbd1..0c0758a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,18 @@ # Go vendor -bin dist .deps/ +# generated +bin +plugins/**/bin + # tests .coverage # local testers and local/ .bin +.configmanager +.trivy/fs-sbom-file.json diff --git a/.trivyignore.yaml b/.trivyignore.yaml new file mode 100644 index 0000000..31c34bb --- /dev/null +++ b/.trivyignore.yaml @@ -0,0 +1,7 @@ +vulnerabilities: + +secrets: + +misconfigurations: + +licenses: diff --git a/Dockerfile b/Dockerfile index 5871513..ba98147 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,10 @@ FROM docker.io/alpine:3 COPY --from=builder /app/bin/configmanager /usr/bin/configmanager +RUN chmod +x /usr/bin/configmanager + +RUN adduser -D -s /bin/sh -h /home/configmanager configmanager + +USER configmanager + ENTRYPOINT ["configmanager"] diff --git a/eirctl.yaml b/eirctl.yaml index 7c64937..22db6e4 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -4,7 +4,8 @@ output: prefixed debug: false import: - - https://raw.githubusercontent.com/Ensono/eirctl/refs/tags/0.9.3/shared/build/go/eirctl.yaml + - https://raw.githubusercontent.com/Ensono/eirctl/refs/tags/0.9.7/shared/build/go/eirctl.yaml + - https://raw.githubusercontent.com/Ensono/eirctl/refs/tags/0.9.7/shared/security/eirctl.yaml contexts: bash: @@ -51,6 +52,19 @@ pipelines: - task: proto:generate depends_on: proto:install + build:plugins: + - task: go:build:plugin + name: awsparamstr + env: + PLUGIN: awsparamstr + - task: go:build:plugin + name: vault + env: + PLUGIN: vault + + scan:plugins: + - task: trivy:file:system:sbom + tasks: show:coverage: description: Opens the current coverage viewer for the the configmanager utility. @@ -66,6 +80,44 @@ tasks: open http://localhost:6060/pkg/github.com/DevLabFoundry/configmanager/v2/?m=all godoc -notes "BUG|TODO" -play -http=:6060 + go:build:plugin: + context: go1xalpine + command: + - | + mkdir -p .deps + unset GOTOOLCHAIN + ldflags="-s -w -extldflags -static" + GOPATH=/eirctl/.deps GOOS=${BUILD_GOOS} GOARCH=${BUILD_GOARCH} CGO_ENABLED=0 go build -mod=readonly -buildvcs=false -ldflags="$ldflags" \ + -o ./plugins/$PLUGIN/bin/$PLUGIN-${BUILD_GOOS}-${BUILD_GOARCH}${BUILD_SUFFIX} ./plugins/$PLUGIN/main.go + echo "---" + echo "Built: $PLUGIN-${BUILD_GOOS}-${BUILD_GOARCH}${BUILD_SUFFIX}" + reset_context: true + variations: + - BUILD_GOOS: darwin + BUILD_GOARCH: amd64 + BUILD_SUFFIX: "" + - BUILD_GOOS: darwin + BUILD_GOARCH: arm64 + BUILD_SUFFIX: "" + - BUILD_GOOS: linux + BUILD_GOARCH: amd64 + BUILD_SUFFIX: "" + - BUILD_GOOS: linux + BUILD_GOARCH: arm64 + BUILD_SUFFIX: "" + - BUILD_GOOS: windows + BUILD_GOARCH: amd64 + BUILD_SUFFIX: ".exe" + - BUILD_GOOS: windows + BUILD_GOARCH: arm64 + BUILD_SUFFIX: ".exe" + - BUILD_GOOS: windows + BUILD_GOARCH: "386" + BUILD_SUFFIX: ".exe" + required: + env: + - PLUGIN + go:build:binary: context: go1x description: | diff --git a/generator/generator.go b/generator/generator.go index 8dcef19..ca3243f 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -14,7 +14,7 @@ import ( "github.com/DevLabFoundry/configmanager/v3/internal/lexer" "github.com/DevLabFoundry/configmanager/v3/internal/log" "github.com/DevLabFoundry/configmanager/v3/internal/parser" - "github.com/DevLabFoundry/configmanager/v3/internal/strategy" + "github.com/DevLabFoundry/configmanager/v3/internal/store" ) // Generator is the main struct holding the @@ -24,10 +24,11 @@ import ( // which wil be passed in a loop into a goroutine to perform the // relevant strategy network calls to the config store implementations type Generator struct { - Logger log.ILogger - strategy strategy.StrategyFuncMap - ctx context.Context - config config.GenVarsConfig + Logger log.ILogger + // strategy strategy.StrategyFuncMap + store *store.Store + ctx context.Context + config config.GenVarsConfig } type Opts func(*Generator) @@ -48,7 +49,7 @@ func new(ctx context.Context, opts ...Opts) *Generator { // return using default config config: *conf, } - g.strategy = nil + // g.strategy = nil // now apply additional opts for _, o := range opts { @@ -58,13 +59,13 @@ func new(ctx context.Context, opts ...Opts) *Generator { return g } -// WithStrategyMap -// -// Adds addtional funcs for storageRetrieval used for testing only -func (c *Generator) WithStrategyMap(sm strategy.StrategyFuncMap) *Generator { - c.strategy = sm - return c -} +// // WithStrategyMap +// // +// // Adds addtional funcs for storageRetrieval used for testing only +// func (c *Generator) WithStrategyMap(sm strategy.StrategyFuncMap) *Generator { +// c.strategy = sm +// return c +// } // WithConfig uses custom config func (c *Generator) WithConfig(cfg *config.GenVarsConfig) *Generator { @@ -97,6 +98,14 @@ func (c *Generator) Generate(tokens []string) (ReplacedToken, error) { return nil, err } + // initialise pugins here based on discovered tokens + // + s, err := store.Init(c.ctx, ntm.TokenSet()) + if err != nil { + return nil, err + } + + c.store = s // pass in default initialised retrieveStrategy // input should be rt, err := c.generate(ntm) @@ -145,14 +154,15 @@ func IsParsed(v any, trm ReplacedToken) bool { // It then denormalizes the NormalizedTokenSafe back to a ReplacedToken map // which stores the values for each token to be returned to the caller func (c *Generator) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { - if len(ntm.normalizedTokenMap) < 1 { + if len(ntm.m) < 1 { c.Logger.Debug("no replaceable tokens found in input") return nil, nil } wg := &sync.WaitGroup{} - s := strategy.New(c.config, c.Logger, strategy.WithStrategyFuncMap(c.strategy)) + // initialise the stores here + // s := strategy.New(c.config, c.Logger, strategy.WithStrategyFuncMap(c.strategy)) // safe read of normalized token map // this will ensure that we are minimizing @@ -164,13 +174,20 @@ func (c *Generator) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { } token := prsdTkn.parsedTokens[0] wg.Go(func() { - prsdTkn.resp = &strategy.TokenResponse{} - storeStrategy, err := s.GetImplementation(c.ctx, token) + prsdTkn.resp = &TokenResponse{} + prsdTkn.resp.WithKey(token) + storeStrategy, err := c.store.GetImplementation(token.Prefix()) + if err != nil { + prsdTkn.resp.Err = err + return + } + // storeStrategy.GetValue(token) + v, err := storeStrategy.GetValue(token) if err != nil { prsdTkn.resp.Err = err return } - prsdTkn.resp = strategy.ExchangeToken(storeStrategy, token) + prsdTkn.resp.WithValue(v) }) } @@ -200,7 +217,9 @@ func (c *Generator) generate(ntm NormalizedTokenSafe) (ReplacedToken, error) { // The idea is to minimize the number of networks calls to the underlying `store` Implementations // // The merging is based on the implemenentation and sanitized token being the same, -// if the token contains metadata then it must be +// if the token contains metadata then it must be stored uniquely even if the underlying store is the same. +// This is because a token with metadata must be called uniquely +// as it may contain different versions of the same token - hence the value would be different // // # Merging strategy // @@ -209,7 +228,7 @@ type NormalizedToken struct { // all the tokens that can be used to do a replacement parsedTokens []*config.ParsedTokenConfig // will be assigned post generate - resp *strategy.TokenResponse + resp *TokenResponse // // configToken is the last assigned full config in the loop if multip // configToken *config.ParsedTokenConfig } @@ -222,36 +241,49 @@ func (n *NormalizedToken) WithParsedToken(v *config.ParsedTokenConfig) *Normaliz // NormalizedTokenSafe is the map of lowest common denominators // by token.Keypathless or token.String (full token) if metadata is included type NormalizedTokenSafe struct { - mu *sync.Mutex - normalizedTokenMap map[string]*NormalizedToken + mu *sync.Mutex + m map[string]*NormalizedToken + set map[string]struct{} } func (n NormalizedTokenSafe) GetMap() map[string]*NormalizedToken { n.mu.Lock() defer n.mu.Unlock() - return n.normalizedTokenMap + return n.m +} + +func (n NormalizedTokenSafe) TokenSet() []string { + n.mu.Lock() + defer n.mu.Unlock() + ss := []string{} + for key, _ := range n.set { + ss = append(ss, strings.ToLower(key)) + } + return ss } func (c *Generator) NormalizeRawToken(rtm *RawTokenConfig) NormalizedTokenSafe { - ntm := NormalizedTokenSafe{mu: &sync.Mutex{}, normalizedTokenMap: make(map[string]*NormalizedToken)} + ntm := NormalizedTokenSafe{mu: &sync.Mutex{}, m: make(map[string]*NormalizedToken), set: make(map[string]struct{})} for _, r := range rtm.RawTokenMap() { // if a string contains we need to store it uniquely // future improvements might group all the metadata values together if len(r.Metadata()) > 0 { - if n, found := ntm.normalizedTokenMap[r.String()]; found { + if n, found := ntm.m[r.String()]; found { n.WithParsedToken(r) continue } - ntm.normalizedTokenMap[r.String()] = (&NormalizedToken{}).WithParsedToken(r) + ntm.m[r.String()] = (&NormalizedToken{}).WithParsedToken(r) + ntm.set[string(r.Prefix())] = struct{}{} continue } - if n, found := ntm.normalizedTokenMap[r.Keypathless()]; found { + if n, found := ntm.m[r.Keypathless()]; found { n.WithParsedToken(r) continue } - ntm.normalizedTokenMap[r.Keypathless()] = (&NormalizedToken{}).WithParsedToken(r) + ntm.m[r.Keypathless()] = (&NormalizedToken{}).WithParsedToken(r) + ntm.set[string(r.Prefix())] = struct{}{} continue } return ntm diff --git a/generator/generatorvars.go b/generator/generatorvars.go index d2ac986..0e1bf8d 100644 --- a/generator/generatorvars.go +++ b/generator/generatorvars.go @@ -43,6 +43,28 @@ func (rtm *RawTokenConfig) RawTokenMap() map[string]*config.ParsedTokenConfig { return rtm.tokenMap } +type TokenResponse struct { + val string + key *config.ParsedTokenConfig + Err error +} + +func (tr *TokenResponse) WithKey(key *config.ParsedTokenConfig) { + tr.key = key +} + +func (tr *TokenResponse) WithValue(val string) { + tr.val = val +} + +func (tr *TokenResponse) Key() *config.ParsedTokenConfig { + return tr.key +} + +func (tr *TokenResponse) Value() string { + return tr.val +} + // keySeparatorLookup checks if the key contains // keySeparator character // If it does contain one then it tries to parse diff --git a/go.mod b/go.mod index 4049a5c..2c30ca2 100644 --- a/go.mod +++ b/go.mod @@ -1,70 +1,54 @@ module github.com/DevLabFoundry/configmanager/v3 -go 1.25.4 +go 1.25.5 require ( - cloud.google.com/go/secretmanager v1.11.4 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 - github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 - github.com/aws/aws-sdk-go-v2 v1.40.0 - github.com/aws/aws-sdk-go-v2/config v1.32.0 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 - github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 + github.com/aws/aws-sdk-go-v2 v1.40.1 + github.com/aws/aws-sdk-go-v2/config v1.32.3 + github.com/aws/aws-sdk-go-v2/service/ssm v1.67.5 github.com/go-test/deep v1.1.1 - github.com/googleapis/gax-go/v2 v2.12.0 github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/api/auth/aws v0.11.0 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spyzhov/ajson v0.9.6 + google.golang.org/grpc v1.77.0 + google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/oklog/run v1.1.0 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( - cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.1.5 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/a8m/envsubst v1.4.3 github.com/aws/aws-sdk-go v1.55.8 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect - github.com/aws/smithy-go v1.23.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.2 + github.com/hashicorp/go-plugin v1.7.0 github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect @@ -75,33 +59,16 @@ require ( github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // 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-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/tetratelabs/wazero v1.10.1 - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.155.0 // indirect - google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/grpc v1.61.2 - google.golang.org/protobuf v1.31.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect ) diff --git a/go.sum b/go.sum index 3008268..fa846d7 100644 --- a/go.sum +++ b/go.sum @@ -1,98 +1,52 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= -cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= -cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0 h1:uU4FujKFQAz31AbWOO3INV9qfIanHeIUSsGhRlcJJmg= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.2.0/go.mod h1:qr3M3Oy6V98VR0c5tCHKUpaeJTRQh6KYzJewRtFWqfc= -github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 h1:j0hhYS006eJ54vusoap0f2NVZ1YY3QnaAEnLM68f0SQ= -github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1/go.mod h1:AdtInaXmK8eYmbjezRWgLz+Qs46nc9Up9GWGwteWNfw= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= -github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA= -github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M= -github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= +github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= +github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU= +github.com/aws/aws-sdk-go-v2/config v1.32.3/go.mod h1:srtPKaJJe3McW6T/+GMBZyIPc+SeqJsNPJsd4mOYZ6s= +github.com/aws/aws-sdk-go-v2/credentials v1.19.3 h1:01Ym72hK43hjwDeJUfi1l2oYLXBAOR8gNSZNmXmvuas= +github.com/aws/aws-sdk-go-v2/credentials v1.19.3/go.mod h1:55nWF/Sr9Zvls0bGnWkRxUdhzKqj9uRNlPvgV1vgxKc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 h1:utxLraaifrSBkeyII9mIbVwXXWrZdlPO7FIKmyLCEcY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15/go.mod h1:hW6zjYUDQwfz3icf4g2O41PHi77u10oAzJ84iSzR/lo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3 h1:ofiQvKwka2E3T8FXBsU1iWj7Yvk2wd1p4ZCdS6qGiKQ= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.3/go.mod h1:+nlWvcgDPQ56mChEBzTC0puAMck+4onOFaHg5cE+Lgg= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 h1:d/6xOGIllc/XW1lzG9a4AUBMmpLA9PXcQnVPTuHHcik= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.3/go.mod h1:fQ7E7Qj9GiW8y0ClD7cUJk3Bz5Iw8wZkWDHsTe8vDKs= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.5 h1:YKGgwB1rye0JpV10Bfma3cZdQzX61j2HPWQw+YxWvrQ= +github.com/aws/aws-sdk-go-v2/service/ssm v1.67.5/go.mod h1:eBDSa0vuYB0lalpNxavIw80Q4Ksy08bhHHbT0aWa4tE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 h1:8sTTiw+9yuNXcfWeqKF2x01GqCF49CpP4Z9nKrrk/ts= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.6/go.mod h1:8WYg+Y40Sn3X2hioaaWAAIngndR8n1XFdRPPX+7QBaM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 h1:E+KqWoVsSrj1tJ6I/fjDIu5xoS2Zacuu1zT+H7KtiIk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11/go.mod h1:qyWHz+4lvkXcr3+PoGlGHEI+3DLLiU6/GdrFfMaAhB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 h1:tzMkjh0yTChUqJDgGkcDdxvZDSrJ/WB6R6ymI5ehqJI= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.3/go.mod h1:T270C0R5sZNLbWUe8ueiAF42XSZxxPocTaGSgs5c/60= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -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/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= -github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= -github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -100,43 +54,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= 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= @@ -147,8 +70,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -173,16 +96,15 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 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/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= -github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -190,8 +112,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -204,23 +124,22 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -233,20 +152,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= -github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -257,89 +167,33 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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/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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -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/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= -google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= -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/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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= -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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.61.2 h1:TzJay21lXCf7BiNFKl7mSskt5DlkKAumAYTs52SpJeo= -google.golang.org/grpc v1.61.2/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= -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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -351,5 +205,3 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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/internal/store/plugin.go b/internal/store/plugin.go index ddfd739..ee6fe83 100644 --- a/internal/store/plugin.go +++ b/internal/store/plugin.go @@ -9,11 +9,8 @@ import ( "github.com/hashicorp/go-plugin" ) -// Plugin is responsible for managing plugins within configmanager -// -// It includes the following methods -// - fetch plugins from known sources -// - maintains a list of tokens answerable by a specified pluginEngine +// Plugin is responsible for managing the plugin lifecycle +// within the configmanager flow. Each Implementation will initialise exactly one instance of the plugin type Plugin struct { Implementations config.ImplementationPrefix SourcePath string @@ -22,8 +19,8 @@ type Plugin struct { tokenStore plugins.TokenStore } -// New Plugin gets called once per implementation -func New(ctx context.Context, path string, prefix config.ImplementationPrefix) (*Plugin, error) { +// NewPlugin Plugin gets called once per implementation +func NewPlugin(ctx context.Context, path string) (*Plugin, error) { // We're a host. Start by launching the plugin process. client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: plugins.Handshake, @@ -31,7 +28,6 @@ func New(ctx context.Context, path string, prefix config.ImplementationPrefix) ( Cmd: exec.Command(path), AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, }) - // Connect via RPC rpcClient, err := client.Client() if err != nil { diff --git a/internal/store/plugin_test.go b/internal/store/plugin_test.go index f646841..47a840a 100644 --- a/internal/store/plugin_test.go +++ b/internal/store/plugin_test.go @@ -2,33 +2,41 @@ package store_test import ( "context" + "fmt" "os" + "runtime" "testing" "github.com/DevLabFoundry/configmanager/v3/internal/config" "github.com/DevLabFoundry/configmanager/v3/internal/store" ) +// TODO: make the implementation of the plugin system more testable func TestPlugin_GetValue_integration(t *testing.T) { + t.Skip() // as the plugin is technically a subprocess // setting env vars at this level will affect the loaded plugin os.Setenv("AWS_REGION", "eu-west-1") - os.Setenv("AWS_PROFILE", "PROFILE_TO_USE") - np, err := store.New(context.TODO(), "../../plugins/awsparamstr/bin/awsparamstr", config.ParamStorePrefix) + os.Setenv("AWS_PROFILE", "FOO") + tp := fmt.Sprintf("../../.configmanager/plugins/awsparamstr/awsparamstr-%s-%s", runtime.GOOS, runtime.GOARCH) + np, err := store.NewPlugin(context.TODO(), tp) if err != nil { t.Fatal(err) } + defer np.ClientCleanUp() token, err := config.NewToken(config.ParamStorePrefix, *config.NewConfig()) if err != nil { t.Fatal(err) } + token.WithSanitizedToken("/int-test/pocketbase/admin-pwd") got, err := np.GetValue(token) if err != nil { t.Fatal(err) } - if got == "" { + + if len(got) < 1 { t.Error("empty...") } } diff --git a/internal/store/store.go b/internal/store/store.go index a4fdbc0..79ae798 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1,18 +1,24 @@ package store import ( + "context" "errors" + "fmt" + "os" + "path" + "runtime" + "strings" + "sync" "github.com/DevLabFoundry/configmanager/v3/internal/config" ) -const implementationNetworkErr string = "implementation %s error: %v for token: %s" - var ( ErrRetrieveFailed = errors.New("failed to retrieve config item") ErrClientInitialization = errors.New("failed to initialize the client") ErrEmptyResponse = errors.New("value retrieved but empty for token") ErrServiceCallFailed = errors.New("failed to complete the service call") + ErrPluginNotFound = errors.New("plugin does not exist") ) // Strategy iface that all store implementations @@ -25,3 +31,90 @@ type Strategy interface { // SetToken SetToken(s *config.ParsedTokenConfig) } + +// +// It includes the following methods +// - fetch plugins from known sources +// - maintains a list of tokens answerable by a specified pluginEngine + +type pluginMap struct { + mu *sync.Mutex + // m holds the map of plugins where the key is the lowercased implementation prefix + // e.g. `AWSPARAMSTR://` => `awsparamstr` + m map[string]*Plugin +} + +func (p pluginMap) Add(key string, pl *Plugin) { + p.mu.Lock() + defer p.mu.Unlock() + p.m[key] = pl +} + +const ( + loc string = ".configmanager/plugins" + namePattern string = "%s-%s-%s" +) + +type Store struct { + pluginLocation []string + plugin pluginMap + // PluginCleanUp func() +} + +func Init(ctx context.Context, implt []string) (*Store, error) { + pm := pluginMap{mu: &sync.Mutex{}, m: make(map[string]*Plugin)} + + // l := []string{""} + // + for _, plugin := range implt { + plpath, err := findPlugin(plugin) + if err != nil { + return nil, err + } + p, err := NewPlugin(ctx, plpath) + pm.Add(plugin, p) + } + return &Store{plugin: pm}, nil +} + +func (s *Store) GetImplementation(implemenation config.ImplementationPrefix) (plugin *Plugin, err error) { + var exists bool + if plugin, exists = s.plugin.m[strings.ToLower(string(implemenation))]; exists { + return plugin, nil + } + return nil, ErrPluginNotFound +} + +// PluginCleanUp ensures the plugins are properly shut down +func (s *Store) PluginCleanUp() { + s.plugin.mu.Lock() + defer s.plugin.mu.Unlock() + for _, plugin := range s.plugin.m { + plugin.ClientCleanUp() + } +} + +// findPlugin ensures the path exists and search the following locations +// +// current dir +// home dir +func findPlugin(plugin string) (string, error) { + // fallback locations + // current dir + cwd, err := os.Getwd() + if err != nil { + return "", err + } + hd, err := os.UserHomeDir() + if err != nil { + return "", err + } + for _, p := range []string{cwd, hd} { + ff := path.Join(p, loc, plugin, fmt.Sprintf(namePattern, plugin, runtime.GOOS, runtime.GOARCH)) + if _, err := os.Stat(ff); err == nil { + // break on first non nil error + return ff, nil + } + } + return "", ErrPluginNotFound +} diff --git a/internal/strategy/strategy.go b/internal/strategy/strategy.go index 7aa8345..9548f8b 100644 --- a/internal/strategy/strategy.go +++ b/internal/strategy/strategy.go @@ -1,124 +1,132 @@ // Package strategy is a factory method wrapper around the backing store implementations package strategy -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/DevLabFoundry/configmanager/v3/internal/config" - "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" -) - -var ErrTokenInvalid = errors.New("invalid token - cannot get prefix") - -// StrategyFunc -type StrategyFunc func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) - -// StrategyFuncMap -type StrategyFuncMap map[config.ImplementationPrefix]StrategyFunc - -type Strategy struct { - config config.GenVarsConfig - strategyFuncMap strategyFnMap -} - -type Opts func(*Strategy) - -// New -func New(config config.GenVarsConfig, logger log.ILogger, opts ...Opts) *Strategy { - rs := &Strategy{ - config: config, - strategyFuncMap: strategyFnMap{mu: sync.Mutex{}, funcMap: defaultStrategyFuncMap(logger)}, - } - // overwrite or add any options/defaults set above - for _, o := range opts { - o(rs) - } - - return rs -} - -// WithStrategyFuncMap Adds custom implementations for prefix -// -// Mainly used for testing -// NOTE: this may lead to eventual optional configurations by users -func WithStrategyFuncMap(funcMap StrategyFuncMap) Opts { - return func(rs *Strategy) { - rs.strategyFuncMap.mu.Lock() - defer rs.strategyFuncMap.mu.Unlock() - for prefix, implementation := range funcMap { - rs.strategyFuncMap.funcMap[config.ImplementationPrefix(prefix)] = implementation - } - } -} - -// GetImplementation is a factory method returning the concrete implementation for the retrieval of the token value -// i.e. facilitating the exchange of the supplied token for the underlying value -func (rs *Strategy) GetImplementation(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - if token == nil { - return nil, fmt.Errorf("unable to get prefix, %w", ErrTokenInvalid) - } - - if store, found := rs.strategyFuncMap.funcMap[token.Prefix()]; found { - return store(ctx, token) - } - - return nil, fmt.Errorf("implementation not found for input string: %s", token) -} - -func ExchangeToken(s store.Strategy, token *config.ParsedTokenConfig) *TokenResponse { - cr := &TokenResponse{} - cr.Err = nil - cr.key = token - s.SetToken(token) - cr.value, cr.Err = s.Value() - return cr -} - -type TokenResponse struct { - value string - key *config.ParsedTokenConfig - Err error -} - -func (tr *TokenResponse) Key() *config.ParsedTokenConfig { - return tr.key -} - -func (tr *TokenResponse) Value() string { - return tr.value -} - -func defaultStrategyFuncMap(logger log.ILogger) StrategyFuncMap { - return map[config.ImplementationPrefix]StrategyFunc{ - // config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewAzTableStore(ctx, token, logger) - // }, - // config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewAzAppConf(ctx, token, logger) - // }, - // config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewGcpSecrets(ctx, logger) - // }, - // config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewSecretsMgr(ctx, logger) - // }, - // config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewParamStore(ctx, logger) - // }, - // config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewKvScrtStore(ctx, token, logger) - // }, - // config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - // return store.NewVaultStore(ctx, token, logger) - // }, - } -} - -type strategyFnMap struct { - mu sync.Mutex - funcMap StrategyFuncMap -} +// import ( +// "context" +// "errors" +// "fmt" +// "sync" + +// "github.com/DevLabFoundry/configmanager/v3/internal/config" +// "github.com/DevLabFoundry/configmanager/v3/internal/log" +// "github.com/DevLabFoundry/configmanager/v3/internal/store" +// ) + +// var ErrTokenInvalid = errors.New("invalid token - cannot get prefix") + +// // StrategyFunc +// type StrategyFunc func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) + +// // StrategyFuncMap +// type StrategyFuncMap map[config.ImplementationPrefix]StrategyFunc + +// type Strategy struct { +// config config.GenVarsConfig +// strategyFuncMap strategyFnMap +// } + +// type Opts func(*Strategy) + +// // New +// func New(config config.GenVarsConfig, logger log.ILogger, opts ...Opts) *Strategy { +// rs := &Strategy{ +// config: config, +// strategyFuncMap: strategyFnMap{mu: sync.Mutex{}, funcMap: defaultStrategyFuncMap(logger)}, +// } +// // overwrite or add any options/defaults set above +// for _, o := range opts { +// o(rs) +// } + +// return rs +// } + +// // WithStrategyFuncMap Adds custom implementations for prefix +// // +// // Mainly used for testing +// // NOTE: this may lead to eventual optional configurations by users +// func WithStrategyFuncMap(funcMap StrategyFuncMap) Opts { +// return func(rs *Strategy) { +// rs.strategyFuncMap.mu.Lock() +// defer rs.strategyFuncMap.mu.Unlock() +// for prefix, implementation := range funcMap { +// rs.strategyFuncMap.funcMap[config.ImplementationPrefix(prefix)] = implementation +// } +// } +// } + +// // GetImplementation is a factory method returning the concrete implementation for the retrieval of the token value +// // i.e. facilitating the exchange of the supplied token for the underlying value +// func (rs *Strategy) GetImplementation(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// if token == nil { +// return nil, fmt.Errorf("unable to get prefix, %w", ErrTokenInvalid) +// } + +// if store, found := rs.strategyFuncMap.funcMap[token.Prefix()]; found { +// return store(ctx, token) +// } + +// return nil, fmt.Errorf("implementation not found for input string: %s", token) +// } + +// func ExchangeToken(s store.Strategy, token *config.ParsedTokenConfig) *TokenResponse { +// cr := &TokenResponse{} +// cr.Err = nil +// cr.key = token +// s.SetToken(token) +// cr.val, cr.Err = s.Value() +// return cr +// } + +// type TokenResponse struct { +// val string +// key *config.ParsedTokenConfig +// Err error +// } + +// func (tr *TokenResponse) WithKey(key *config.ParsedTokenConfig) { +// tr.key = key +// } + +// func (tr *TokenResponse) WithValue(val string) { +// tr.val = val +// } + +// func (tr *TokenResponse) Key() *config.ParsedTokenConfig { +// return tr.key +// } + +// func (tr *TokenResponse) Value() string { +// return tr.val +// } + +// func defaultStrategyFuncMap(logger log.ILogger) StrategyFuncMap { +// return map[config.ImplementationPrefix]StrategyFunc{ +// // config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewAzTableStore(ctx, token, logger) +// // }, +// // config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewAzAppConf(ctx, token, logger) +// // }, +// // config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewGcpSecrets(ctx, logger) +// // }, +// // config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewSecretsMgr(ctx, logger) +// // }, +// // config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewParamStore(ctx, logger) +// // }, +// // config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewKvScrtStore(ctx, token, logger) +// // }, +// // config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// // return store.NewVaultStore(ctx, token, logger) +// // }, +// } +// } + +// type strategyFnMap struct { +// mu sync.Mutex +// funcMap StrategyFuncMap +// } diff --git a/internal/strategy/strategy_test.go b/internal/strategy/strategy_test.go index c5fe3f7..b567e52 100644 --- a/internal/strategy/strategy_test.go +++ b/internal/strategy/strategy_test.go @@ -1,117 +1,117 @@ package strategy_test -import ( - "context" - "io" - "testing" +// import ( +// "context" +// "io" +// "testing" - "github.com/DevLabFoundry/configmanager/v3/internal/config" - log "github.com/DevLabFoundry/configmanager/v3/internal/log" - "github.com/DevLabFoundry/configmanager/v3/internal/store" - "github.com/DevLabFoundry/configmanager/v3/internal/strategy" - "github.com/DevLabFoundry/configmanager/v3/internal/testutils" -) +// "github.com/DevLabFoundry/configmanager/v3/internal/config" +// log "github.com/DevLabFoundry/configmanager/v3/internal/log" +// "github.com/DevLabFoundry/configmanager/v3/internal/store" +// "github.com/DevLabFoundry/configmanager/v3/internal/strategy" +// "github.com/DevLabFoundry/configmanager/v3/internal/testutils" +// ) -type mockGenerate struct { - inToken, value string - err error -} +// type mockGenerate struct { +// inToken, value string +// err error +// } -func (m mockGenerate) SetToken(s *config.ParsedTokenConfig) { -} +// func (m mockGenerate) SetToken(s *config.ParsedTokenConfig) { +// } -func (m mockGenerate) Value() (s string, e error) { - return m.value, m.err -} +// func (m mockGenerate) Value() (s string, e error) { +// return m.value, m.err +// } -var TEST_GCP_CREDS = []byte(`{ - "type": "service_account", - "project_id": "xxxxx", - "private_key_id": "yyyyyyyyyyyy", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDf842hcn5Nvp6e\n7yKARaCVIDfLXpKDhRwUOvHMzJ1ioRgQo/kbv1n4yHGCSUFyY6hKGj0HBjaGj5kE\n79H/6Y3dJNGhnsMnxBhHdo+3FI8QF0CHZh460NMZSAJ41UMQSBGssGVsNfyUzXGH\nLc45sIx/Twx3yr1k2GD3E8FlDcKlZqa3xGHf+aipg2X3NxbYi+Sz7Yed+SOMhNHl\ncX6E/TqG9n1aTyIwjMIHscCYarJqURkJxr24ukDroCeMxAfxYTdMvRU2e8pFEdoY\nrgUC88fYfaVI5txJ6j/ZKauKQX9Pa8tSyXJeGva3JYp4VC7V4IyoVviCUgEGWZDN\n6/i3zoF/AgMBAAECggEAcVBCcVYFIkE48SH+Svjv74SFtpj7eSB4vKO2hPFjEOyB\nyKmu+aMwWvjQtiNqwf46wIPWLR+vpxYxTpYpo1sBNMvUZfp2tEA8KKyMuw3j9ThO\npjO9R/UxWrFcztbZP/u3NbFrH/2Q95mbv9IlbnsuG5xbqqEig0wYg+uzBvaXbig3\n/Jr0vLT2BkRCBKQkYGjVZcHlHVLoF7/J8cghFgkV1PGvknOv6/q7qzn9L4TjQIet\nfhrhN8Z1vgFiSYtpjP6YQEUEPSHmCQeD3WzJcnASPpU2uCUwd/z65ltKPnn+rqMt\n6jt9R1S1Ju2ZSjv+kR5fIXzihdOzncyzDDm33c/QwQKBgQD2QDZuzLjTxnhsfGii\nKJDAts+Jqfs/6SeEJcJKtEngj4m7rgzyEjbKVp8qtRHIzglKRWAe62/qzzy2BkKi\nvAd4+ZzmG2SkgypGsKVfjGXVFixz2gtUdmBOmK/TnYsxNT9yTt+rX9IGqKK60q73\nOWl8VsliLIsfvSH7+bqi7sRcXQKBgQDo0VUebyQHoTAXPdzGy2ysrVPDiHcldH0Y\n/hvhQTZwxYaJr3HpOCGol2Xl6zyawuudEQsoQwJ3Li6yeb0YMGiWX77/t+qX3pSn\nkGuoftGaNDV7sLn9UV2y+InF8EL1CasrhG1k5RIuxyfV0w+QUo+E7LpVR5XkbJqT\n9QNKnDQXiwKBgQDvvEYCCqbp7e/xVhEbxbhfFdro4Cat6tRAz+3egrTlvXhO0jzi\nMp9Kz5f3oP5ma0gaGX5hu75icE1fvKqE+d+ghAqe7w5FJzkyRulJI0tEb2jphN7A\n5NoPypBqyZboWjmhlG4mzouPVf/POCuEnk028truDAWJ6by7Lj3oP+HFNQKBgQCc\n5BQ8QiFBkvnZb7LLtGIzq0n7RockEnAK25LmJRAOxs13E2fsBguIlR3x5qgckqY8\nXjPqmd2bet+1HhyzpEuWqkcIBGRum2wJz2T9UxjklbJE/D8Z2i8OYDZX0SUOA8n5\ntXASwduS8lqB2Y1vcHOO3AhlV6xHFnjEpCPnr4PbKQKBgAhQ9D9MPeuz+5yw3yHg\nkvULZRtud+uuaKrOayprN25RTxr9c0erxqnvM7KHeo6/urOXeEa7x2n21kAT0Nch\nkF2RtWBLZKXGZEVBtw1Fw0UKNh4IDgM26dwlzRfTVHCiw6M6dCiTNk9KkP2vlkim\n3QFDSSUp+eBTXA17WkDAQf7w\n-----END PRIVATE KEY-----\n", - "client_email": "foo@project.iam.gserviceaccount.com", - "client_id": "99999911111111", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bla" - }`) +// var TEST_GCP_CREDS = []byte(`{ +// "type": "service_account", +// "project_id": "xxxxx", +// "private_key_id": "yyyyyyyyyyyy", +// "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDf842hcn5Nvp6e\n7yKARaCVIDfLXpKDhRwUOvHMzJ1ioRgQo/kbv1n4yHGCSUFyY6hKGj0HBjaGj5kE\n79H/6Y3dJNGhnsMnxBhHdo+3FI8QF0CHZh460NMZSAJ41UMQSBGssGVsNfyUzXGH\nLc45sIx/Twx3yr1k2GD3E8FlDcKlZqa3xGHf+aipg2X3NxbYi+Sz7Yed+SOMhNHl\ncX6E/TqG9n1aTyIwjMIHscCYarJqURkJxr24ukDroCeMxAfxYTdMvRU2e8pFEdoY\nrgUC88fYfaVI5txJ6j/ZKauKQX9Pa8tSyXJeGva3JYp4VC7V4IyoVviCUgEGWZDN\n6/i3zoF/AgMBAAECggEAcVBCcVYFIkE48SH+Svjv74SFtpj7eSB4vKO2hPFjEOyB\nyKmu+aMwWvjQtiNqwf46wIPWLR+vpxYxTpYpo1sBNMvUZfp2tEA8KKyMuw3j9ThO\npjO9R/UxWrFcztbZP/u3NbFrH/2Q95mbv9IlbnsuG5xbqqEig0wYg+uzBvaXbig3\n/Jr0vLT2BkRCBKQkYGjVZcHlHVLoF7/J8cghFgkV1PGvknOv6/q7qzn9L4TjQIet\nfhrhN8Z1vgFiSYtpjP6YQEUEPSHmCQeD3WzJcnASPpU2uCUwd/z65ltKPnn+rqMt\n6jt9R1S1Ju2ZSjv+kR5fIXzihdOzncyzDDm33c/QwQKBgQD2QDZuzLjTxnhsfGii\nKJDAts+Jqfs/6SeEJcJKtEngj4m7rgzyEjbKVp8qtRHIzglKRWAe62/qzzy2BkKi\nvAd4+ZzmG2SkgypGsKVfjGXVFixz2gtUdmBOmK/TnYsxNT9yTt+rX9IGqKK60q73\nOWl8VsliLIsfvSH7+bqi7sRcXQKBgQDo0VUebyQHoTAXPdzGy2ysrVPDiHcldH0Y\n/hvhQTZwxYaJr3HpOCGol2Xl6zyawuudEQsoQwJ3Li6yeb0YMGiWX77/t+qX3pSn\nkGuoftGaNDV7sLn9UV2y+InF8EL1CasrhG1k5RIuxyfV0w+QUo+E7LpVR5XkbJqT\n9QNKnDQXiwKBgQDvvEYCCqbp7e/xVhEbxbhfFdro4Cat6tRAz+3egrTlvXhO0jzi\nMp9Kz5f3oP5ma0gaGX5hu75icE1fvKqE+d+ghAqe7w5FJzkyRulJI0tEb2jphN7A\n5NoPypBqyZboWjmhlG4mzouPVf/POCuEnk028truDAWJ6by7Lj3oP+HFNQKBgQCc\n5BQ8QiFBkvnZb7LLtGIzq0n7RockEnAK25LmJRAOxs13E2fsBguIlR3x5qgckqY8\nXjPqmd2bet+1HhyzpEuWqkcIBGRum2wJz2T9UxjklbJE/D8Z2i8OYDZX0SUOA8n5\ntXASwduS8lqB2Y1vcHOO3AhlV6xHFnjEpCPnr4PbKQKBgAhQ9D9MPeuz+5yw3yHg\nkvULZRtud+uuaKrOayprN25RTxr9c0erxqnvM7KHeo6/urOXeEa7x2n21kAT0Nch\nkF2RtWBLZKXGZEVBtw1Fw0UKNh4IDgM26dwlzRfTVHCiw6M6dCiTNk9KkP2vlkim\n3QFDSSUp+eBTXA17WkDAQf7w\n-----END PRIVATE KEY-----\n", +// "client_email": "foo@project.iam.gserviceaccount.com", +// "client_id": "99999911111111", +// "auth_uri": "https://accounts.google.com/o/oauth2/auth", +// "token_uri": "https://oauth2.googleapis.com/token", +// "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", +// "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bla" +// }`) -func Test_Strategy_Retrieve_succeeds(t *testing.T) { - ttests := map[string]struct { - impl func(t *testing.T) store.Strategy - config *config.GenVarsConfig - token string - expect string - impPrefix config.ImplementationPrefix - }{ - "with mocked implementation AZTABLESTORAGE": { - func(t *testing.T) store.Strategy { - return &mockGenerate{"mountPath/token", "bar", nil} - }, - config.NewConfig().WithOutputPath("stdout"), - "mountPath/token", - "bar", - config.AzTableStorePrefix, - }, - // "error in retrieval": { - // func(t *testing.T) store.Strategy { - // return &mockGenerate{"SOME://mountPath/token", "bar", fmt.Errorf("unable to perform getTokenValue")} - // }, - // config.NewConfig().WithOutputPath("stdout").WithTokenSeparator("://"), - // []string{"SOME://token"}, - // config.AzAppConfigPrefix, - // "unable to perform getTokenValue", - // }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - token, _ := config.NewToken(tt.impPrefix, *tt.config) - token.WithSanitizedToken(tt.token) - got := strategy.ExchangeToken(tt.impl(t), token) - if got.Err != nil { - t.Errorf(testutils.TestPhraseWithContext, "Token response errored", got.Err.Error(), tt.expect) - } - if got.Value() != tt.expect { - t.Errorf(testutils.TestPhraseWithContext, "Value not correct", got.Value(), tt.expect) - } - if got.Key().StoreToken() != tt.token { - t.Errorf(testutils.TestPhraseWithContext, "Incorrect Token returned in Key", got.Key().StoreToken(), tt.token) - } - }) - } -} +// func Test_Strategy_Retrieve_succeeds(t *testing.T) { +// ttests := map[string]struct { +// impl func(t *testing.T) store.Strategy +// config *config.GenVarsConfig +// token string +// expect string +// impPrefix config.ImplementationPrefix +// }{ +// "with mocked implementation AZTABLESTORAGE": { +// func(t *testing.T) store.Strategy { +// return &mockGenerate{"mountPath/token", "bar", nil} +// }, +// config.NewConfig().WithOutputPath("stdout"), +// "mountPath/token", +// "bar", +// config.AzTableStorePrefix, +// }, +// // "error in retrieval": { +// // func(t *testing.T) store.Strategy { +// // return &mockGenerate{"SOME://mountPath/token", "bar", fmt.Errorf("unable to perform getTokenValue")} +// // }, +// // config.NewConfig().WithOutputPath("stdout").WithTokenSeparator("://"), +// // []string{"SOME://token"}, +// // config.AzAppConfigPrefix, +// // "unable to perform getTokenValue", +// // }, +// } +// for name, tt := range ttests { +// t.Run(name, func(t *testing.T) { +// token, _ := config.NewToken(tt.impPrefix, *tt.config) +// token.WithSanitizedToken(tt.token) +// got := strategy.ExchangeToken(tt.impl(t), token) +// if got.Err != nil { +// t.Errorf(testutils.TestPhraseWithContext, "Token response errored", got.Err.Error(), tt.expect) +// } +// if got.Value() != tt.expect { +// t.Errorf(testutils.TestPhraseWithContext, "Value not correct", got.Value(), tt.expect) +// } +// if got.Key().StoreToken() != tt.token { +// t.Errorf(testutils.TestPhraseWithContext, "Incorrect Token returned in Key", got.Key().StoreToken(), tt.token) +// } +// }) +// } +// } -func Test_CustomStrategyFuncMap_add_own(t *testing.T) { +// func Test_CustomStrategyFuncMap_add_own(t *testing.T) { - ttests := map[string]struct { - }{ - "default": {}, - } - for name, _ := range ttests { - t.Run(name, func(t *testing.T) { - called := 0 - genVarsConf := config.NewConfig() - token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig()) - token.WithSanitizedToken("mountPath/token") +// ttests := map[string]struct { +// }{ +// "default": {}, +// } +// for name := range ttests { +// t.Run(name, func(t *testing.T) { +// called := 0 +// genVarsConf := config.NewConfig() +// token, _ := config.NewToken(config.AzTableStorePrefix, *config.NewConfig()) +// token.WithSanitizedToken("mountPath/token") - var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { - m := &mockGenerate{"AZTABLESTORE://mountPath/token", "bar", nil} - called++ - return m, nil - } +// var custFunc = func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { +// m := &mockGenerate{"AZTABLESTORE://mountPath/token", "bar", nil} +// called++ +// return m, nil +// } - s := strategy.New(*genVarsConf, log.New(io.Discard), strategy.WithStrategyFuncMap(strategy.StrategyFuncMap{config.AzTableStorePrefix: custFunc})) +// s := strategy.New(*genVarsConf, log.New(io.Discard), strategy.WithStrategyFuncMap(strategy.StrategyFuncMap{config.AzTableStorePrefix: custFunc})) - store, _ := s.GetImplementation(context.TODO(), token) - _ = strategy.ExchangeToken(store, token) +// store, _ := s.GetImplementation(context.TODO(), token) +// _ = strategy.ExchangeToken(store, token) - if called != 1 { - t.Errorf(testutils.TestPhraseWithContext, "custom func not called", called, 1) - } - }) - } -} +// if called != 1 { +// t.Errorf(testutils.TestPhraseWithContext, "custom func not called", called, 1) +// } +// }) +// } +// } // func Test_SelectImpl_With(t *testing.T) { diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..5ed18b5 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,240 @@ +# Configmanager Plugin System + +The plugin architecture for configmanager is built using the [go-plugin](https://github.com/hashicorp/go-plugin?tab=readme-ov-file#go-plugin-system-over-rpc) from hashicorp. + + +The existing implementations are converted into plugins using the gRPC model and are built using +gRPC [go-plugin](https://github.com/hashicorp/go-plugin?tab=readme-ov-file#go-plugin-system-over-rpc) and generated/updated with the [buf cli](https://buf.build/docs/cli/). + + +## Plugin Architecture + + +```mermaid +``` + +The plugins will need to be downloaded into any one of these locations on disk, they will be checked in this order + +- currentDirectory (directory from which the configmanager executable is run) +- users home directory + +The plugin is expected to be found under this path in the above locations +> `.configmanager/plugins/$PLUGIN_PREFIX_LOWERCASE/$PLUGIN_PREFIX_LOWERCASE-$GOOS-$GOARCH` + +e.g. in case of the AWS Parameter Store plugin `.configmanager/plugins/awsparamstr/awsparamstr-linux-amd64` + + + +## Alternate architecture explored + +As part of the decision on which pluging architecture to use we also explored an alternate architecture using WASIP1. + + + +```go +import ( + "context" + "encoding/binary" + "encoding/json" + "sync" + "unicode/utf8" + "unsafe" + + "github.com/DevLabFoundry/configmanager/v3/plugins" +) + +// ==================== +// Bump allocator +// ==================== + +const heapSize = 64 * 1024 // 64 KiB arena; tune as needed + +type bumpAllocator struct { + mu sync.Mutex + heap []byte + used uint32 +} + +var alloc = bumpAllocator{ + heap: make([]byte, heapSize), +} + +// round allocation up to 8 bytes for basic alignment. +func roundUp(n uint32) uint32 { + const align = 8 + return (n + align - 1) &^ (align - 1) +} + +//go:wasmexport allocate +func Allocate(size uint32) uint32 { + if size == 0 { + return 0 + } + size = roundUp(size) + + alloc.mu.Lock() + defer alloc.mu.Unlock() + + if alloc.used+size > uint32(len(alloc.heap)) { + // Out of memory in our arena. + return 0 + } + + offset := alloc.used + alloc.used += size + + // Return pointer into linear memory for &heap[offset]. + return uint32(uintptr(unsafe.Pointer(&alloc.heap[offset]))) +} + +//go:wasmexport deallocate +func Deallocate(ptr, size uint32) { + // For a simple bump allocator, deallocate is a no-op. + // Memory is reclaimed when the module instance is destroyed. + _ = ptr + _ = size +} + +type Hdr struct { + Data uintptr + Len int + Cap int +} + +// ==================== +// Helpers +// ==================== + +// bytesFromPtrLen reinterprets a (ptr,len) pair in wasm linear memory +// as a Go []byte without copying. +func bytesFromPtrLen(ptr, length uint32) []byte { + if length == 0 { + return nil + } + + hdr := Hdr{ + Data: uintptr(ptr), + Len: int(length), + Cap: int(length), + } + + return *(*[]byte)(unsafe.Pointer(&hdr)) +} + +// ==================== +// strategy_token_value +// ==================== +// +// ABI: +// +// strategy_token_value( +// inPtr, inLen, outPtr, outCap, outLenPtr uint32, +// ) int32 +// +// Host contract: +// - Input bytes are at (inPtr, inLen) +// - Output buffer is [outPtr : outPtr+outCap) +// - outLenPtr points to 4 bytes where we write the required length +// +// Behaviour: +// - If input length == 0 => ERR_EMPTY_INPUT +// - If invalid UTF-8 => ERR_INVALID_UTF8 +// - Always write required length to *outLenPtr (little-endian) +// - If required > outCap => ERR_BUF_TOO_SMALL +// - Else copy into caller buffer and return OK +// +//go:wasmexport strategy_token_value +func StrategyTokenValue(tokenPtr, tokenLen, outPtr, outCap, outLenPtr uint32) int32 { + defer func() { + // Make sure panics don't leak as traps. + if r := recover(); r != nil { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + } + }() + + if tokenLen == 0 { + // Mark required length as 0 and signal error. + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, 0) + } + } + return plugins.ERR_EMPTY_INPUT + } + + tokenBytes := bytesFromPtrLen(tokenPtr, tokenLen) + if !utf8.Valid(tokenBytes) { + if outLenPtr != 0 { + if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { + binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) + } + } + return plugins.ERR_INVALID_UTF8 + } + + // --- Business logic (replace with your real token strategy) --- + // unmarshal string into an object + token := &plugins.MessagExchange{} + if err := json.Unmarshal(tokenBytes, token); err != nil { + return plugins.ERR_FAILED_UNMARSHAL_MESSAGE + } + + // logger := log.New(os.Stdout) + // logger.SetLevel(log.DebugLvl) + + store, err := NewParamStore(context.Background()) + if err != nil { + return plugins.ERR_INIT_STORE + } + + outStr, err := store.Value(token) + if err != nil { + return plugins.ERR_FAILED_VALUE_RETRIEVAL + } + + outBytes := []byte(outStr) + // -------------------------------------------------------------- + // BEGIN RETURN Allocation + // -------------------------------------------------------------- + required := uint32(len(outBytes)) + + // Always write required length. + if outLenPtr != 0 { + lenCell := bytesFromPtrLen(outLenPtr, 4) + if len(lenCell) != 4 { + return plugins.ERR_INTERNAL + } + binary.LittleEndian.PutUint32(lenCell, required) + } + + if required > outCap { + return plugins.ERR_BUF_TOO_SMALL + } + + if required == 0 { + return plugins.OK + } + + outSlice := bytesFromPtrLen(outPtr, outCap) + if uint32(len(outSlice)) < required { + return plugins.ERR_INTERNAL + } + + copy(outSlice, outBytes) + return plugins.OK +} + +// main is required for wasip1 +// scaffolds the `_initialize` method +func main() {} +``` + +### Build notes + +build using the `-buildmode=c-shared` which will convert the module to a reactor module + +`GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awsparams.wasm` diff --git a/plugins/awsparamstr/README.md b/plugins/awsparamstr/README.md index cc07402..8f4fda3 100644 --- a/plugins/awsparamstr/README.md +++ b/plugins/awsparamstr/README.md @@ -1,216 +1,4 @@ # AWS PARAM STORE Plugin -This is the `awsparamstr` implementation plugin built using the gp-plugin architecture from hashicorp... +This is the `awsparamstr` implementation plugin built using the go-plugin architecture from hashicorp... - -## Alternate architecture - -Explored an alternate architecture using WASIP1 - -```go -import ( - "context" - "encoding/binary" - "encoding/json" - "sync" - "unicode/utf8" - "unsafe" - - "github.com/DevLabFoundry/configmanager/v3/plugins" -) - -// ==================== -// Bump allocator -// ==================== - -const heapSize = 64 * 1024 // 64 KiB arena; tune as needed - -type bumpAllocator struct { - mu sync.Mutex - heap []byte - used uint32 -} - -var alloc = bumpAllocator{ - heap: make([]byte, heapSize), -} - -// round allocation up to 8 bytes for basic alignment. -func roundUp(n uint32) uint32 { - const align = 8 - return (n + align - 1) &^ (align - 1) -} - -//go:wasmexport allocate -func Allocate(size uint32) uint32 { - if size == 0 { - return 0 - } - size = roundUp(size) - - alloc.mu.Lock() - defer alloc.mu.Unlock() - - if alloc.used+size > uint32(len(alloc.heap)) { - // Out of memory in our arena. - return 0 - } - - offset := alloc.used - alloc.used += size - - // Return pointer into linear memory for &heap[offset]. - return uint32(uintptr(unsafe.Pointer(&alloc.heap[offset]))) -} - -//go:wasmexport deallocate -func Deallocate(ptr, size uint32) { - // For a simple bump allocator, deallocate is a no-op. - // Memory is reclaimed when the module instance is destroyed. - _ = ptr - _ = size -} - -type Hdr struct { - Data uintptr - Len int - Cap int -} - -// ==================== -// Helpers -// ==================== - -// bytesFromPtrLen reinterprets a (ptr,len) pair in wasm linear memory -// as a Go []byte without copying. -func bytesFromPtrLen(ptr, length uint32) []byte { - if length == 0 { - return nil - } - - hdr := Hdr{ - Data: uintptr(ptr), - Len: int(length), - Cap: int(length), - } - - return *(*[]byte)(unsafe.Pointer(&hdr)) -} - -// ==================== -// strategy_token_value -// ==================== -// -// ABI: -// -// strategy_token_value( -// inPtr, inLen, outPtr, outCap, outLenPtr uint32, -// ) int32 -// -// Host contract: -// - Input bytes are at (inPtr, inLen) -// - Output buffer is [outPtr : outPtr+outCap) -// - outLenPtr points to 4 bytes where we write the required length -// -// Behaviour: -// - If input length == 0 => ERR_EMPTY_INPUT -// - If invalid UTF-8 => ERR_INVALID_UTF8 -// - Always write required length to *outLenPtr (little-endian) -// - If required > outCap => ERR_BUF_TOO_SMALL -// - Else copy into caller buffer and return OK -// -//go:wasmexport strategy_token_value -func StrategyTokenValue(tokenPtr, tokenLen, outPtr, outCap, outLenPtr uint32) int32 { - defer func() { - // Make sure panics don't leak as traps. - if r := recover(); r != nil { - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, 0) - } - } - } - }() - - if tokenLen == 0 { - // Mark required length as 0 and signal error. - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, 0) - } - } - return plugins.ERR_EMPTY_INPUT - } - - tokenBytes := bytesFromPtrLen(tokenPtr, tokenLen) - if !utf8.Valid(tokenBytes) { - if outLenPtr != 0 { - if lenCell := bytesFromPtrLen(outLenPtr, 4); len(lenCell) == 4 { - binary.LittleEndian.PutUint32(lenCell, uint32(len(tokenBytes))) - } - } - return plugins.ERR_INVALID_UTF8 - } - - // --- Business logic (replace with your real token strategy) --- - // unmarshal string into an object - token := &plugins.MessagExchange{} - if err := json.Unmarshal(tokenBytes, token); err != nil { - return plugins.ERR_FAILED_UNMARSHAL_MESSAGE - } - - // logger := log.New(os.Stdout) - // logger.SetLevel(log.DebugLvl) - - store, err := NewParamStore(context.Background()) - if err != nil { - return plugins.ERR_INIT_STORE - } - - outStr, err := store.Value(token) - if err != nil { - return plugins.ERR_FAILED_VALUE_RETRIEVAL - } - - outBytes := []byte(outStr) - // -------------------------------------------------------------- - // BEGIN RETURN Allocation - // -------------------------------------------------------------- - required := uint32(len(outBytes)) - - // Always write required length. - if outLenPtr != 0 { - lenCell := bytesFromPtrLen(outLenPtr, 4) - if len(lenCell) != 4 { - return plugins.ERR_INTERNAL - } - binary.LittleEndian.PutUint32(lenCell, required) - } - - if required > outCap { - return plugins.ERR_BUF_TOO_SMALL - } - - if required == 0 { - return plugins.OK - } - - outSlice := bytesFromPtrLen(outPtr, outCap) - if uint32(len(outSlice)) < required { - return plugins.ERR_INTERNAL - } - - copy(outSlice, outBytes) - return plugins.OK -} - -// main is required for wasip1 -// scaffolds the `_initialize` method -func main() {} -``` - -### Build notes - -build using the `-buildmode=c-shared` which will convert the module to a reactor module - -`GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o awsparams.wasm` diff --git a/plugins/awssecrets/.gitkeep b/plugins/awssecrets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/grpc.go b/plugins/grpc.go index 5f9ebf0..c59d35f 100644 --- a/plugins/grpc.go +++ b/plugins/grpc.go @@ -6,7 +6,8 @@ import ( "github.com/DevLabFoundry/configmanager/v3/plugins/proto" ) -// GRPCClient is an implementation of KV that talks over RPC. +// GRPCClient is the host process talking to the plugins +// i.e. the GRPCServer implementation of the TokenStore type GRPCClient struct{ client proto.TokenStoreClient } func (m *GRPCClient) Value(key string, metadata []byte) (string, error) { diff --git a/plugins/vault/README.md b/plugins/vault/README.md new file mode 100644 index 0000000..27b590e --- /dev/null +++ b/plugins/vault/README.md @@ -0,0 +1,3 @@ +# Hashicorp Vault + + diff --git a/plugins/vault/impl/hashivault.go b/plugins/vault/impl/hashivault.go new file mode 100644 index 0000000..75ba351 --- /dev/null +++ b/plugins/vault/impl/hashivault.go @@ -0,0 +1,173 @@ +package impl + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/plugins" + + vault "github.com/hashicorp/vault/api" + auth "github.com/hashicorp/vault/api/auth/aws" +) + +// HashiVaultHelper provides a broken up string +type HashiVaultHelper struct { + Path string + Token string +} + +type hashiVaultApi interface { + Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) + GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) +} + +type VaultStore struct { + svc hashiVaultApi + ctx context.Context + logger log.ILogger + config *VaultConfig + token *config.ParsedTokenConfig + strippedToken string +} + +// VaultConfig holds the parseable metadata struct +type VaultConfig struct { + Version string `json:"version"` + Role string `json:"iam_role"` +} + +func NewVaultStore(ctx context.Context, token *config.ParsedTokenConfig, logger log.ILogger) (*VaultStore, error) { + storeConf := &VaultConfig{} + _ = token.ParseMetadata(storeConf) + imp := &VaultStore{ + ctx: ctx, + logger: logger, + config: storeConf, + token: token, + } + + config := vault.DefaultConfig() + vt := SplitHashiVaultToken(token.StoreToken()) + imp.strippedToken = vt.Token + client, err := vault.NewClient(config) + if err != nil { + return nil, fmt.Errorf("%v\n%w", err, plugins.ErrClientInitialization) + } + + if strings.HasPrefix(os.Getenv("VAULT_TOKEN"), "aws_iam") { + awsclient, err := newVaultStoreWithAWSAuthIAM(client, storeConf.Role) + if err != nil { + return nil, err + } + client = awsclient + } + imp.svc = client.KVv2(vt.Path) + return imp, nil +} + +func (s *VaultStore) WithSvc(svc hashiVaultApi) { + s.svc = svc +} + +// newVaultStoreWithAWSAuthIAM returns an initialised client with AWSIAMAuth +// EC2 auth type is not supported currently +func newVaultStoreWithAWSAuthIAM(client *vault.Client, role string) (*vault.Client, error) { + if len(role) < 1 { + return nil, fmt.Errorf("role provided is empty, EC2 auth not supported") + } + awsAuth, err := auth.NewAWSAuth( + auth.WithRole(role), + ) + if err != nil { + return nil, fmt.Errorf("unable to initialize AWS auth method: %s. %w", err, plugins.ErrClientInitialization) + } + + authInfo, err := client.Auth().Login(context.Background(), awsAuth) + + if err != nil { + return nil, fmt.Errorf("unable to login to AWS auth method: %s. %w", err, plugins.ErrClientInitialization) + } + if authInfo == nil { + return nil, fmt.Errorf("no auth info was returned after login") + } + + return client, nil +} + +// setTokenVal +// imp.token is already set in the Vault constructor +// +// This happens inside the New func call +// due to the way the client needs to be +// initialised with a mountpath +// and mountpath is part of the token so it is set then +func (imp *VaultStore) SetToken(token *config.ParsedTokenConfig) {} + +// getTokenValue implements the underlying techonology +// token retrieval and returns a stringified version +// of the secret +func (imp *VaultStore) Value() (string, error) { + imp.logger.Info("%s", "Concrete implementation HashiVault") + imp.logger.Info("Getting Secret: %s", imp.token) + + ctx, cancel := context.WithCancel(imp.ctx) + defer cancel() + + secret, err := imp.getSecret(ctx, imp.strippedToken, imp.config.Version) + if err != nil { + imp.logger.Error(plugins.ImplementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) + return "", err + } + + if secret.Data != nil { + resp, err := marshall(secret.Data) + if err != nil { + imp.logger.Error("marshalling error: %s", err.Error()) + return "", err + } + imp.logger.Debug("marhalled kvv2: %s", resp) + return resp, nil + } + + imp.logger.Error("value retrieved but empty for token: %v", imp.token) + return "", nil +} + +func (imp *VaultStore) getSecret(ctx context.Context, token string, version string) (*vault.KVSecret, error) { + if version != "" { + v, err := strconv.Atoi(version) + if err != nil { + return nil, fmt.Errorf("unable to parse version into an integer: %s", err.Error()) + } + return imp.svc.GetVersion(ctx, token, v) + } + return imp.svc.Get(ctx, token) +} + +func SplitHashiVaultToken(token string) HashiVaultHelper { + vh := HashiVaultHelper{} + // split token to extract the mount path + s := strings.Split(strings.TrimPrefix(token, "/"), "___") + // grab token and trim prefix if slash + vh.Token = strings.TrimPrefix(strings.Join(s[1:], ""), "/") + // assign mount path as extracted from input token + vh.Path = s[0] + return vh +} + +// marshall converts map[string]any into a JSON +// object. Secrets should only be a single level +// deep. +func marshall(secret map[string]any) (string, error) { + b, err := json.Marshal(secret) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/plugins/vault/impl/hashivault_test.go b/plugins/vault/impl/hashivault_test.go new file mode 100644 index 0000000..8e6f94a --- /dev/null +++ b/plugins/vault/impl/hashivault_test.go @@ -0,0 +1,624 @@ +package impl_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/DevLabFoundry/configmanager/v3/internal/config" + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/internal/testutils" + "github.com/DevLabFoundry/configmanager/v3/plugins/vault/impl" + vault "github.com/hashicorp/vault/api" +) + +func TestMountPathExtract(t *testing.T) { + ttests := map[string]struct { + token func() *config.ParsedTokenConfig + expect string + }{ + "without leading slash": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/demo/configmanager" + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret"}, + "with leading slash": { + func() *config.ParsedTokenConfig { + // "VAULT:///secret___/demo/configmanager", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("/secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret"}, + "with underscore in path name": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("_secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "_secret"}, + "with double underscore in path name": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("__secret___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "__secret"}, + "with multiple paths in mountpath": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret/bar/path___/demo/configmanager") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, "secret/bar/path"}, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + got := impl.SplitHashiVaultToken(tt.token().StoreToken()) + if got.Path != tt.expect { + t.Errorf("got %q, expected %q", got, tt.expect) + } + }) + } +} + +type mockVaultApi struct { + g func(ctx context.Context, secretPath string) (*vault.KVSecret, error) + gv func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) +} + +func (m mockVaultApi) Get(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + return m.g(ctx, secretPath) +} + +func (m mockVaultApi) GetVersion(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { + return m.gv(ctx, secretPath, version) +} + +func TestVaultScenarios(t *testing.T) { + + ttests := map[string]struct { + token func() *config.ParsedTokenConfig + expect string + mockClient func(t *testing.T) mockVaultApi + setupEnv func() func() + }{ + "happy return": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `{"foo":"test2130-9sd-0ds"}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "foo" { + t.Errorf("got %v; want %s", secretPath, `foo`) + } + m := make(map[string]interface{}) + m["foo"] = "test2130-9sd-0ds" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "incorrect json": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/foo", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `json: unsupported type: func() error`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "foo" { + t.Errorf("got %v; want %s", secretPath, `foo`) + } + m := make(map[string]interface{}) + m["error"] = func() error { return fmt.Errorf("ddodod") } + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "another return": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret/engine1___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `{"foo1":"test2130-9sd-0ds","foo2":"dsfsdf3454456"}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + m["foo1"] = "test2130-9sd-0ds" + m["foo2"] = "dsfsdf3454456" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "not found": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/foo", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/foo") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `secret not found`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "foo" { + t.Errorf("got %v; want %s", secretPath, `foo`) + } + return nil, fmt.Errorf("secret not found") + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "403": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `client 403`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) + } + return nil, fmt.Errorf("client 403") + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "found but empty": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + // config.NewConfig(), + `{}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf("got %v; want %s", secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "found but nil returned": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + "", + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return &vault.KVSecret{Data: nil}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "version provided correctly": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2[version=1]", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1") + return tkn + }, + `{"foo2":"dsfsdf3454456"}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + m["foo2"] = "dsfsdf3454456" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "version provided but unable to parse": { + func() *config.ParsedTokenConfig { + // "VAULT://secret___/some/other/foo2[version=1a]", + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1a") + return tkn + }, + "unable to parse version into an integer: strconv.Atoi: parsing \"1a\": invalid syntax", + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.gv = func(ctx context.Context, secretPath string, version int) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return nil, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "129378y1231283") + return func() { + os.Clearenv() + } + }, + }, + "vault rate limit incorrect": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("") + return tkn + }, + `error encountered setting up default configuration: VAULT_RATE_LIMIT was provided but incorrectly formatted +failed to initialize the client`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return &vault.KVSecret{Data: nil}, nil + } + return mv + }, + func() func() { + os.Setenv("VAULT_TOKEN", "") + os.Setenv("VAULT_RATE_LIMIT", "wrong") + return func() { + os.Clearenv() + } + }, + }, + } + + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + tearDown := tt.setupEnv() + defer tearDown() + + i, err := impl.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) + if err != nil { + if err.Error() != tt.expect { + t.Fatalf("failed to init hashivault, %v", err.Error()) + } + return + } + + i.WithSvc(tt.mockClient(t)) + got, err := i.Value() + if err != nil { + if err.Error() != tt.expect { + t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) + } + return + } + if got != tt.expect { + t.Errorf(testutils.TestPhrase, got, tt.expect) + } + }) + } +} + +func TestAwsIamAuth(t *testing.T) { + ttests := map[string]struct { + token func() *config.ParsedTokenConfig + expect string + mockClient func(t *testing.T) mockVaultApi + mockHanlder func(t *testing.T) http.Handler + setupEnv func(addr string) func() + }{ + "aws_iam auth no role specified": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1") + return tkn + }, + "role provided is empty, EC2 auth not supported", + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return &vault.KVSecret{Data: nil}, nil + } + return mv + }, + func(t *testing.T) http.Handler { + return nil + }, + func(_ string) func() { + os.Setenv("VAULT_TOKEN", "aws_iam") + os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") + os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") + os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") + os.Setenv("AWS_REGION", "eu-west-1") + return func() { + os.Clearenv() + } + }, + }, + "aws_iam auth incorrectly formatted request": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("version=1,iam_role=not_a_role") + return tkn + }, + `unable to login to AWS auth method: unable to log in to auth method: unable to log in with AWS auth: Error making API request. + +URL: PUT %s/v1/auth/aws/login +Code: 400. Raw Message: + +incorrect values supplied. failed to initialize the client`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + return &vault.KVSecret{Data: nil}, nil + } + return mv + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + w.Write([]byte(`incorrect values supplied`)) + }) + return mux + }, + func(addr string) func() { + os.Setenv("VAULT_TOKEN", "aws_iam") + os.Setenv("VAULT_ADDR", addr) + os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") + os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") + os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") + os.Setenv("AWS_REGION", "eu-west-1") + return func() { + os.Clearenv() + } + }, + }, + "aws_iam auth success": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") + return tkn + }, + // + `{"foo2":"dsfsdf3454456"}`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + m := make(map[string]any) + m["foo2"] = "dsfsdf3454456" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(`{"auth":{"client_token": "fooresddfasdsasad"}}`)) + }) + return mux + }, + func(addr string) func() { + os.Setenv("VAULT_TOKEN", "aws_iam") + os.Setenv("VAULT_ADDR", addr) + os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") + os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") + os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") + os.Setenv("AWS_REGION", "eu-west-1") + return func() { + os.Clearenv() + } + }, + }, + "aws_iam auth no token returned": { + func() *config.ParsedTokenConfig { + tkn, _ := config.NewToken(config.HashicorpVaultPrefix, *config.NewConfig()) + tkn.WithSanitizedToken("secret___/some/other/foo2") + tkn.WithKeyPath("") + tkn.WithMetadata("iam_role=arn:aws:iam::1111111:role/i-orchestration") + return tkn + }, + `unable to login to AWS auth method: response did not return ClientToken, client token not set. failed to initialize the client`, + func(t *testing.T) mockVaultApi { + mv := mockVaultApi{} + mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { + t.Helper() + if secretPath != "some/other/foo2" { + t.Errorf(testutils.TestPhrase, secretPath, `some/other/foo2`) + } + m := make(map[string]interface{}) + m["foo2"] = "dsfsdf3454456" + return &vault.KVSecret{Data: m}, nil + } + return mv + }, + func(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/v1/auth/aws/login", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(`{"auth":{}}`)) + }) + return mux + }, + func(addr string) func() { + os.Setenv("VAULT_TOKEN", "aws_iam") + os.Setenv("VAULT_ADDR", addr) + os.Setenv("AWS_ACCESS_KEY_ID", "1280qwed9u9nsc9fdsbv9gsfrd") + os.Setenv("AWS_SECRET_ACCESS_KEY", "SED)SDVfdv0jfds08sdfgu09sd943tj4fELH/") + os.Setenv("AWS_SESSION_TOKEN", "IQoJb3JpZ2luX2VjELH//////////wEaCWV1LXdlc3QtMiJIMEYCIQDPU6UGJ0...df.fdgdfg.dfg.gdf.dgf") + os.Setenv("AWS_REGION", "eu-west-1") + return func() { + os.Clearenv() + } + }, + }, + } + + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + // + ts := httptest.NewServer(tt.mockHanlder(t)) + tearDown := tt.setupEnv(ts.URL) + defer tearDown() + i, err := impl.NewVaultStore(context.TODO(), tt.token(), log.New(io.Discard)) + if err != nil { + // WHAT A CRAP way to do this... + if err.Error() != strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0] { + t.Errorf(testutils.TestPhraseWithContext, "aws iam auth", err.Error(), strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0]) + t.Fatalf("failed to init hashivault, %v", err.Error()) + } + return + } + + i.WithSvc(tt.mockClient(t)) + got, err := i.Value() + if err != nil { + if err.Error() != tt.expect { + t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) + } + return + } + if got != tt.expect { + t.Errorf(testutils.TestPhrase, got, tt.expect) + } + }) + } +} diff --git a/plugins/vault/main.go b/plugins/vault/main.go new file mode 100644 index 0000000..c4b340f --- /dev/null +++ b/plugins/vault/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "os" + + "github.com/DevLabFoundry/configmanager/v3/internal/log" + "github.com/DevLabFoundry/configmanager/v3/plugins" + "github.com/DevLabFoundry/configmanager/v3/plugins/awsparamstr/impl" + "github.com/hashicorp/go-plugin" +) + +// Here is a real implementation of KV that writes to a local file with +// the key name and the contents are the value of the key. +type TokenStorePlugin struct{} + +func (ts TokenStorePlugin) Value(key string, metadata []byte) (string, error) { + srv, err := impl.NewParamStore(context.Background(), log.New(os.Stderr)) + if err != nil { + return "", err + } + return srv.Value(key, metadata) +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: plugins.Handshake, + Plugins: map[string]plugin.Plugin{ + "configmanager_token_store": &plugins.TokenStoreGRPCPlugin{Impl: &TokenStorePlugin{}}, + }, + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +}