From 29a93974d8a215a1d2233ff01c551ecb85b4b794 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Thu, 28 Mar 2024 15:14:26 +0000 Subject: [PATCH] Feat/v2 alpha0.0.1 (#28) * BREAKING!: change the interface of configmanager +semver: feat +semver: feature * fix[breaking]: added post processor to cmd utils * fix: add strategy package into internal * fix: add store implementations * fix: ci updated * fix: add more tests * fix: unit tests * fix: ci definitions * fix: add additional tests TODO: still uncomment some not ported negative assertions * docs: update breaking change notice * fix: add more tests to cmdutils * fix: typo +semver: major +semver: breaking --- .github/workflows/build.yml | 18 +- .github/workflows/pr.yml | 18 +- .github/workflows/release.yml | 10 +- Makefile | 4 +- README.md | 13 + cmd/configmanager/fromfileinput.go | 9 +- cmd/configmanager/retrieve.go | 9 +- cmd/configmanager/root.go | 5 +- cmd/main.go | 8 +- configmanager.go | 181 ++-- configmanager_test.go | 818 +++++++++------- examples/examples.go | 69 +- internal/cmdutils/cmdutils.go | 93 +- internal/cmdutils/cmdutils_test.go | 635 +++++-------- internal/cmdutils/postprocessor.go | 95 ++ internal/cmdutils/postprocessor_test.go | 77 ++ internal/config/config.go | 258 ++++++ internal/config/config_test.go | 140 ++- .../generator => internal/store}/azappconf.go | 36 +- .../store}/azappconf_test.go | 37 +- .../generator => internal/store}/azhelpers.go | 2 +- .../store}/azkeyvault.go | 33 +- .../store}/azkeyvault_test.go | 27 +- .../store}/aztablestorage.go | 31 +- .../store}/aztablestorage_test.go | 61 +- .../store}/gcpsecrets.go | 25 +- .../store}/gcpsecrets_test.go | 46 +- .../store}/hashivault.go | 29 +- .../store}/hashivault_test.go | 55 +- .../store}/paramstore.go | 24 +- .../store}/paramstore_test.go | 37 +- .../store}/secretsmanager.go | 26 +- .../store}/secretsmanager_test.go | 41 +- internal/store/store.go | 39 + internal/store/store_test.go | 24 + internal/strategy/strategy.go | 126 +++ internal/strategy/strategy_test.go | 381 ++++++++ pkg/generator/config.go | 137 --- pkg/generator/config_test.go | 84 -- pkg/generator/defaultstrategy.go | 30 - pkg/generator/generator.go | 254 ++--- pkg/generator/generator_test.go | 873 ++++++++++-------- pkg/generator/strategy.go | 94 -- pkg/generator/strategy_test.go | 223 ----- 44 files changed, 2918 insertions(+), 2317 deletions(-) create mode 100644 internal/cmdutils/postprocessor.go create mode 100644 internal/cmdutils/postprocessor_test.go rename {pkg/generator => internal/store}/azappconf.go (66%) rename {pkg/generator => internal/store}/azappconf_test.go (83%) rename {pkg/generator => internal/store}/azhelpers.go (98%) rename {pkg/generator => internal/store}/azkeyvault.go (62%) rename {pkg/generator => internal/store}/azkeyvault_test.go (87%) rename {pkg/generator => internal/store}/aztablestorage.go (72%) rename {pkg/generator => internal/store}/aztablestorage_test.go (84%) rename {pkg/generator => internal/store}/gcpsecrets.go (70%) rename {pkg/generator => internal/store}/gcpsecrets_test.go (84%) rename {pkg/generator => internal/store}/hashivault.go (84%) rename {pkg/generator => internal/store}/hashivault_test.go (85%) rename {pkg/generator => internal/store}/paramstore.go (68%) rename {pkg/generator => internal/store}/paramstore_test.go (81%) rename {pkg/generator => internal/store}/secretsmanager.go (70%) rename {pkg/generator => internal/store}/secretsmanager_test.go (80%) create mode 100644 internal/store/store.go create mode 100644 internal/store/store_test.go create mode 100644 internal/strategy/strategy.go create mode 100644 internal/strategy/strategy_test.go delete mode 100644 pkg/generator/config.go delete mode 100644 pkg/generator/config_test.go delete mode 100644 pkg/generator/defaultstrategy.go delete mode 100644 pkg/generator/strategy.go delete mode 100644 pkg/generator/strategy_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 940cae9..f76a6e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,15 +12,21 @@ jobs: outputs: semVer: ${{ steps.gitversion.outputs.semVer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: install deps + run: | + apt-get update && apt-get install -y jq git + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git config user.email ${{ github.actor }}-ci@gha.org + git config user.name ${{ github.actor }} - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.15 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: '5.x' - name: Set SemVer Version - uses: gittools/actions/gitversion/execute@v0.9.15 + uses: gittools/actions/gitversion/execute@v1.1.1 id: gitversion - name: echo VERSIONS @@ -30,14 +36,14 @@ jobs: test: runs-on: ubuntu-latest container: - image: golang:1.19-bullseye + image: golang:1.21-bullseye needs: set-version env: SEMVER: ${{ needs.set-version.outputs.semVer }} GIT_TAG: ${{ needs.set-version.outputs.semVer }} GOVCS: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 - name: install deps @@ -50,7 +56,7 @@ jobs: run: | make REVISION=$GITHUB_SHA test - name: Publish Junit style Test Report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: report_paths: '**/.coverage/report-junit.xml' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6e41776..e000335 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,26 +12,32 @@ jobs: outputs: semVer: ${{ steps.gitversion.outputs.semVer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: install deps + run: | + apt-get update && apt-get install -y jq git + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git config user.email ${{ github.actor }}-ci@gha.org + git config user.name ${{ github.actor }} - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.15 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: '5.x' - name: Set SemVer Version - uses: gittools/actions/gitversion/execute@v0.9.15 + uses: gittools/actions/gitversion/execute@v1.1.1 id: gitversion pr: runs-on: ubuntu-latest container: - image: golang:1.19-bullseye + image: golang:1.21-bullseye needs: set-version env: REVISION: $GITHUB_SHA SEMVER: ${{ needs.set-version.outputs.semVer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: install deps run: | apt-get update && apt-get install -y jq git @@ -42,7 +48,7 @@ jobs: run: | make REVISION=$GITHUB_SHA test - name: Publish Junit style Test Report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: report_paths: '**/report-junit.xml' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c2abe0..ada8ae8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,15 +15,15 @@ jobs: outputs: semVer: ${{ steps.gitversion.outputs.semVer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.15 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: '5.x' - name: Set SemVer Version - uses: gittools/actions/gitversion/execute@v0.9.15 + uses: gittools/actions/gitversion/execute@v1.1.1 id: gitversion - name: echo VERSIONS @@ -33,14 +33,14 @@ jobs: release: runs-on: ubuntu-latest container: - image: golang:1.19-bullseye + image: golang:1.21-bullseye env: FOO: Bar needs: set-version env: SEMVER: ${{ needs.set-version.outputs.semVer }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 - name: install deps diff --git a/Makefile b/Makefile index 621629d..f307691 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ LDFLAGS := -ldflags="-s -w -X \"github.com/$(OWNER)/$(NAME)/cmd/configmanager.Ve .PHONY: test test_ci tidy install cross-build test: test_prereq - go test `go list ./... | grep -v */generated/` -v -buildvcs=false -mod=readonly -coverprofile=.coverage/out ; \ - cat .coverage/out | go-junit-report > .coverage/report-junit.xml && \ + go test ./... -v -buildvcs=false -mod=readonly -coverprofile=.coverage/out > .coverage/unit ; \ + cat .coverage/unit | go-junit-report > .coverage/report-junit.xml && \ gocov convert .coverage/out | gocov-xml > .coverage/report-cobertura.xml test_ci: diff --git a/README.md b/README.md index 553a207..6476e37 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,19 @@ Where `configVar` can be either a parseable string `'som3#!S$CRet'` or a number This can be leveraged from any application written in Go - on start up or at runtime. Secrets/Config items can be retrieved in "bulk" and parsed into a provided type, [see here for examples](./examples/examples.go). + > BREAKING CHANGE v2.x with the API (see [examples](./examples/examples.go)) + - `generator.NewConfig()` is no longer required. + + ```go + // initialise new configmanager instance + cm := configmanager.New(context.TODO()) + // add additional config to apply on your tokens + cm.Config.WithTokenSeparator("://") + pm, err := cm.Retrieve([]string{"IMPLEMENTATION://token1", "IMPLEMENTATION:// token2","ANOTHER_IMPL://token1"}) + ``` + + - `RetrieveUnmarshalledFromYaml`|`RetrieveUnmarshalledFromJson`|`RetrieveMarshalledJson`|`RetrieveMarshalledYaml` methods are now on the ConfigManager struct, see `exampleRetrieveYamlMarshalled` or `exampleRetrieveYamlUnmarshalled` in [examples](./examples/examples.go) + - 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. diff --git a/cmd/configmanager/fromfileinput.go b/cmd/configmanager/fromfileinput.go index e5ecb80..ae056fe 100644 --- a/cmd/configmanager/fromfileinput.go +++ b/cmd/configmanager/fromfileinput.go @@ -1,12 +1,10 @@ package cmd import ( - "context" "fmt" "github.com/dnitsch/configmanager" "github.com/dnitsch/configmanager/internal/cmdutils" - "github.com/dnitsch/configmanager/pkg/generator" "github.com/spf13/cobra" ) @@ -43,8 +41,7 @@ unix style output only`) } func retrieveFromStr(cmd *cobra.Command, args []string) error { - conf := generator.NewConfig().WithTokenSeparator(tokenSeparator).WithOutputPath(path).WithKeySeparator(keySeparator) - gv := generator.NewGenerator().WithConfig(conf).WithContext(context.Background()) - configManager := &configmanager.ConfigManager{} - return cmdutils.New(gv, configManager).GenerateStrOut(input, path) + cm := configmanager.New(cmd.Context()) + cm.Config.WithTokenSeparator(tokenSeparator).WithOutputPath(path).WithKeySeparator(keySeparator) + return cmdutils.New(cm).GenerateStrOut(input, path) } diff --git a/cmd/configmanager/retrieve.go b/cmd/configmanager/retrieve.go index 08301f5..30d831c 100644 --- a/cmd/configmanager/retrieve.go +++ b/cmd/configmanager/retrieve.go @@ -1,12 +1,10 @@ package cmd import ( - "context" "fmt" "github.com/dnitsch/configmanager" "github.com/dnitsch/configmanager/internal/cmdutils" - "github.com/dnitsch/configmanager/pkg/generator" "github.com/spf13/cobra" ) @@ -39,8 +37,7 @@ func init() { } func retrieveRun(cmd *cobra.Command, args []string) error { - conf := generator.NewConfig().WithTokenSeparator(tokenSeparator).WithOutputPath(path).WithKeySeparator(keySeparator) - gv := generator.NewGenerator().WithConfig(conf).WithContext(context.Background()) - configManager := &configmanager.ConfigManager{} - return cmdutils.New(gv, configManager).GenerateFromCmd(tokens, path) + cm := configmanager.New(cmd.Context()) + cm.Config.WithTokenSeparator(tokenSeparator).WithOutputPath(path).WithKeySeparator(keySeparator) + return cmdutils.New(cm).GenerateFromCmd(tokens, path) } diff --git a/cmd/configmanager/root.go b/cmd/configmanager/root.go index be19bdb..d570c08 100644 --- a/cmd/configmanager/root.go +++ b/cmd/configmanager/root.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "os" @@ -20,8 +21,8 @@ var ( } ) -func Execute() { - if err := configmanagerCmd.Execute(); err != nil { +func Execute(ctx context.Context) { + if err := configmanagerCmd.ExecuteContext(ctx); err != nil { fmt.Errorf("cli error: %v", err) os.Exit(1) } diff --git a/cmd/main.go b/cmd/main.go index 54fc98f..5102e51 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,8 +1,12 @@ package main -import cfgmgr "github.com/dnitsch/configmanager/cmd/configmanager" +import ( + "context" + + cfgmgr "github.com/dnitsch/configmanager/cmd/configmanager" +) func main() { // init loggerHere or in init function - cfgmgr.Execute() + cfgmgr.Execute(context.Background()) } diff --git a/configmanager.go b/configmanager.go index 9565f98..61e3a2c 100644 --- a/configmanager.go +++ b/configmanager.go @@ -1,48 +1,77 @@ package configmanager import ( + "context" "encoding/json" "fmt" "regexp" "sort" "strings" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/generator" - yaml "gopkg.in/yaml.v3" + "gopkg.in/yaml.v3" ) const ( TERMINATING_CHAR string = `[^\'\"\s\n\\\,]` ) -type ConfigManager struct{} +// generateAPI +type generateAPI interface { + Generate(tokens []string) (generator.ParsedMap, error) +} -// Retrieve gets a rawMap from a set implementation -// will be empty if no matches found -func (c *ConfigManager) Retrieve(tokens []string, config generator.GenVarsConfig) (generator.ParsedMap, error) { - gv := generator.NewGenerator().WithConfig(&config) - return retrieve(tokens, gv) +type ConfigManager struct { + Config *config.GenVarsConfig + generator generateAPI } -// GenerateAPI -type GenerateAPI interface { - Generate(tokens []string) (generator.ParsedMap, error) +// New returns an initialised instance of ConfigManager +// Uses default config for: +// +// ``` +// outputPath = "" +// keySeparator = "|" +// tokenSeparator = "://" +// ``` +// +// Calling cm.Config.WithXXX() will overwrite the generator config +func New(ctx context.Context) *ConfigManager { + cm := &ConfigManager{} + defaultConfig := config.NewConfig() + cm.Config = defaultConfig + cm.generator = generator.NewGenerator().WithConfig(cm.Config) + return cm } -func retrieve(tokens []string, gv GenerateAPI) (generator.ParsedMap, error) { - return gv.Generate(tokens) +// GeneratorConfig +// Returns the gettable generator config +func (c *ConfigManager) GeneratorConfig() *config.GenVarsConfig { + return c.Config } -// 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, config generator.GenVarsConfig) (string, error) { - gv := generator.NewGenerator().WithConfig(&config) - return retrieveWithInputReplaced(input, gv) +// WithGenerator replaces the generator instance +func (c *ConfigManager) WithGenerator(generator generateAPI) *ConfigManager { + c.generator = generator + return c } -func retrieveWithInputReplaced(input string, gv GenerateAPI) (string, error) { +// 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) { + return c.generator.Generate(tokens) +} + +// 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) { - m, err := retrieve(FindTokens(input), gv) + m, err := c.retrieve(FindTokens(input)) if err != nil { return "", err @@ -55,7 +84,7 @@ func retrieveWithInputReplaced(input string, gv GenerateAPI) (string, error) { // from a given input string func FindTokens(input string) []string { tokens := []string{} - for k := range generator.VarPrefix { + for k := range config.VarPrefix { matches := regexp.MustCompile(regexp.QuoteMeta(string(k))+`.(`+TERMINATING_CHAR+`+)`).FindAllString(input, -1) tokens = append(tokens, matches...) } @@ -94,96 +123,84 @@ func orderedKeysList(inputMap generator.ParsedMap) []string { return mkeys } -type CMRetrieveWithInputReplacediface interface { - RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) -} - -// KubeControllerSpecHelper is a helper method, it marshalls an input value of that type into a string and passes it into the relevant configmanger retrieve method -// and returns the unmarshalled object back. +// RetrieveMarshalledJson // -// # It accepts a DI of configmanager and the config (for testability) to replace all occurences of replaceable tokens inside a Marshalled string of that type +// 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. // -// Deprecated: Left for compatibility reasons -func KubeControllerSpecHelper[T any](inputType T, cm CMRetrieveWithInputReplacediface, config generator.GenVarsConfig) (*T, error) { - outType := new(T) - rawBytes, err := json.Marshal(inputType) - if err != nil { - return nil, err - } - - replaced, err := cm.RetrieveWithInputReplaced(string(rawBytes), config) - if err != nil { - return nil, err - } - if err := json.Unmarshal([]byte(replaced), outType); err != nil { - return nil, err - } - return outType, nil -} - -// RetrieveMarshalledJson is a helper method. +// 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. // -// It marshalls an input value of that type into a []byte and passes it into the relevant configmanger retrieve method -// returns the unmarshalled object back with all tokens replaced IF found for their specific vault implementation values. -// Type must contain all public members with a JSON tag on the struct -func RetrieveMarshalledJson[T any](input *T, cm CMRetrieveWithInputReplacediface, config generator.GenVarsConfig) (*T, error) { - outType := new(T) +// 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 nil, err + return err } - - outVal, err := RetrieveUnmarshalledFromJson(rawBytes, outType, cm, config) + // run the replacement of tokens for values + replacedString, err := cm.RetrieveWithInputReplaced(string(rawBytes)) if err != nil { - return outType, err + return err } - - return outVal, nil + // replace the original pointer value with replaced tokens + if err := json.Unmarshal([]byte(replacedString), input); err != nil { + return err + } + return nil } -// RetrieveUnmarshalledFromJson is a helper method. -// Same as RetrieveMarshalledJson but it accepts an already marshalled byte slice -func RetrieveUnmarshalledFromJson[T any](input []byte, output *T, cm CMRetrieveWithInputReplacediface, config generator.GenVarsConfig) (*T, error) { - replaced, err := cm.RetrieveWithInputReplaced(string(input), config) +// 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 output, err + return err } if err := json.Unmarshal([]byte(replaced), output); err != nil { - return output, err + return err } - return output, nil + return nil } -// RetrieveMarshalledYaml is a helper method. +// RetrieveMarshalledYaml // -// It marshalls an input value of that type into a []byte and passes it into the relevant configmanger retrieve method -// returns the unmarshalled object back with all tokens replaced IF found for their specific vault implementation values. -// Type must contain all public members with a YAML tag on the struct -func RetrieveMarshalledYaml[T any](input *T, cm CMRetrieveWithInputReplacediface, config generator.GenVarsConfig) (*T, error) { - outType := new(T) +// 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 outType, err + return err } - outVal, err := RetrieveUnmarshalledFromYaml(rawBytes, outType, cm, config) + // run the replacement of tokens for values + replacedString, err := cm.RetrieveWithInputReplaced(string(rawBytes)) if err != nil { - return outType, err + return err } - - return outVal, nil + // replace the original pointer value with replaced tokens + if err := yaml.Unmarshal([]byte(replacedString), input); err != nil { + return err + } + return nil } -// RetrieveUnmarshalledFromYaml is a helper method. +// RetrieveUnmarshalledFromYaml // -// Same as RetrieveMarshalledYaml but it accepts an already marshalled byte slice -func RetrieveUnmarshalledFromYaml[T any](input []byte, output *T, cm CMRetrieveWithInputReplacediface, config generator.GenVarsConfig) (*T, error) { - replaced, err := cm.RetrieveWithInputReplaced(string(input), config) +// Same as RetrieveUnmarshalledFromJson +func (c *ConfigManager) RetrieveUnmarshalledFromYaml(input []byte, output any) error { + replaced, err := c.RetrieveWithInputReplaced(string(input)) if err != nil { - return output, err + return err } if err := yaml.Unmarshal([]byte(replaced), output); err != nil { - return output, err + return err } - return output, nil + return nil } diff --git a/configmanager_test.go b/configmanager_test.go index 04577f1..fc3fe4e 100644 --- a/configmanager_test.go +++ b/configmanager_test.go @@ -1,76 +1,77 @@ -package configmanager +package configmanager_test import ( + "context" "fmt" - "io" "reflect" "sort" "testing" + "github.com/dnitsch/configmanager" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" "github.com/dnitsch/configmanager/pkg/generator" + "github.com/go-test/deep" ) -type mockConfigManageriface interface { - Retrieve(tokens []string, config generator.GenVarsConfig) (generator.ParsedMap, error) - RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) - Insert(force bool) error -} +// type mockConfigManageriface interface { +// Retrieve(tokens []string, config generator.GenVarsConfig) (generator.ParsedMap, error) +// RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) +// Insert(force bool) error +// } -type mockGenVars struct{} +type mockGenerator struct { + generate func(tokens []string) (generator.ParsedMap, error) +} -var ( - testKey = "FOO#/test" - testVal = "val1" -) +// var ( +// testKey = "FOO#/test" +// testVal = "val1" +// ) -func (m *mockGenVars) Generate(tokens []string) (generator.ParsedMap, error) { +func (m *mockGenerator) Generate(tokens []string) (generator.ParsedMap, error) { + if m.generate != nil { + return m.generate(tokens) + } pm := generator.ParsedMap{} - pm[testKey] = testVal + pm["FOO#/test"] = "val1" + pm["ANOTHER://bar/quz"] = "fux" + pm["ZODTHER://bar/quz"] = "xuf" return pm, nil } -func (m *mockGenVars) ConvertToExportVar() []string { - return []string{} +type mockGenIface interface { + Generate(tokens []string) (generator.ParsedMap, error) } -func (m *mockGenVars) FlushToFile(w io.Writer, str []string) error { - return nil -} - -func (m *mockGenVars) StrToFile(w io.Writer, str string) error { - return nil -} - -func Test_retrieve(t *testing.T) { - tests := []struct { - name string +func Test_Retrieve_from_token_list(t *testing.T) { + tests := map[string]struct { tokens []string - genvar generator.Generatoriface + genvar mockGenIface expectKey string expectVal string }{ - { - name: "standard", - tokens: []string{"FOO#/test"}, - genvar: &mockGenVars{}, - expectKey: testKey, - expectVal: testVal, + "standard": { + tokens: []string{"FOO#/test", "ANOTHER://bar/quz", "ZODTHER://bar/quz"}, + genvar: &mockGenerator{}, + expectKey: "FOO#/test", + expectVal: "val1", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - pm, err := retrieve(tt.tokens, tt.genvar) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + cm := configmanager.New(context.TODO()) + cm.WithGenerator(tt.genvar) + pm, err := cm.Retrieve(tt.tokens) if err != nil { t.Errorf(testutils.TestPhrase, err, nil) } - for k, v := range pm { - if k != tt.expectKey { - t.Errorf(testutils.TestPhrase, k, tt.expectKey) - } - if v != tt.expectVal { - t.Errorf(testutils.TestPhrase, v, tt.expectVal) + if val, found := pm[tt.expectKey]; found { + if val != pm[tt.expectKey] { + t.Errorf(testutils.TestPhrase, val, tt.expectVal) } + } else { + t.Errorf(testutils.TestPhrase, "nil", tt.expectKey) } }) } @@ -80,7 +81,7 @@ func Test_retrieveWithInputReplaced(t *testing.T) { tests := map[string]struct { name string input string - genvar generator.Generatoriface + genvar mockGenIface expect string }{ "strYaml": { @@ -91,8 +92,9 @@ space: preserved // comments preserved arr: - "FOO#/test" + - ANOTHER://bar/quz `, - genvar: &mockGenVars{}, + genvar: &mockGenerator{}, expect: ` space: preserved indents: preserved @@ -100,6 +102,7 @@ space: preserved // comments preserved arr: - "val1" + - fux `, }, "strToml": { @@ -108,7 +111,7 @@ space: preserved [[somestuff]] key = "FOO#/test" `, - genvar: &mockGenVars{}, + genvar: &mockGenerator{}, expect: ` // TOML [[somestuff]] @@ -124,7 +127,7 @@ key2 = FOO#/test key3 = FOO#/test key4 = FOO#/test `, - genvar: &mockGenVars{}, + genvar: &mockGenerator{}, expect: ` // TOML [[somestuff]] @@ -146,7 +149,7 @@ export FOO4=FOO#/test foo23 = FOO#/test `, - genvar: &mockGenVars{}, + genvar: &mockGenerator{}, expect: ` export FOO='val1' export FOO1=val1 @@ -161,14 +164,16 @@ foo23 = val1 }, "escaped input": { input: `"{\"patchPayloadTemplate\":\"{\\\"password\\\":\\\"FOO#/test\\\",\\\"passwordConfirm\\\":\\\"FOO#/test\\\"}\\n\"}"`, - genvar: &mockGenVars{}, + genvar: &mockGenerator{}, expect: `"{\"patchPayloadTemplate\":\"{\\\"password\\\":\\\"val1\\\",\\\"passwordConfirm\\\":\\\"val1\\\"}\\n\"}"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := retrieveWithInputReplaced(tt.input, tt.genvar) + cm := configmanager.New(context.TODO()) + cm.WithGenerator(tt.genvar) + got, err := cm.RetrieveWithInputReplaced(tt.input) if err != nil { t.Errorf("failed with %v", err) } @@ -179,60 +184,43 @@ foo23 = val1 } } -func Test_replaceString(t *testing.T) { - tests := []struct { - name string - parsedMap generator.ParsedMap - inputStr string - expectStr string - }{ - { - name: "ordered correctly", - parsedMap: generator.ParsedMap{ - "AZKVSECRET#/test-vault/db-config|user": "foo", - "AZKVSECRET#/test-vault/db-config|pass": "bar", - "AZKVSECRET#/test-vault/db-config": fmt.Sprintf("%v", "{\"user\": \"foo\", \"pass\": \"bar\"}"), - }, - inputStr: `app: foo -db2: AZKVSECRET#/test-vault/db-config -db: - user: AZKVSECRET#/test-vault/db-config|user - pass: AZKVSECRET#/test-vault/db-config|pass -`, - expectStr: `app: foo -db2: {"user": "foo", "pass": "bar"} -db: - user: foo - pass: bar -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := replaceString(tt.parsedMap, tt.inputStr) - if got != tt.expectStr { - t.Errorf(testutils.TestPhrase, got, tt.expectStr) - } - }) - } -} - -type MockCfgMgr struct { - retrieveInput func(input string, config generator.GenVarsConfig) (string, error) - // retrieve func(input string, config generator.GenVarsConfig) (string, error) -} - -func (m *MockCfgMgr) RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) { - return m.retrieveInput(input, config) -} - -func (m *MockCfgMgr) Insert(force bool) error { - return nil -} - -func (m *MockCfgMgr) Retrieve(tokens []string, config generator.GenVarsConfig) (generator.ParsedMap, error) { - return nil, nil -} +// func Test_replaceString(t *testing.T) { +// tests := []struct { +// name string +// parsedMap generator.ParsedMap +// inputStr string +// expectStr string +// }{ +// { +// name: "ordered correctly", +// parsedMap: generator.ParsedMap{ +// "AZKVSECRET#/test-vault/db-config|user": "foo", +// "AZKVSECRET#/test-vault/db-config|pass": "bar", +// "AZKVSECRET#/test-vault/db-config": fmt.Sprintf("%v", "{\"user\": \"foo\", \"pass\": \"bar\"}"), +// }, +// inputStr: `app: foo +// db2: AZKVSECRET#/test-vault/db-config +// db: +// user: AZKVSECRET#/test-vault/db-config|user +// pass: AZKVSECRET#/test-vault/db-config|pass +// `, +// expectStr: `app: foo +// db2: {"user": "foo", "pass": "bar"} +// db: +// user: foo +// pass: bar +// `, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// got := replaceString(tt.parsedMap, tt.inputStr) +// if got != tt.expectStr { +// t.Errorf(testutils.TestPhrase, got, tt.expectStr) +// } +// }) +// } +// } type testSimpleStruct struct { Foo string `json:"foo" yaml:"foo"` @@ -259,139 +247,118 @@ const ( testTokenAWS = "AWSSECRETS:///bar/foo" ) -func Test_KubeControllerSpecHelper(t *testing.T) { - tests := []struct { - name string - testType testSimpleStruct - expect testSimpleStruct - cfmgr func(t *testing.T) mockConfigManageriface - }{ - { - name: "happy path simple struct", - testType: testSimpleStruct{ - Foo: testTokenAWS, - Bar: "quz", - }, - expect: testSimpleStruct{ - Foo: "baz", - Bar: "quz", - }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz"}`, nil - } - return mcm +var marshallTests = map[string]struct { + testType testNestedStruct + expect testNestedStruct + generator func(t *testing.T) *mockGenerator +}{ + "happy path complex struct complete": { + testType: testNestedStruct{ + Foo: testTokenAWS, + Bar: "quz", + Lol: testLol{ + Bla: "booo", + Another: testAnotherNEst{ + Number: 1235, + Float: 123.09, + }, }, }, - { - name: "happy path simple struct2", - testType: testSimpleStruct{ - Foo: "AWSSECRETS:///bar/foo2", - Bar: "quz", - }, - expect: testSimpleStruct{ - Foo: "baz2", - Bar: "quz", - }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz2","bar":"quz"}`, nil - } - return mcm + expect: testNestedStruct{ + Foo: "baz", + Bar: "quz", + Lol: testLol{ + Bla: "booo", + Another: testAnotherNEst{ + Number: 1235, + Float: 123.09, + }, }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - config := generator.NewConfig() - resp, err := KubeControllerSpecHelper(tt.testType, tt.cfmgr(t), *config) - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) + 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 } - if !reflect.DeepEqual(resp, &tt.expect) { - t.Error("") + return m + }, + }, + "complex struct - missing fields": { + testType: testNestedStruct{ + Foo: testTokenAWS, + 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 + }, + }, +} + +func Test_RetrieveMarshalledJson(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) }) } } -func Test_KubeControllerComplex(t *testing.T) { - tests := []struct { - name string - testType testNestedStruct - expect testNestedStruct - cfmgr func(t *testing.T) mockConfigManageriface - }{ - { - name: "happy path complex struct", - testType: testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, - expect: testNestedStruct{ - Foo: "baz", - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz", "lol":{"bla":"booo","another":{"number": 1235, "float": 123.09}}}`, nil - } - return mcm - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := generator.NewConfig().WithTokenSeparator("://") - got, err := KubeControllerSpecHelper(tt.testType, tt.cfmgr(t), *config) - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) - } - if !reflect.DeepEqual(got, &tt.expect) { - t.Errorf(testutils.TestPhraseWithContext, "returned types do not deep equal", got, tt.expect) - } +func Test_YamlRetrieveMarshalled(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) }) } } -func Test_YamlRetrieveMarshalled(t *testing.T) { - tests := []struct { - name string - testType *testNestedStruct - expect testNestedStruct - cfmgr func(t *testing.T) mockConfigManageriface +func MarhsalledHelper(t *testing.T, err error, input, expectOut any) { + t.Helper() + if err != nil { + t.Errorf(testutils.TestPhrase, err.Error(), nil) + } + if !reflect.DeepEqual(input, expectOut) { + t.Errorf(testutils.TestPhraseWithContext, "returned types do not deep equal", input, expectOut) + } +} + +func Test_YamlRetrieveUnmarshalled(t *testing.T) { + ttests := map[string]struct { + input []byte + expect testNestedStruct + generator func(t *testing.T) *mockGenerator }{ - { - name: "complex struct - complete", - testType: &testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, + "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", @@ -403,121 +370,55 @@ func Test_YamlRetrieveMarshalled(t *testing.T) { }, }, }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz", "lol":{"bla":"booo","another":{"number": 1235, "float": 123.09}}}`, nil + 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 mcm + return m }, }, - { - name: "complex struct - missing fields", - testType: &testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - }, + "complex struct - missing fields": { + input: []byte(`foo: AWSSECRETS:///bar/foo +bar: quz`), expect: testNestedStruct{ Foo: "baz", Bar: "quz", Lol: testLol{}, }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz", "lol":{"bla":"","another":{"number": 0, "float": 0}}}`, nil - } - return mcm - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := generator.NewConfig().WithTokenSeparator("://") - - got, err := RetrieveMarshalledYaml(tt.testType, tt.cfmgr(t), *config) - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) - } - if !reflect.DeepEqual(got, &tt.expect) { - t.Errorf(testutils.TestPhraseWithContext, "returned types do not deep equal", got, tt.expect) - } - }) - } -} - -func Test_YamlRetrieveMarshalled_errored(t *testing.T) { - tests := []struct { - name string - testType *testNestedStruct - expect error - cfmgr func(t *testing.T) mockConfigManageriface - }{ - { - name: "complex struct - complete", - testType: &testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, - // expect: testNestedStruct{ - // Foo: "baz", - // Bar: "quz", - // Lol: testLol{ - // Bla: "booo", - // Another: testAnotherNEst{ - // Number: 1235, - // Float: 123.09, - // }, - // }, - // }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return ``, fmt.Errorf("%s", "error decoding") + 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 mcm + return m }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := generator.NewConfig().WithTokenSeparator("://") - - _, err := RetrieveMarshalledYaml(tt.testType, tt.cfmgr(t), *config) - if err == nil { - t.Errorf(testutils.TestPhrase, nil, err.Error()) - } + 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_RetrieveMarshalledJson(t *testing.T) { - tests := []struct { - name string - testType *testNestedStruct - expect testNestedStruct - cfmgr func(t *testing.T) mockConfigManageriface +func Test_JsonRetrieveUnmarshalled(t *testing.T) { + tests := map[string]struct { + input []byte + expect testNestedStruct + generator func(t *testing.T) *mockGenerator }{ - { - name: "happy path complex struct complete", - testType: &testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - Lol: testLol{ - Bla: "booo", - Another: testAnotherNEst{ - Number: 1235, - Float: 123.09, - }, - }, - }, + "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", @@ -529,44 +430,42 @@ func Test_RetrieveMarshalledJson(t *testing.T) { }, }, }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz", "lol":{"bla":"booo","another":{"number": 1235, "float": 123.09}}}`, nil + 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 mcm + return m }, }, - { - name: "complex struct - missing fields", - testType: &testNestedStruct{ - Foo: testTokenAWS, - Bar: "quz", - }, + "complex struct - missing fields": { + input: []byte(`{"foo":"AWSSECRETS:///bar/foo","bar":"quz"}`), expect: testNestedStruct{ Foo: "baz", Bar: "quz", Lol: testLol{}, }, - cfmgr: func(t *testing.T) mockConfigManageriface { - mcm := &MockCfgMgr{} - mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { - return `{"foo":"baz","bar":"quz", "lol":{"bla":"","another":{"number": 0, "float": 0}}}`, nil + 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 mcm + return m }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := generator.NewConfig().WithTokenSeparator("://") - got, err := RetrieveMarshalledJson(tt.testType, tt.cfmgr(t), *config) - if err != nil { - t.Errorf(testutils.TestPhrase, err.Error(), nil) - } - if !reflect.DeepEqual(got, &tt.expect) { - t.Errorf(testutils.TestPhraseWithContext, "returned types do not deep equal", got, tt.expect) - } + 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) }) } } @@ -623,7 +522,7 @@ func TestFindTokens(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - got := FindTokens(tt.input) + got := configmanager.FindTokens(tt.input) sort.Strings(got) sort.Strings(tt.expect) @@ -633,3 +532,228 @@ func TestFindTokens(t *testing.T) { }) } } + +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) + // input := &testNestedStruct{} + // var errYaml = func() {} + // type failingMarshaler struct{} + err := c.RetrieveMarshalledYaml(&struct { + A int + B map[string]int ",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 { + expect config.GenVarsConfig + keySeparator, tokenSep, outputPath string + }{ + "default config": { + expect: config.NewConfig().Config(), + // keySeparator: "|", tokenSep: "://",outputPath:"", + }, + "outputPath overwritten only": { + expect: (config.NewConfig()).WithOutputPath("baresd").Config(), + // keySeparator: "|", tokenSep: "://", + outputPath: "baresd", + }, + "outputPath and keysep overwritten": { + expect: (config.NewConfig()).WithOutputPath("baresd").WithKeySeparator("##").Config(), + keySeparator: "##", + outputPath: "baresd", + // tokenSep: "://", + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + cm := configmanager.New(context.TODO()) + if tt.keySeparator != "" { + cm.Config.WithKeySeparator(tt.keySeparator) + } + if tt.tokenSep != "" { + cm.Config.WithTokenSeparator(tt.tokenSep) + } + if tt.outputPath != "" { + cm.Config.WithOutputPath(tt.outputPath) + } + got := cm.GeneratorConfig() + if diff := deep.Equal(got, &tt.expect); diff != nil { + t.Errorf(testutils.TestPhraseWithContext, "generator config", fmt.Sprintf("%q", got), fmt.Sprintf("%q", tt.expect)) + } + }) + } +} + +// func Test_KubeControllerSpecHelper(t *testing.T) { +// tests := []struct { +// name string +// testType testSimpleStruct +// expect testSimpleStruct +// cfmgr func(t *testing.T) mockConfigManageriface +// }{ +// { +// name: "happy path simple struct", +// testType: testSimpleStruct{ +// Foo: testTokenAWS, +// Bar: "quz", +// }, +// expect: testSimpleStruct{ +// Foo: "baz", +// Bar: "quz", +// }, +// cfmgr: func(t *testing.T) mockConfigManageriface { +// mcm := &MockCfgMgr{} +// mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { +// return `{"foo":"baz","bar":"quz"}`, nil +// } +// return mcm +// }, +// }, +// { +// name: "happy path simple struct2", +// testType: testSimpleStruct{ +// Foo: "AWSSECRETS:///bar/foo2", +// Bar: "quz", +// }, +// expect: testSimpleStruct{ +// Foo: "baz2", +// Bar: "quz", +// }, +// cfmgr: func(t *testing.T) mockConfigManageriface { +// mcm := &MockCfgMgr{} +// mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { +// return `{"foo":"baz2","bar":"quz"}`, nil +// } +// return mcm +// }, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { + +// config := generator.NewConfig() +// resp, err := KubeControllerSpecHelper(tt.testType, tt.cfmgr(t), *config) +// if err != nil { +// t.Errorf(testutils.TestPhrase, err.Error(), nil) +// } +// if !reflect.DeepEqual(resp, &tt.expect) { +// t.Error("") +// } +// }) +// } +// } + +// func Test_KubeControllerComplex(t *testing.T) { +// tests := []struct { +// name string +// testType testNestedStruct +// expect testNestedStruct +// cfmgr func(t *testing.T) mockConfigManageriface +// }{ +// { +// name: "happy path complex struct", +// testType: testNestedStruct{ +// Foo: testTokenAWS, +// Bar: "quz", +// Lol: testLol{ +// Bla: "booo", +// Another: testAnotherNEst{ +// Number: 1235, +// Float: 123.09, +// }, +// }, +// }, +// expect: testNestedStruct{ +// Foo: "baz", +// Bar: "quz", +// Lol: testLol{ +// Bla: "booo", +// Another: testAnotherNEst{ +// Number: 1235, +// Float: 123.09, +// }, +// }, +// }, +// cfmgr: func(t *testing.T) mockConfigManageriface { +// mcm := &MockCfgMgr{} +// mcm.retrieveInput = func(input string, config generator.GenVarsConfig) (string, error) { +// return `{"foo":"baz","bar":"quz", "lol":{"bla":"booo","another":{"number": 1235, "float": 123.09}}}`, nil +// } +// return mcm +// }, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// config := generator.NewConfig().WithTokenSeparator("://") +// got, err := KubeControllerSpecHelper(tt.testType, tt.cfmgr(t), *config) +// if err != nil { +// t.Errorf(testutils.TestPhrase, err.Error(), nil) +// } +// if !reflect.DeepEqual(got, &tt.expect) { +// t.Errorf(testutils.TestPhraseWithContext, "returned types do not deep equal", got, tt.expect) +// } +// }) +// } +// } diff --git a/examples/examples.go b/examples/examples.go index baf0110..facb274 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -1,28 +1,22 @@ -package main +package examples import ( + "context" "encoding/json" "fmt" "github.com/dnitsch/configmanager" - "github.com/dnitsch/configmanager/pkg/generator" ) const DO_STUFF_WITH_VALS_HERE = "connstring:user@%v:host=%s/someschema..." -func main() { - retrieveExample() - retrieveStringOut() - retrieveYaml() -} - // retrieveExample uses the standard Retrieve method on the API // this will return generator.ParsedMap which can be later used for more complex use cases func retrieveExample() { - cm := &configmanager.ConfigManager{} - cnf := generator.NewConfig() + cm := configmanager.New(context.TODO()) + cm.Config.WithTokenSeparator("://") - pm, err := cm.Retrieve([]string{"token1", "token2"}, *cnf) + pm, err := cm.Retrieve([]string{"token1", "token2"}) if err != nil { panic(err) @@ -40,8 +34,7 @@ func retrieveExample() { // retrieveStringOut accepts a string as an input func retrieveStringOut() { - cm := &configmanager.ConfigManager{} - cnf := generator.NewConfig() + cm := configmanager.New(context.TODO()) // JSON Marshal K8s CRD into exampleK8sCrdMarshalled := `apiVersion: crd.foo.custom/v1alpha1 kind: CustomFooCrd @@ -53,7 +46,7 @@ spec: secret_val: AWSSECRETS#/customfoo/secret-val owner: test_10016@example.com ` - pm, err := cm.RetrieveWithInputReplaced(exampleK8sCrdMarshalled, *cnf) + pm, err := cm.RetrieveWithInputReplaced(exampleK8sCrdMarshalled) if err != nil { panic(err) @@ -72,13 +65,11 @@ func SpecConfigTokenReplace[T any](inputType T) (*T, error) { return nil, err } - cm := configmanager.ConfigManager{} - + cm := configmanager.New(context.TODO()) // use custom token separator - // inline with - cnf := generator.NewConfig().WithTokenSeparator("://") + cm.Config.WithTokenSeparator("://") - replaced, err := cm.RetrieveWithInputReplaced(string(rawBytes), *cnf) + replaced, err := cm.RetrieveWithInputReplaced(string(rawBytes)) if err != nil { return nil, err } @@ -88,8 +79,9 @@ func SpecConfigTokenReplace[T any](inputType T) (*T, error) { return outType, nil } -// Example using a helper method -func retrieveYaml() { +// Example +func exampleRetrieveYamlUnmarshalled() { + type config struct { DbHost string `yaml:"dbhost"` Username string `yaml:"user"` @@ -101,10 +93,11 @@ pass: AWSPARAMSTR:///int-test/pocketbase/config|pwd dbhost: AWSPARAMSTR:///int-test/pocketbase/config|host ` - cm := &configmanager.ConfigManager{} + appConf := &config{} + cm := configmanager.New(context.TODO()) // use custom token separator inline with future releases - cmConf := generator.NewConfig().WithTokenSeparator("://") - appConf, err := configmanager.RetrieveUnmarshalledFromYaml([]byte(configMarshalled), &config{}, cm, *cmConf) + cm.Config.WithTokenSeparator("://") + err := cm.RetrieveUnmarshalledFromYaml([]byte(configMarshalled), appConf) if err != nil { panic(err) } @@ -112,3 +105,31 @@ dbhost: AWSPARAMSTR:///int-test/pocketbase/config|host 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) +} diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index 800f828..ba89c9e 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -1,6 +1,8 @@ -// command line utils -// testable methods that wrap around the low level -// implementation when invoked from the cli method. +// pacakge cmdutils +// +// Wraps around the ConfigManager library +// with additional postprocessing capabilities for +// output management when used with cli flags. package cmdutils import ( @@ -9,39 +11,52 @@ import ( "os" "strings" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/generator" "github.com/dnitsch/configmanager/pkg/log" ) -type confMgrRetrieveWithInputReplacediface interface { - RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) +type configManagerIface interface { + RetrieveWithInputReplaced(input string) (string, error) + Retrieve(tokens []string) (generator.ParsedMap, error) + GeneratorConfig() *config.GenVarsConfig +} + +type writerIface interface { + Write(p []byte) (n int, err error) + Close() error } type CmdUtils struct { - cfgmgr confMgrRetrieveWithInputReplacediface - generator generator.GenVarsiface + configManager configManagerIface + writer writerIface } -func New(gv generator.GenVarsiface, confManager confMgrRetrieveWithInputReplacediface) *CmdUtils { +func New(confManager configManagerIface) *CmdUtils { return &CmdUtils{ - cfgmgr: confManager, - generator: gv, + configManager: confManager, + writer: os.Stdout, // default writer } } +func (cmd *CmdUtils) WithWriter(w writerIface) *CmdUtils { + cmd.writer = w + return cmd +} + // GenerateFromTokens is a helper cmd method to call from retrieve command func (c *CmdUtils) GenerateFromCmd(tokens []string, output string) error { - w, err := writer(output) + err := c.setWriter(output) if err != nil { return err } - defer w.Close() - return c.generateFromToken(tokens, w) + defer c.writer.Close() + return c.generateFromToken(tokens) } // generateFromToken -func (c *CmdUtils) generateFromToken(tokens []string, w io.Writer) error { - pm, err := c.generator.Generate(tokens) +func (c *CmdUtils) generateFromToken(tokens []string) error { + pm, err := c.configManager.Retrieve(tokens) if err != nil { // return full error to terminal if no tokens were parsed if len(pm) < 1 { @@ -51,7 +66,9 @@ func (c *CmdUtils) generateFromToken(tokens []string, w io.Writer) error { log.Errorf("%e", err) } // Conver to ExportVars and flush to file - return c.generator.FlushToFile(w, c.generator.ConvertToExportVar()) + pp := &PostProcessor{ProcessedMap: pm, Config: c.configManager.GeneratorConfig()} + pp.ConvertToExportVar() + return pp.FlushOutToFile(c.writer) } // Generate a replaced string from string input command @@ -70,38 +87,37 @@ func (c *CmdUtils) GenerateStrOut(input, output string) error { } defer os.Remove(tempfile.Name()) log.Debugf("tmp file created: %s", tempfile.Name()) - outtmp, err := writer(tempfile.Name()) - if err != nil { + if err := c.setWriter(tempfile.Name()); err != nil { return err } - defer outtmp.Close() - return c.generateFromStrOutOverwrite(input, tempfile.Name(), outtmp) + defer c.writer.Close() + return c.generateFromStrOutOverwrite(input, tempfile.Name()) } - out, err := writer(output) + err := c.setWriter(output) if err != nil { return err } - defer out.Close() + defer c.writer.Close() - return c.generateFromStrOut(input, out) + return c.generateFromStrOut(input) } // generateFromStrOut -func (c *CmdUtils) generateFromStrOut(input string, output io.Writer) error { +func (c *CmdUtils) generateFromStrOut(input string) error { f, err := os.Open(input) if err != nil { if perr, ok := err.(*os.PathError); ok { log.Debugf("input is not a valid file path: %v, falling back on using the string directly", perr) // is actual string parse and write out to location - return c.generateStrOutFromInput(strings.NewReader(input), output) + return c.generateStrOutFromInput(strings.NewReader(input), c.writer) } return err } defer f.Close() - return c.generateStrOutFromInput(f, output) + return c.generateStrOutFromInput(f, c.writer) } // generateFromStrOutOverwrite uses the same file for input as output @@ -109,7 +125,7 @@ func (c *CmdUtils) generateFromStrOut(input string, output io.Writer) error { // and then write contents from temp to actual target // otherwise, two open file operations would be targeting same descriptor // will cause issues and inconsistent writes -func (c *CmdUtils) generateFromStrOutOverwrite(input, outtemp string, outtmp io.Writer) error { +func (c *CmdUtils) generateFromStrOutOverwrite(input, outtemp string) error { f, err := os.Open(input) if err != nil { @@ -117,16 +133,15 @@ func (c *CmdUtils) generateFromStrOutOverwrite(input, outtemp string, outtmp io. } defer f.Close() - if err := c.generateStrOutFromInput(f, outtmp); err != nil { + if err := c.generateStrOutFromInput(f, c.writer); err != nil { return err } tr, err := os.ReadFile(outtemp) if err != nil { return err } - // move temp file to output path - return os.WriteFile(c.generator.Config().OutputPath(), tr, 0644) + return os.WriteFile(c.configManager.GeneratorConfig().OutputPath(), tr, 0644) } // generateStrOutFromInput takes a reader and writer as input @@ -136,19 +151,25 @@ func (c *CmdUtils) generateStrOutFromInput(input io.Reader, output io.Writer) er if err != nil { return err } - str, err := c.cfgmgr.RetrieveWithInputReplaced(string(b), *c.generator.Config()) + str, err := c.configManager.RetrieveWithInputReplaced(string(b)) if err != nil { return err } + pp := &PostProcessor{} - return c.generator.StrToFile(output, str) + return pp.StrToFile(output, str) } -func writer(outputpath string) (*os.File, error) { - if outputpath == "stdout" { - return os.Stdout, nil +func (c *CmdUtils) setWriter(outputpath string) error { + // empty output path means StdOut + if outputpath != "stdout" { + f, err := os.OpenFile(outputpath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + c.writer = f } - return os.OpenFile(outputpath, os.O_WRONLY|os.O_CREATE, 0644) + return nil } // UploadTokensWithVals takes in a map of key/value pairs and uploads them diff --git a/internal/cmdutils/cmdutils_test.go b/internal/cmdutils/cmdutils_test.go index fef875b..75166cb 100644 --- a/internal/cmdutils/cmdutils_test.go +++ b/internal/cmdutils/cmdutils_test.go @@ -1,463 +1,278 @@ -package cmdutils +package cmdutils_test import ( "bytes" "fmt" "io" "os" + "strings" "testing" + "github.com/dnitsch/configmanager/internal/cmdutils" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" "github.com/dnitsch/configmanager/pkg/generator" ) -type mockGenVars struct { - mGen func(tokens []string) (generator.ParsedMap, error) - mConvertToExpVars func() []string - mFlushToFile func(w io.Writer, out []string) error - config *generator.GenVarsConfig - confOutputPath string +type mockCfgMgr struct { + parsedMap generator.ParsedMap + err error + parsedString string + config *config.GenVarsConfig } -var ( - tempOutPath = "" -) - -func (m *mockGenVars) Generate(tokens []string) (generator.ParsedMap, error) { - return m.mGen(tokens) -} - -func (m *mockGenVars) ConvertToExportVar() []string { - return m.mConvertToExpVars() -} - -func (m *mockGenVars) FlushToFile(w io.Writer, out []string) error { - return m.mFlushToFile(w, out) +func (m mockCfgMgr) RetrieveWithInputReplaced(input string) (string, error) { + return m.parsedString, m.err } -func (m *mockGenVars) StrToFile(w io.Writer, str string) error { - return generator.NewGenerator().StrToFile(w, str) +func (m mockCfgMgr) Retrieve(tokens []string) (generator.ParsedMap, error) { + return m.parsedMap, m.err } -func (m *mockGenVars) Config() *generator.GenVarsConfig { +func (m mockCfgMgr) GeneratorConfig() *config.GenVarsConfig { return m.config } -func (m *mockGenVars) ConfigOutputPath() string { - return m.confOutputPath -} - -type mockRetrieveWithInput func(input string, config generator.GenVarsConfig) (string, error) - -func (m mockRetrieveWithInput) RetrieveWithInputReplaced(input string, config generator.GenVarsConfig) (string, error) { - return m(input, config) -} - -func Test_generateStrOutFromInput(t *testing.T) { - tests := map[string]struct { - confmgrMock func(t *testing.T) confMgrRetrieveWithInputReplacediface - genMock func(t *testing.T) generator.GenVarsiface - w func([]byte) io.Writer - in string - expect string - }{ - "standard replace": { - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "pass=val1", nil - }) - }, - genMock: func(t *testing.T) generator.GenVarsiface { - gen := &mockGenVars{} - gen.config = generator.NewConfig() - - return gen - }, - in: "pass=FOO#/test", - expect: "pass=val1", - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - cu := &CmdUtils{ - cfgmgr: tt.confmgrMock(t), - generator: tt.genMock(t), - } - // want := []byte("pass=val1") - in := bytes.NewBuffer([]byte(tt.in)) - out := bytes.NewBuffer([]byte{}) - if err := cu.generateStrOutFromInput(in, out); err != nil { - t.Error(err) - } - got, err := io.ReadAll(out) - if err != nil { - t.Fatal(err) - } - if string(got) != tt.expect { - t.Errorf(testutils.TestPhrase, string(got), tt.expect) - } - }) +func Test_UploadTokens_errors(t *testing.T) { + m := &mockCfgMgr{} + cmd := cmdutils.New(m) + tokenMap := make(map[string]string) + if err := cmd.UploadTokensWithVals(tokenMap); err == nil { + t.Errorf(testutils.TestPhraseWithContext, "NOT YET IMPLEMENTED should fail", nil, "err") } } -func Test_generateFromStrOutOverwrite(t *testing.T) { - tests := map[string]struct { - confmgrMock func(t *testing.T) confMgrRetrieveWithInputReplacediface - genMock func(t *testing.T, out string) generator.GenVarsiface - w func([]byte) io.Writer - in string - expect string - }{ - "standard replace": { - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "pass=val1", nil - }) - }, - genMock: func(t *testing.T, out string) generator.GenVarsiface { - gen := &mockGenVars{} - gen.config = generator.NewConfig().WithOutputPath(out) - return gen - }, - in: "pass=FOO#/test", - expect: "pass=val1", - }, +func cmdTestHelper(t *testing.T, err error, got []byte, expect []string) { + t.Helper() + if err != nil { + t.Errorf("wanted file to not Error") } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - tempinfile, err := os.CreateTemp(os.TempDir(), "configmanager-mock-in") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tempinfile.Name()) - if err := os.WriteFile(tempinfile.Name(), []byte(tt.in), 0644); err != nil { - t.Fatal(err) - } - tempoutfile, err := os.CreateTemp(os.TempDir(), "configmanager-mock-out") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tempoutfile.Name()) - - cu := &CmdUtils{ - cfgmgr: tt.confmgrMock(t), - generator: tt.genMock(t, tempoutfile.Name()), - } - tempOutPath = tempoutfile.Name() - outwriter, err := writer(tempoutfile.Name()) - if err != nil { - t.Fatal(err) - } - if err := cu.generateFromStrOutOverwrite(tempinfile.Name(), tempoutfile.Name(), outwriter); err != nil { - t.Fatal(err) - } - got, err := os.ReadFile(tempoutfile.Name()) - if err != nil { - t.Fatal(err) - } - if string(got) != tt.expect { - t.Errorf(testutils.TestPhrase, string(got), tt.expect) - } - }) + if len(got) < 1 { + t.Error("empty file") + } + for _, want := range expect { + if !strings.Contains(string(got), want) { + t.Errorf(testutils.TestPhraseWithContext, "contents not found", string(got), want) + } } } -func TestGenerateStrOut(t *testing.T) { - type testReturn struct { - name string - isFile bool - inOutSame bool - } +func Test_GenerateFromCmd(t *testing.T) { ttests := map[string]struct { - input func() testReturn - output func() testReturn - confmgrMock func(t *testing.T) confMgrRetrieveWithInputReplacediface - genMock func(t *testing.T, out string) generator.GenVarsiface + mockMap generator.ParsedMap + tokens []string + expect []string }{ - "without overwrite": { - input: func() testReturn { - return testReturn{ - name: "token", - isFile: false, - } - }, - output: func() testReturn { - return testReturn{ - name: "replaced", - isFile: false, - } - }, - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "replaced", nil - }) - }, - genMock: func(t *testing.T, out string) generator.GenVarsiface { - gen := &mockGenVars{} - gen.confOutputPath = out - gen.config = generator.NewConfig().WithOutputPath(out) - return gen - }, - }, - "overwrite": { - input: func() testReturn { - tf, err := os.CreateTemp(os.TempDir(), "configmanager-mock-in") - if err != nil { - t.Fatal(err) - } - return testReturn{ - name: tf.Name(), - isFile: true, - inOutSame: true, - } - }, - output: func() testReturn { - tf, err := os.CreateTemp(os.TempDir(), "configmanager-mock-out") - if err != nil { - t.Fatal(err) - } - return testReturn{ - name: tf.Name(), - isFile: true, - } - }, - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "replaced", nil - }) - }, - genMock: func(t *testing.T, out string) generator.GenVarsiface { - gen := &mockGenVars{} - gen.confOutputPath = out - gen.config = generator.NewConfig().WithOutputPath(out) - return gen - }, + "succeeds with 3 tokens": { + generator.ParsedMap{"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'"}, }, } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - in, out := tt.input().name, tt.output().name - defer os.Remove(in) - defer os.Remove(tt.output().name) + // create a temp file + f, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-token*") + defer os.Remove(f.Name()) - if tt.input().inOutSame { - out = in + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedMap: tt.mockMap, } - cu := &CmdUtils{ - cfgmgr: tt.confmgrMock(t), - generator: tt.genMock(t, out), + cmd := cmdutils.New(m) + err := cmd.GenerateFromCmd(tt.tokens, f.Name()) + if err != nil { + t.Fatalf(testutils.TestPhraseWithContext, "generate from cmd tokens", err, nil) } - if err := cu.GenerateStrOut(in, out); err != nil { - t.Errorf(testutils.TestPhrase, err, nil) - } - // if tt.input().isFile { - // } - // if tt.output().isFile { - // } + got, err := io.ReadAll(f) + cmdTestHelper(t, err, got, tt.expect) }) } } -func Test_generateFromToken(t *testing.T) { - ttests := map[string]struct { - confmgrMock func(t *testing.T) confMgrRetrieveWithInputReplacediface - genMock func(t *testing.T) generator.GenVarsiface - tokens []string - w func([]byte) io.Writer - expect string - }{ - "success": { - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "replaced", nil - }) - }, - genMock: func(t *testing.T) generator.GenVarsiface { - gen := &mockGenVars{} - gen.config = generator.NewConfig() - gen.mGen = func(tokens []string) (generator.ParsedMap, error) { - gm := generator.ParsedMap{} - gm["foo"] = "replaced" - if tokens[0] != "foo" { - t.Errorf(testutils.TestPhrase, tokens[0], "foo") - } - return gm, nil - } - gen.mConvertToExpVars = func() []string { - return []string{"export FOO=replaced"} - } - gen.mFlushToFile = func(w io.Writer, out []string) error { - _, _ = w.Write([]byte("export FOO=replaced")) - return nil - } - return gen - }, - tokens: []string{"foo"}, - w: func(b []byte) io.Writer { - return bytes.NewBuffer(b) - }, - expect: "export FOO=replaced", - }, - "error": { - confmgrMock: func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "replaced", nil - }) - }, - genMock: func(t *testing.T) generator.GenVarsiface { - gen := &mockGenVars{} - gen.config = generator.NewConfig() - gen.mGen = func(tokens []string) (generator.ParsedMap, error) { - gm := generator.ParsedMap{} - return gm, fmt.Errorf("unable to generate secrets from tokens") - } - return gen - }, - tokens: []string{"foo"}, - w: func(b []byte) io.Writer { - return bytes.NewBuffer(b) - }, - expect: "unable to generate secrets from tokens", - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - b := new(bytes.Buffer) - cu := &CmdUtils{ - cfgmgr: tt.confmgrMock(t), - generator: tt.genMock(t), - } +type mockWriter struct { + w io.Writer +} - if err := cu.generateFromToken(tt.tokens, b); err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err, nil) - } - return - } - out, _ := io.ReadAll(b) - if len(out) < 1 { - t.Errorf(testutils.TestPhrase, out, "to not be be empty") - } - if string(out) != tt.expect { - t.Errorf(testutils.TestPhrase, string(out), tt.expect) - } - }) - } +func (m *mockWriter) Close() error { + return nil } -func TestGenerateFromCmd(t *testing.T) { - ttests := map[string]struct { - confmgrMock func(t *testing.T) confMgrRetrieveWithInputReplacediface - genMock func(t *testing.T) generator.GenVarsiface - tokens []string - output func(t *testing.T) string - expect string - }{ - "success to file": { - func(t *testing.T) confMgrRetrieveWithInputReplacediface { - return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - return "replaced", nil - }) - }, func(t *testing.T) generator.GenVarsiface { - gen := &mockGenVars{} - gen.config = generator.NewConfig() - gen.mGen = func(tokens []string) (generator.ParsedMap, error) { - gm := generator.ParsedMap{} - gm["foo"] = "replaced" - if tokens[0] != "foo" { - t.Errorf(testutils.TestPhrase, tokens[0], "foo") - } - return gm, nil - } - gen.mConvertToExpVars = func() []string { - return []string{"export FOO=replaced"} - } - gen.mFlushToFile = func(w io.Writer, out []string) error { - _, _ = w.Write([]byte("export FOO=replaced")) - return nil - } - return gen - }, - []string{"foo"}, - func(t *testing.T) string { - tempoutfile, err := os.CreateTemp(os.TempDir(), "configmanager-mock-out") - if err != nil { - t.Fatal(err) - } - return tempoutfile.Name() - }, - "export FOO=replaced", - }, - // "success to stdout": { - // func(t *testing.T) confMgrRetrieveWithInputReplacediface { - // return mockRetrieveWithInput(func(input string, config generator.GenVarsConfig) (string, error) { - // return "replaced", nil - // }) - // }, func(t *testing.T) generator.GenVarsiface { - // gen := &mockGenVars{} - // gen.config = generator.NewConfig() - // gen.mGen = func(tokens []string) (generator.ParsedMap, error) { - // gm := generator.ParsedMap{} - // gm["foo"] = "replaced" - // if tokens[0] != "foo" { - // t.Errorf(testutils.TestPhrase, tokens[0], "foo") - // } - // return gm, nil - // } - // gen.mConvertToExpVars = func() []string { - // return []string{"export FOO=replaced"} - // } - // gen.mFlushToFile = func(w io.Writer, out []string) error { - // _, _ = w.Write([]byte("export FOO=replaced")) - // return nil - // } - // return gen - // }, - // []string{"foo"}, - // func(t *testing.T) string { - // return "stdout" - // }, - // "export FOO=replaced", - // }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - outputPath := tt.output(t) - if outputPath != "stdout" { - defer os.Remove(outputPath) - } +func (m *mockWriter) Write(in []byte) (int, error) { + return m.w.Write(in) +} - cu := &CmdUtils{ - cfgmgr: tt.confmgrMock(t), - generator: tt.genMock(t), - } - if err := cu.GenerateFromCmd(tt.tokens, outputPath); err != nil { - if err.Error() != tt.expect { - t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) - } - return - } - }) - } +func Test_GenerateStrOut(t *testing.T) { + + inputStr := `FOO://bar/qusx FOO://bar/lorem FOO://bar/ducks` + mockParsedStr := `aksujg fooLorem Mighty` + expect := []string{"aksujg", "fooLorem", "Mighty"} + + t.Run("succeeds with input from string and output different", func(t *testing.T) { + tearDown, reader, file := func(t *testing.T) (func(), io.Reader, string) { + f, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + return func() { + os.Remove(f.Name()) + }, f, f.Name() + }(t) + defer tearDown() + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedString: mockParsedStr, + } + cmd := cmdutils.New(m) + err := cmd.GenerateStrOut(inputStr, file) + if err != nil { + t.Fatalf(testutils.TestPhraseWithContext, "generate from string", err, nil) + } + got, err := io.ReadAll(reader) + cmdTestHelper(t, err, got, expect) + }) + + t.Run("succeeds output set to stdout", func(t *testing.T) { + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedString: mockParsedStr, + } + cmd := cmdutils.New(m) + writer := bytes.NewBuffer([]byte{}) + mw := &mockWriter{w: writer} + cmd.WithWriter(mw) + err := cmd.GenerateStrOut(inputStr, "stdout") + if err != nil { + t.Fatalf(testutils.TestPhraseWithContext, "generate from string", err, nil) + } + got, err := io.ReadAll(writer) + cmdTestHelper(t, err, got, expect) + }) + t.Run("succeeds input and output are set to file names", func(t *testing.T) { + inputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + inputF.Write([]byte(inputStr)) + outputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + defer func() { + os.Remove(inputF.Name()) + os.Remove(outputF.Name()) + }() + + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedString: mockParsedStr, + } + cmd := cmdutils.New(m) + err := cmd.GenerateStrOut(inputF.Name(), outputF.Name()) + if err != nil { + t.Fatalf(testutils.TestPhraseWithContext, "generate from string", err, nil) + } + got, err := io.ReadAll(outputF) + cmdTestHelper(t, err, got, expect) + }) + + t.Run("succeeds input and output are set to the same file", func(t *testing.T) { + inputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + inputF.Write([]byte(inputStr)) + // outputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + defer func() { + os.Remove(inputF.Name()) + }() + + m := &mockCfgMgr{ + config: config.NewConfig().WithOutputPath(inputF.Name()), + parsedString: mockParsedStr, + } + cmd := cmdutils.New(m) + err := cmd.GenerateStrOut(inputF.Name(), inputF.Name()) + if err != nil { + t.Fatalf(testutils.TestPhraseWithContext, "generate from string", err, nil) + } + got, err := os.ReadFile(inputF.Name()) + cmdTestHelper(t, err, got, expect) + }) } -// func Test_generateFromStrOut(t *testing.T) { -// ttests := map[string]struct { -// objType any +func Test_CmdUtils_Errors_on(t *testing.T) { + t.Run("outputFile wrong", func(t *testing.T) { + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedMap: generator.ParsedMap{"FOO://bar/qusx": "aksujg", "FOO://bar/lorem": "", "FOO://bar/ducks": "sdhbjk0293"}, + } + + cmd := cmdutils.New(m) + if err := cmd.GenerateFromCmd([]string{"IMNP://foo"}, "xunknown/file"); err == nil { + t.Errorf(testutils.TestPhraseWithContext, "file does not exist unable to create a writer", "err", nil) + } + }) + t.Run("REtrieve from tokens in fetching ANY of the tokens", func(t *testing.T) { + m := &mockCfgMgr{ + config: config.NewConfig(), + parsedMap: generator.ParsedMap{}, + err: fmt.Errorf("err in fetching tokens"), + } + + cmd := cmdutils.New(m) + writer := bytes.NewBuffer([]byte{}) + mw := &mockWriter{w: writer} + cmd.WithWriter(mw) + if err := cmd.GenerateFromCmd([]string{"IMNP://foo"}, "stdout"); err == nil { + t.Errorf(testutils.TestPhraseWithContext, "NOT fetching ANY tokens should error", "err", nil) + } + }) -// }{ -// "test1": -// { -// objType: nil, + 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"}, + err: fmt.Errorf("err in fetching tokens"), + } -// }, -// } -// for name, tt := range ttests { -// t.Run(name, func(t *testing.T) { + cmd := cmdutils.New(m) + writer := bytes.NewBuffer([]byte{}) + mw := &mockWriter{w: writer} + cmd.WithWriter(mw) + if err := cmd.GenerateFromCmd([]string{"IMNP://foo", "IMNP://foo2"}, "stdout"); err != nil { + t.Errorf(testutils.TestPhraseWithContext, "fetching tokens some erroring should only be logged out", "err", nil) + } + }) -// }) -// } -// } + 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"}, + parsedString: `bar `, + err: fmt.Errorf("err in fetching tokens"), + } + + cmd := cmdutils.New(m) + writer := bytes.NewBuffer([]byte{}) + mw := &mockWriter{w: writer} + cmd.WithWriter(mw) + if err := cmd.GenerateStrOut(`"IMNP://foo", "IMNP://foo2"`, "stdout"); err == nil { + t.Errorf(testutils.TestPhraseWithContext, "fetching tokens some erroring should only be logged out", nil, "err") + } + }) + + t.Run("REtrieve from string in fetching SOME of the tokens with input/output the same", func(t *testing.T) { + inputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + inputF.Write([]byte(`"IMNP://foo", "IMNP://foo2"`)) + // outputF, _ := os.CreateTemp(os.TempDir(), "gen-conf-frrom-string*") + defer func() { + os.Remove(inputF.Name()) + }() + + m := &mockCfgMgr{ + config: config.NewConfig().WithOutputPath(inputF.Name()), + parsedString: `bar `, + err: fmt.Errorf("err in fetching tokens"), + } + + cmd := cmdutils.New(m) + writer := bytes.NewBuffer([]byte{}) + mw := &mockWriter{w: writer} + cmd.WithWriter(mw) + if err := cmd.GenerateStrOut(inputF.Name(), inputF.Name()); err == nil { + t.Errorf(testutils.TestPhraseWithContext, "fetching tokens some erroring should only be logged out", nil, "err") + } + }) +} diff --git a/internal/cmdutils/postprocessor.go b/internal/cmdutils/postprocessor.go new file mode 100644 index 0000000..b853649 --- /dev/null +++ b/internal/cmdutils/postprocessor.go @@ -0,0 +1,95 @@ +package cmdutils + +import ( + "fmt" + "io" + "strings" + + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/pkg/generator" +) + +// PostProcessor +// processes the rawMap and outputs the result +// depending on cmdline options +type PostProcessor struct { + ProcessedMap generator.ParsedMap + Config *config.GenVarsConfig + outString []string +} + +// ConvertToExportVar assigns the k/v out +// as unix style export key=val pairs separated by `\n` +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 := make(generator.ParsedMap) + if parsedOk := generator.IsParsed(v, &trm); parsedOk { + // if is a map + // try look up on key if separator defined + normMap := p.envVarNormalize(trm) + p.exportVars(normMap) + continue + } + p.exportVars(generator.ParsedMap{topLevelKey: v}) + } + return p.outString +} + +// envVarNormalize +func (p *PostProcessor) envVarNormalize(pmap generator.ParsedMap) generator.ParsedMap { + normalizedMap := make(generator.ParsedMap) + for k, v := range pmap { + normalizedMap[p.normalizeKey(k)] = v + } + return normalizedMap +} + +func (p *PostProcessor) exportVars(exportMap generator.ParsedMap) { + + for k, v := range exportMap { + // NOTE: \n line ending is not totally cross platform + t := fmt.Sprintf("%T", v) + switch t { + case "string": + p.outString = append(p.outString, fmt.Sprintf("export %s='%s'", p.normalizeKey(k), v)) + default: + p.outString = append(p.outString, fmt.Sprintf("export %s=%v", p.normalizeKey(k), v)) + } + } +} + +// normalizeKeys returns env var compatible key +func (p *PostProcessor) normalizeKey(k string) string { + // the order of replacer pairs matters less + // as the Replace builds a node tree without overlapping matches + replacer := strings.NewReplacer([]string{" ", "", "@", "", "!", "", "-", "_", p.Config.KeySeparator(), "__"}...) + return strings.ToUpper(replacer.Replace(k)) +} + +// FlushOutToFile saves contents to file provided +// in the config input into the generator +// default location is ./app.env +// +// can also be to stdout or another file location +func (p *PostProcessor) FlushOutToFile(w io.Writer) error { + return p.flushToFile(w, listToString(p.outString)) +} + +// StrToFile writes a provided string to the writer +func (p *PostProcessor) StrToFile(w io.Writer, str string) error { + return p.flushToFile(w, str) +} + +func (p *PostProcessor) flushToFile(f io.Writer, str string) error { + _, e := f.Write([]byte(str)) + if e != nil { + return e + } + return nil +} + +func listToString(strList []string) string { + return strings.Join(strList, "\n") +} diff --git a/internal/cmdutils/postprocessor_test.go b/internal/cmdutils/postprocessor_test.go new file mode 100644 index 0000000..eb61c2f --- /dev/null +++ b/internal/cmdutils/postprocessor_test.go @@ -0,0 +1,77 @@ +package cmdutils_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/dnitsch/configmanager/internal/cmdutils" + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/internal/testutils" + "github.com/dnitsch/configmanager/pkg/generator" +) + +func postprocessorHelper(t *testing.T) { + t.Helper() + +} +func Test_ConvertToExportVars(t *testing.T) { + tests := map[string]struct { + rawMap generator.ParsedMap + 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}, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + pp := cmdutils.PostProcessor{ProcessedMap: tt.rawMap, Config: config.NewConfig()} + got := pp.ConvertToExportVar() + + if got == nil { + t.Errorf(testutils.TestPhrase, got, "not nil") + } + if len(got) != tt.expectLength { + t.Errorf(testutils.TestPhrase, len(got), tt.expectLength) + } + st := strings.Join(got, "\n") + if !strings.Contains(st, tt.expectStr) { + t.Errorf(testutils.TestPhrase, st, tt.expectStr) + } + + // check FlushToFile + tw := bytes.NewBuffer([]byte{}) + pp.FlushOutToFile(tw) + readBuffer := tw.Bytes() + if len(readBuffer) == 0 { + t.Errorf(testutils.TestPhraseWithContext, "buffer should be filled", string(readBuffer), tt.expectStr) + } + + }) + } +} + +func Test_StrToWriter(t *testing.T) { + ttests := map[string]struct { + input string + }{ + "matches": {`export FOO=BAR`}, + "multiline": {`export FOO=BAR\nBUX=GED`}, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + want := tt.input + tw := bytes.NewBuffer([]byte{}) + pp := cmdutils.PostProcessor{} + pp.StrToFile(tw, tt.input) + readBuffer := tw.Bytes() + if string(readBuffer) != want { + t.Errorf(testutils.TestPhraseWithContext, "incorrectly written buffer stream", string(readBuffer), want) + } + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 22a3d8f..ebb2f4b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,5 +1,263 @@ package config +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + const ( SELF_NAME = "configmanager" ) + +const ( + // tokenSeparator used for identifying the end of a prefix and beginning of token + // see notes about special consideration for AZKVSECRET tokens + tokenSeparator = "://" + // keySeparator used for accessing nested objects within the retrieved map + keySeparator = "|" +) + +type ImplementationPrefix string + +const ( + // AWS SecretsManager prefix + SecretMgrPrefix ImplementationPrefix = "AWSSECRETS" + // AWS Parameter Store prefix + ParamStorePrefix ImplementationPrefix = "AWSPARAMSTR" + // Azure Key Vault Secrets prefix + AzKeyVaultSecretsPrefix ImplementationPrefix = "AZKVSECRET" + // Azure Key Vault Secrets prefix + AzTableStorePrefix ImplementationPrefix = "AZTABLESTORE" + // Azure App Config prefix + AzAppConfigPrefix ImplementationPrefix = "AZAPPCONF" + // Hashicorp Vault prefix + HashicorpVaultPrefix ImplementationPrefix = "VAULT" + // GcpSecrets + GcpSecretsPrefix ImplementationPrefix = "GCPSECRETS" + // Unknown + UnknownPrefix ImplementationPrefix = "UNKNOWN" +) + +var ( + // default varPrefix used by the replacer function + // any token must beging with one of these else + // it will be skipped as not a replaceable token + VarPrefix = map[ImplementationPrefix]bool{ + SecretMgrPrefix: true, ParamStorePrefix: true, AzKeyVaultSecretsPrefix: true, + GcpSecretsPrefix: true, HashicorpVaultPrefix: true, AzTableStorePrefix: true, + AzAppConfigPrefix: true, UnknownPrefix: true, + } +) + +// GenVarsConfig defines the input config object to be passed +type GenVarsConfig struct { + outpath string + tokenSeparator string + keySeparator string + // parseAdditionalVars func(token string) TokenConfigVars +} + +// NewConfig +func NewConfig() *GenVarsConfig { + return &GenVarsConfig{ + tokenSeparator: tokenSeparator, + keySeparator: keySeparator, + } +} + +// WithOutputPath +func (c *GenVarsConfig) WithOutputPath(out string) *GenVarsConfig { + c.outpath = out + return c +} + +// WithTokenSeparator adds a custom token separator +// token is the actual value of the parameter/secret in the +// provider store +func (c *GenVarsConfig) WithTokenSeparator(tokenSeparator string) *GenVarsConfig { + c.tokenSeparator = tokenSeparator + return c +} + +// WithKeySeparator adds a custom key separotor +func (c *GenVarsConfig) WithKeySeparator(keySeparator string) *GenVarsConfig { + c.keySeparator = keySeparator + return c +} + +// OutputPath returns the outpath set in the config +func (c *GenVarsConfig) OutputPath() string { + return c.outpath +} + +// TokenSeparator returns the tokenSeparator set in the config +func (c *GenVarsConfig) TokenSeparator() string { + return c.tokenSeparator +} + +// KeySeparator returns the keySeparator set in the config +func (c *GenVarsConfig) KeySeparator() string { + return c.keySeparator +} + +// Config returns the derefed value +func (c *GenVarsConfig) Config() GenVarsConfig { + cc := *c + return cc +} + +// Parsed token config section + +var ErrInvalidTokenPrefix = errors.New("token prefix has no implementation") + +type ParsedTokenConfig struct { + prefix ImplementationPrefix + keySeparator, tokenSeparator string + prefixLessToken, fullToken string + metadataStr, keysPath string + storeToken, metadataLess 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("%w", ErrInvalidTokenPrefix) + } + + ptc.keySeparator = config.keySeparator + ptc.tokenSeparator = config.tokenSeparator + ptc.prefix = ImplementationPrefix(prfx) + ptc.fullToken = token + return ptc.new(), nil +} + +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) + + // token without metadata and the string itself + ptc.extractMetadataStr() + // token without keys + ptc.keysLookup() + return ptc +} + +func (t *ParsedTokenConfig) ParseMetadata(typ any) error { + // 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.Split(t.metadataStr, ",") { + mapKeyVal := strings.Split(keyVal, "=") + if len(mapKeyVal) == 2 { + 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 + b := []byte(fmt.Sprintf(`{%s}`, strings.Join(metaMap, ","))) + if err := json.Unmarshal(b, typ); 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) 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 +func (t *ParsedTokenConfig) StoreToken() string { + return t.storeToken +} + +// Full returns the full Token path. +// Including key separator and metadata values +func (t *ParsedTokenConfig) String() string { + return t.fullToken +} + +func (t *ParsedTokenConfig) LookupKeys() string { + return t.keysPath +} + +func (t *ParsedTokenConfig) Prefix() ImplementationPrefix { + return t.prefix +} + +const ( + startMetaStr string = `[` + endMetaStr string = `]` +) + +// 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):] + + 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.Replace(token, startMetaStr+metaString+endMetaStr, "", -1) +} + +// 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 +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7631c07..0767e62 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,11 @@ -package config +package config_test -import "testing" +import ( + "testing" + + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/internal/testutils" +) func Test_SelfName(t *testing.T) { tests := []struct { @@ -12,9 +17,138 @@ func Test_SelfName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.name != SELF_NAME { + if tt.name != config.SELF_NAME { t.Error("self name does not match") } }) } } + +func Test_MarshalMetadata_with_label_struct_succeeds(t *testing.T) { + type labelMeta struct { + Label string `json:"label"` + } + + ttests := map[string]struct { + config *config.GenVarsConfig + rawToken string + wantLabel string + wantMetaStrippedToken string + }{ + "when provider expects label on token and label exists": { + config.NewConfig().WithTokenSeparator("://"), + `AZTABLESTORE://basjh/dskjuds/123|d88[label=dev]`, + "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]`, + "", + "basjh/dskjuds/123", + }, + "no metadata found": { + config.NewConfig().WithTokenSeparator("://"), + `AZTABLESTORE://basjh/dskjuds/123|d88`, + "", + "basjh/dskjuds/123", + }, + "no metadata found incorrect marker placement": { + config.NewConfig().WithTokenSeparator("://"), + `AZTABLESTORE://basjh/dskjuds/123|d88]asdas=bar[`, + "", + "basjh/dskjuds/123", + }, + "no metadata found incorrect marker placement and no key separator": { + config.NewConfig().WithTokenSeparator("://"), + `AZTABLESTORE://basjh/dskjuds/123]asdas=bar[`, + "", + "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]`, + "", + "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) + } + + if got == nil { + t.Errorf(testutils.TestPhraseWithContext, "Unable to parse token", nil, config.ParsedTokenConfig{}) + } + + got.ParseMetadata(inputTyp) + + if got.StoreToken() != tt.wantMetaStrippedToken { + t.Errorf(testutils.TestPhraseWithContext, "Token does not match", got.StripMetadata(), tt.wantMetaStrippedToken) + } + + if inputTyp.Label != tt.wantLabel { + t.Errorf(testutils.TestPhraseWithContext, "Metadata Label does not match", inputTyp.Label, tt.wantLabel) + } + }) + } +} + +func Test_TokenParser_config(t *testing.T) { + type mockConfAwsSecrMgr struct { + Version string `json:"version"` + } + ttests := map[string]struct { + input string + expPrefix config.ImplementationPrefix + expLookupKeys string + expStoreToken string + 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"}, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + conf := &mockConfAwsSecrMgr{} + got, _ := config.NewParsedTokenConfig(tt.input, *config.NewConfig()) + got.ParseMetadata(conf) + + if got.LookupKeys() != tt.expLookupKeys { + t.Errorf(testutils.TestPhrase, got.LookupKeys(), tt.expLookupKeys) + } + if got.StoreToken() != tt.expStoreToken { + t.Errorf(testutils.TestPhrase, got.StoreToken(), tt.expLookupKeys) + } + if got.String() != tt.expString { + t.Errorf(testutils.TestPhrase, got.String(), tt.expString) + } + if got.Prefix() != tt.expPrefix { + t.Errorf(testutils.TestPhrase, got.Prefix(), tt.expPrefix) + } + if conf.Version != tt.expMetadataVersion { + t.Errorf(testutils.TestPhrase, conf.Version, tt.expMetadataVersion) + } + }) + } +} diff --git a/pkg/generator/azappconf.go b/internal/store/azappconf.go similarity index 66% rename from pkg/generator/azappconf.go rename to internal/store/azappconf.go index 273d37c..118a3a6 100644 --- a/pkg/generator/azappconf.go +++ b/internal/store/azappconf.go @@ -1,7 +1,7 @@ /** * Azure App Config implementation **/ -package generator +package store import ( "context" @@ -11,6 +11,7 @@ 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/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" ) @@ -21,10 +22,11 @@ type appConfApi interface { } type AzAppConf struct { - svc appConfApi - ctx context.Context - token string - config *AzAppConfConfig + svc appConfApi + ctx context.Context + config *AzAppConfConfig + token *config.ParsedTokenConfig + strippedToken string } // AzAppConfConfig is the azure conf service specific config @@ -36,16 +38,16 @@ type AzAppConfConfig struct { } // NewAzAppConf -func NewAzAppConf(ctx context.Context, token string, conf GenVarsConfig) (*AzAppConf, error) { +func NewAzAppConf(ctx context.Context, token *config.ParsedTokenConfig) (*AzAppConf, error) { storeConf := &AzAppConfConfig{} - initialToken := ParseMetadata(token, storeConf) + token.ParseMetadata(storeConf) backingStore := &AzAppConf{ ctx: ctx, config: storeConf, + token: token, } - - srvInit := azServiceFromToken(stripPrefix(initialToken, AzAppConfigPrefix, conf.TokenSeparator(), conf.KeySeparator()), "https://%s.azconfig.io", 1) - backingStore.token = srvInit.token + srvInit := azServiceFromToken(token.StoreToken(), "https://%s.azconfig.io", 1) + backingStore.strippedToken = srvInit.token cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -65,15 +67,15 @@ func NewAzAppConf(ctx context.Context, token string, conf GenVarsConfig) (*AzApp } // setTokenVal sets the token -func (implmt *AzAppConf) setTokenVal(token string) {} +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) tokenVal(v *retrieveStrategy) (string, error) { +func (imp *AzAppConf) Token() (string, error) { log.Info("Concrete implementation AzAppConf") - log.Infof("AzAppConf Token: %s", imp.token) + log.Infof("AzAppConf Token: %s", imp.token.String()) ctx, cancel := context.WithCancel(imp.ctx) defer cancel() @@ -88,14 +90,14 @@ func (imp *AzAppConf) tokenVal(v *retrieveStrategy) (string, error) { opts.OnlyIfChanged = imp.config.Etag } - s, err := imp.svc.GetSetting(ctx, imp.token, opts) + s, err := imp.svc.GetSetting(ctx, imp.strippedToken, opts) if err != nil { - log.Errorf(implementationNetworkErr, AzAppConfigPrefix, err, imp.token) - return "", fmt.Errorf("token: %s, error: %v. %w", imp.token, err, ErrRetrieveFailed) + log.Errorf(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 } - log.Errorf("token: %v, %w", imp.token, ErrEmptyResponse) + log.Errorf("token: %v, %w", imp.token.String(), ErrEmptyResponse) return "", nil } diff --git a/pkg/generator/azappconf_test.go b/internal/store/azappconf_test.go similarity index 83% rename from pkg/generator/azappconf_test.go rename to internal/store/azappconf_test.go index 6174fae..838a161 100644 --- a/pkg/generator/azappconf_test.go +++ b/internal/store/azappconf_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -7,6 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" ) @@ -33,11 +34,12 @@ func (m mockAzAppConfApi) GetSetting(ctx context.Context, key string, options *a } func Test_AzAppConf_Success(t *testing.T) { + tsuccessParam := "somecvla" tests := map[string]struct { token string expect string mockClient func(t *testing.T) appConfApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "successVal": { "AZAPPCONF#/test-app-config-instance/table//token/1", @@ -50,7 +52,7 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, "successVal with :// token Separator": { "AZAPPCONF:///test-app-config-instance/conf_key[label=dev]", @@ -63,7 +65,7 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), }, "successVal with :// token Separator and etag specified": { "AZAPPCONF:///test-app-config-instance/conf_key[label=dev,etag=sometifdsssdsfdi_string01209222]", @@ -79,7 +81,7 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), }, "successVal with keyseparator but no val returned": { "AZAPPCONF#/test-app-config-instance/try_to_find|key_separator.lookup", @@ -92,21 +94,21 @@ func Test_AzAppConf_Success(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := NewAzAppConf(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + + impl, err := NewAzAppConf(context.TODO(), token) if err != nil { t.Errorf("failed to init AZAPPCONF") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -127,7 +129,7 @@ func Test_AzAppConf_Error(t *testing.T) { token string expect error mockClient func(t *testing.T) appConfApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "errored on service method call": { "AZAPPCONF#/test-app-config-instance/table/token/ok", @@ -139,20 +141,19 @@ func Test_AzAppConf_Error(t *testing.T) { return resp, fmt.Errorf("network error") }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := NewAzAppConf(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + impl, err := NewAzAppConf(context.TODO(), token) if err != nil { t.Fatal("failed to init AZAPPCONF") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - if _, err := rs.getTokenValue(); !errors.Is(err, tt.expect) { + if _, err := impl.Token(); !errors.Is(err, tt.expect) { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) } }) @@ -162,7 +163,9 @@ func Test_AzAppConf_Error(t *testing.T) { func Test_fail_AzAppConf_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 - _, err := NewAzAppConf(context.TODO(), "/%25%65%6e%301-._~/") } diff --git a/pkg/generator/azhelpers.go b/internal/store/azhelpers.go similarity index 98% rename from pkg/generator/azhelpers.go rename to internal/store/azhelpers.go index f7c92b1..7b85387 100644 --- a/pkg/generator/azhelpers.go +++ b/internal/store/azhelpers.go @@ -1,4 +1,4 @@ -package generator +package store import ( "fmt" diff --git a/pkg/generator/azkeyvault.go b/internal/store/azkeyvault.go similarity index 62% rename from pkg/generator/azkeyvault.go rename to internal/store/azkeyvault.go index 72b804d..ca8a795 100644 --- a/pkg/generator/azkeyvault.go +++ b/internal/store/azkeyvault.go @@ -1,13 +1,14 @@ /** * Azure KeyVault implementation **/ -package generator +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/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" ) @@ -16,10 +17,11 @@ type kvApi interface { } type KvScrtStore struct { - svc kvApi - ctx context.Context - token string - config *AzKvConfig + svc kvApi + ctx context.Context + token *config.ParsedTokenConfig + config *AzKvConfig + strippedToken string } // AzKvConfig takes any metadata from the token @@ -30,18 +32,19 @@ type AzKvConfig struct { // NewKvScrtStore returns a KvScrtStore // requires `AZURE_SUBSCRIPTION_ID` environment variable to be present to successfully work -func NewKvScrtStore(ctx context.Context, token string, conf GenVarsConfig) (*KvScrtStore, error) { +func NewKvScrtStore(ctx context.Context, token *config.ParsedTokenConfig) (*KvScrtStore, error) { storeConf := &AzKvConfig{} + token.ParseMetadata(storeConf) - initialToken := ParseMetadata(token, storeConf) backingStore := &KvScrtStore{ ctx: ctx, config: storeConf, + token: token, } - srvInit := azServiceFromToken(stripPrefix(initialToken, AzKeyVaultSecretsPrefix, conf.TokenSeparator(), conf.KeySeparator()), "https://%s.vault.azure.net", 1) - backingStore.token = srvInit.token + srvInit := azServiceFromToken(token.StoreToken(), "https://%s.vault.azure.net", 1) + backingStore.strippedToken = srvInit.token cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { @@ -61,20 +64,20 @@ func NewKvScrtStore(ctx context.Context, token string, conf GenVarsConfig) (*KvS } // setToken already happens in AzureKVClient in the constructor -func (implmt *KvScrtStore) setTokenVal(token string) {} +func (implmt *KvScrtStore) SetToken(token *config.ParsedTokenConfig) {} -func (imp *KvScrtStore) tokenVal(v *retrieveStrategy) (string, error) { - log.Infof("%s", "Concrete implementation AzKeyVault Secret") - log.Infof("AzKeyVault Token: %s", imp.token) +func (imp *KvScrtStore) Token() (string, error) { + log.Info("Concrete implementation AzKeyVault Secret") + log.Infof("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.token, imp.config.Version, nil) + s, err := imp.svc.GetSecret(ctx, imp.strippedToken, imp.config.Version, nil) if err != nil { - log.Errorf(implementationNetworkErr, AzKeyVaultSecretsPrefix, err, imp.token) + log.Errorf(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) return "", err } if s.Value != nil { diff --git a/pkg/generator/azkeyvault_test.go b/internal/store/azkeyvault_test.go similarity index 87% rename from pkg/generator/azkeyvault_test.go rename to internal/store/azkeyvault_test.go index 8580d8a..a67c30c 100644 --- a/pkg/generator/azkeyvault_test.go +++ b/internal/store/azkeyvault_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -7,6 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" ) @@ -74,7 +75,7 @@ func azKvCommonGetSecretChecker(t *testing.T, name, version, expectedName string t.Errorf("incorrectly stripped token separator") } - if strings.Contains(name, string(AzKeyVaultSecretsPrefix)) { + if strings.Contains(name, string(config.AzKeyVaultSecretsPrefix)) { t.Errorf("incorrectly stripped prefix") } @@ -90,12 +91,12 @@ func (m mockAzKvSecretApi) GetSecret(ctx context.Context, name string, version s } func TestAzKeyVault(t *testing.T) { - + tsuccessParam := "dssdfdweiuyh" tests := map[string]struct { token string expect string mockClient func(t *testing.T) kvApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "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) { @@ -105,7 +106,7 @@ func TestAzKeyVault(t *testing.T) { resp.Value = &tsuccessParam return resp, nil }) - }, NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + }, 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) { @@ -115,7 +116,7 @@ func TestAzKeyVault(t *testing.T) { resp.Value = &tsuccessParam return resp, nil }) - }, NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + }, 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) { @@ -127,7 +128,7 @@ func TestAzKeyVault(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, "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) { @@ -138,7 +139,7 @@ func TestAzKeyVault(t *testing.T) { return resp, fmt.Errorf("unable to retrieve secret") }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, "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) { @@ -149,21 +150,21 @@ func TestAzKeyVault(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := NewKvScrtStore(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + + impl, err := NewKvScrtStore(context.TODO(), token) if err != nil { t.Errorf("failed to init azkvstore") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/pkg/generator/aztablestorage.go b/internal/store/aztablestorage.go similarity index 72% rename from pkg/generator/aztablestorage.go rename to internal/store/aztablestorage.go index 4521f35..103ee19 100644 --- a/pkg/generator/aztablestorage.go +++ b/internal/store/aztablestorage.go @@ -1,7 +1,7 @@ /** * Azure TableStore implementation **/ -package generator +package store import ( "context" @@ -12,6 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" ) @@ -26,8 +27,11 @@ type tableStoreApi interface { type AzTableStore struct { svc tableStoreApi ctx context.Context - token string config *AzTableStrgConfig + token *config.ParsedTokenConfig + // token only without table indicators + // key only + strippedToken string } type AzTableStrgConfig struct { @@ -35,17 +39,19 @@ type AzTableStrgConfig struct { } // NewAzTableStore -func NewAzTableStore(ctx context.Context, token string, conf GenVarsConfig) (*AzTableStore, error) { +func NewAzTableStore(ctx context.Context, token *config.ParsedTokenConfig) (*AzTableStore, error) { storeConf := &AzTableStrgConfig{} - initialToken := ParseMetadata(token, storeConf) + token.ParseMetadata(storeConf) + // initialToken := config.ParseMetadata(token, storeConf) backingStore := &AzTableStore{ ctx: ctx, config: storeConf, + token: token, } - srvInit := azServiceFromToken(stripPrefix(initialToken, AzTableStorePrefix, conf.TokenSeparator(), conf.KeySeparator()), "https://%s.table.core.windows.net/%s", 2) - backingStore.token = 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,34 +67,33 @@ func NewAzTableStore(ctx context.Context, token string, conf GenVarsConfig) (*Az backingStore.svc = c return backingStore, nil - } // setToken already happens in the constructor -func (implmt *AzTableStore) setTokenVal(token string) {} +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) tokenVal(v *retrieveStrategy) (string, error) { +func (imp *AzTableStore) Token() (string, error) { log.Info("Concrete implementation AzTableSTore") - log.Infof("AzTableSTore Token: %s", imp.token) + log.Infof("AzTableSTore Token: %s", imp.token.String()) ctx, cancel := context.WithCancel(imp.ctx) defer cancel() // split the token for partition and rowKey - pKey, rKey, err := azTableStoreTokenSplitter(imp.token) + pKey, rKey, err := azTableStoreTokenSplitter(imp.strippedToken) if err != nil { return "", err } s, err := imp.svc.GetEntity(ctx, pKey, rKey, &aztables.GetEntityOptions{}) if err != nil { - log.Errorf(implementationNetworkErr, AzTableStorePrefix, err, imp.token) - return "", fmt.Errorf(implementationNetworkErr+" %w", AzTableStorePrefix, err, imp.token, ErrRetrieveFailed) + log.Errorf(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 diff --git a/pkg/generator/aztablestorage_test.go b/internal/store/aztablestorage_test.go similarity index 84% rename from pkg/generator/aztablestorage_test.go rename to internal/store/aztablestorage_test.go index 053e57c..60da809 100644 --- a/pkg/generator/aztablestorage_test.go +++ b/internal/store/aztablestorage_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -8,6 +8,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/data/aztables" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" ) @@ -20,7 +21,7 @@ func azTableStoreCommonChecker(t *testing.T, partitionKey, rowKey, expectedParti t.Errorf(testutils.TestPhrase, partitionKey, expectedPartitionKey) } - if strings.Contains(partitionKey, string(AzKeyVaultSecretsPrefix)) { + if strings.Contains(partitionKey, string(config.AzTableStorePrefix)) { t.Errorf("incorrectly stripped prefix") } @@ -41,27 +42,27 @@ func Test_AzTableStore_Success(t *testing.T) { token string expect string mockClient func(t *testing.T) tableStoreApi - config *GenVarsConfig + config *config.GenVarsConfig }{ - "successVal": {"AZTABLESTORE#/test-account/table//token/1", tsuccessParam, func(t *testing.T) tableStoreApi { + "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) + resp.Value = []byte("tsuccessParam") return resp, nil }) - }, NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + }, config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, - "successVal with :// token Separator": {"AZTABLESTORE:///test-account/table//token/1", tsuccessParam, func(t *testing.T) tableStoreApi { + "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) + resp.Value = []byte("tsuccessParam") return resp, nil }) - }, NewConfig().WithKeySeparator("|").WithTokenSeparator("://"), + }, 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) { @@ -73,21 +74,20 @@ func Test_AzTableStore_Success(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := NewAzTableStore(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + impl, err := NewAzTableStore(context.TODO(), token) if err != nil { t.Errorf("failed to init aztablestore") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -103,12 +103,12 @@ func Test_AzTableStore_Success(t *testing.T) { } func Test_azstorage_with_value_property(t *testing.T) { - conf := NewConfig().WithKeySeparator("|").WithTokenSeparator("://") + conf := config.NewConfig().WithKeySeparator("|").WithTokenSeparator("://") ttests := map[string]struct { token string expect string mockClient func(t *testing.T) tableStoreApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "return value property with json like object": { "AZTABLESTORE:///test-account/table/partitionkey/rowKey|host", @@ -161,15 +161,16 @@ func Test_azstorage_with_value_property(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - impl, err := NewAzTableStore(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + + impl, err := NewAzTableStore(context.TODO(), token) if err != nil { t.Fatal("failed to init aztablestore") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + + got, err := impl.Token() if err != nil { t.Fatalf(testutils.TestPhrase, err.Error(), nil) } @@ -187,7 +188,7 @@ func Test_AzTableStore_Error(t *testing.T) { token string expect error mockClient func(t *testing.T) tableStoreApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "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) { @@ -196,7 +197,7 @@ func Test_AzTableStore_Error(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + 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) { @@ -205,7 +206,7 @@ func Test_AzTableStore_Error(t *testing.T) { return resp, fmt.Errorf("network error") }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, "empty": {"AZTABLESTORE#/test-vault/token/1|somekey", ErrIncorrectlyStructuredToken, func(t *testing.T) tableStoreApi { @@ -215,21 +216,21 @@ func Test_AzTableStore_Error(t *testing.T) { return resp, nil }) }, - NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), + config.NewConfig().WithKeySeparator("|").WithTokenSeparator("#"), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - impl, err := NewAzTableStore(context.TODO(), tt.token, *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + + impl, err := NewAzTableStore(context.TODO(), token) if err != nil { t.Fatal("failed to init aztablestore") } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - if _, err := rs.getTokenValue(); !errors.Is(err, tt.expect) { + if _, err := impl.Token(); !errors.Is(err, tt.expect) { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) } }) @@ -239,7 +240,9 @@ 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 - _, err := NewAzTableStore(context.TODO(), "/%25%65%6e%301-._~/") } diff --git a/pkg/generator/gcpsecrets.go b/internal/store/gcpsecrets.go similarity index 70% rename from pkg/generator/gcpsecrets.go rename to internal/store/gcpsecrets.go index ddd71f2..805a859 100644 --- a/pkg/generator/gcpsecrets.go +++ b/internal/store/gcpsecrets.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -6,6 +6,7 @@ import ( gcpsecrets "cloud.google.com/go/secretmanager/apiv1" gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" "github.com/googleapis/gax-go/v2" ) @@ -19,7 +20,7 @@ type GcpSecrets struct { ctx context.Context config *GcpSecretsConfig close func() error - token string + token *config.ParsedTokenConfig } type GcpSecretsConfig struct { @@ -32,8 +33,6 @@ func NewGcpSecrets(ctx context.Context) (*GcpSecrets, error) { if err != nil { return nil, err } - // defer c.Close() - return &GcpSecrets{ svc: c, ctx: ctx, @@ -41,17 +40,19 @@ func NewGcpSecrets(ctx context.Context) (*GcpSecrets, error) { }, nil } -func (imp *GcpSecrets) setTokenVal(token string) { +func (imp *GcpSecrets) SetToken(token *config.ParsedTokenConfig) { storeConf := &GcpSecretsConfig{} - initialToken := ParseMetadata(token, storeConf) - + token.ParseMetadata(storeConf) + imp.token = token imp.config = storeConf - imp.token = initialToken } -func (imp *GcpSecrets) tokenVal(v *retrieveStrategy) (string, error) { +func (imp *GcpSecrets) Token() (string, error) { + // Close client currently as new one would be created per iteration defer imp.close() - log.Infof("%s", "Concrete implementation GcpSecrets") + + log.Info("Concrete implementation GcpSecrets") + log.Infof("GcpSecrets Token: %s", imp.token.String()) version := "latest" if imp.config.Version != "" { @@ -61,7 +62,7 @@ func (imp *GcpSecrets) tokenVal(v *retrieveStrategy) (string, error) { log.Infof("Getting Secret: %s @version: %s", imp.token, version) input := &gcpsecretspb.AccessSecretVersionRequest{ - Name: fmt.Sprintf("%s/versions/%s", v.stripPrefix(imp.token, GcpSecretsPrefix), version), + Name: fmt.Sprintf("%s/versions/%s", imp.token.StoreToken(), version), } ctx, cancel := context.WithCancel(imp.ctx) @@ -70,7 +71,7 @@ func (imp *GcpSecrets) tokenVal(v *retrieveStrategy) (string, error) { result, err := imp.svc.AccessSecretVersion(ctx, input) if err != nil { - log.Errorf(implementationNetworkErr, GcpSecretsPrefix, err, imp.token) + log.Errorf(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) return "", err } if result.Payload != nil { diff --git a/pkg/generator/gcpsecrets_test.go b/internal/store/gcpsecrets_test.go similarity index 84% rename from pkg/generator/gcpsecrets_test.go rename to internal/store/gcpsecrets_test.go index b188426..2ad4cb8 100644 --- a/pkg/generator/gcpsecrets_test.go +++ b/internal/store/gcpsecrets_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -8,6 +8,7 @@ import ( "testing" gcpsecretspb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" "github.com/googleapis/gax-go/v2" ) @@ -50,6 +51,7 @@ func fixtureInitMockClient() struct { } return resp } + func gcpSecretsGetChecker(t *testing.T, req *gcpsecretspb.AccessSecretVersionRequest) { if req.Name == "" { t.Fatal("expect name to not be nil") @@ -57,7 +59,7 @@ func gcpSecretsGetChecker(t *testing.T, req *gcpsecretspb.AccessSecretVersionReq if strings.Contains(req.Name, "#") { t.Errorf("incorrectly stripped token separator") } - if strings.Contains(req.Name, string(GcpSecretsPrefix)) { + if strings.Contains(req.Name, string(config.GcpSecretsPrefix)) { t.Errorf("incorrectly stripped prefix") } } @@ -67,7 +69,7 @@ func Test_GetGcpSecretVarHappy(t *testing.T) { token string expect string mockClient func(t *testing.T) gcpSecretsApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "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) { @@ -77,9 +79,9 @@ func Test_GetGcpSecretVarHappy(t *testing.T) { Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, }, nil }) - }, NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), }, - "success with version": {"GCPSECRETS#/token/1[version:123]", "someValue", func(t *testing.T) gcpSecretsApi { + "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) { t.Helper() gcpSecretsGetChecker(t, req) @@ -87,25 +89,25 @@ func Test_GetGcpSecretVarHappy(t *testing.T) { Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, }, nil }) - }, NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), }, "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) { - t.Helper() gcpSecretsGetChecker(t, req) return nil, fmt.Errorf("unable to retrieve secret") }) - }, NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + }, config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), }, - "found but empty": {"GCPSECRETS#/token/1", "someValue", func(t *testing.T) gcpSecretsApi { - return mockGcpSecretsApi(func(ctx context.Context, req *gcpsecretspb.AccessSecretVersionRequest, opts ...gax.CallOption) (*gcpsecretspb.AccessSecretVersionResponse, error) { - t.Helper() - gcpSecretsGetChecker(t, req) - return &gcpsecretspb.AccessSecretVersionResponse{ - Payload: &gcpsecretspb.SecretPayload{Data: []byte("someValue")}, - }, nil - }) - }, NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), + "found but empty": { + "GCPSECRETS#/token/1", + "", + 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{}, nil + }) + }, + config.NewConfig().WithTokenSeparator("#").WithKeySeparator("|"), }, } for name, tt := range tests { @@ -115,6 +117,8 @@ 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()) if err != nil { t.Errorf(testutils.TestPhrase, err.Error(), nil) @@ -122,12 +126,8 @@ func Test_GetGcpSecretVarHappy(t *testing.T) { impl.svc = tt.mockClient(t) impl.close = func() error { return nil } - - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - - rs.setImplementation(impl) - rs.setTokenVal(tt.token) - got, err := rs.getTokenValue() + impl.SetToken(token) + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { diff --git a/pkg/generator/hashivault.go b/internal/store/hashivault.go similarity index 84% rename from pkg/generator/hashivault.go rename to internal/store/hashivault.go index 006c355..997655d 100644 --- a/pkg/generator/hashivault.go +++ b/internal/store/hashivault.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" vault "github.com/hashicorp/vault/api" @@ -26,10 +27,11 @@ type hashiVaultApi interface { } type VaultStore struct { - svc hashiVaultApi - ctx context.Context - config *VaultConfig - token string + svc hashiVaultApi + ctx context.Context + config *VaultConfig + token *config.ParsedTokenConfig + strippedToken string } // VaultConfig holds the parseable metadata struct @@ -38,17 +40,18 @@ type VaultConfig struct { Role string `json:"iam_role"` } -func NewVaultStore(ctx context.Context, token string, conf GenVarsConfig) (*VaultStore, error) { +func NewVaultStore(ctx context.Context, token *config.ParsedTokenConfig) (*VaultStore, error) { storeConf := &VaultConfig{} - initialToken := ParseMetadata(token, storeConf) + token.ParseMetadata(storeConf) imp := &VaultStore{ ctx: ctx, config: storeConf, + token: token, } config := vault.DefaultConfig() - vt := splitToken(stripPrefix(initialToken, HashicorpVaultPrefix, conf.TokenSeparator(), conf.KeySeparator())) - imp.token = vt.token + vt := splitToken(token.StoreToken()) + imp.strippedToken = vt.token client, err := vault.NewClient(config) if err != nil { return nil, fmt.Errorf("unable to initialize Vault client: %v", err) @@ -97,21 +100,21 @@ func newVaultStoreWithAWSAuthIAM(client *vault.Client, role string) (*vault.Clie // 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) setTokenVal(token string) {} +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) tokenVal(v *retrieveStrategy) (string, error) { +func (imp *VaultStore) Token() (string, error) { log.Infof("%s", "Concrete implementation HashiVault") log.Infof("Getting Secret: %s", imp.token) ctx, cancel := context.WithCancel(imp.ctx) defer cancel() - secret, err := imp.getSecret(ctx, v.stripPrefix(imp.token, HashicorpVaultPrefix), imp.config.Version) + secret, err := imp.getSecret(ctx, imp.strippedToken, imp.config.Version) if err != nil { - log.Errorf(implementationNetworkErr, HashicorpVaultPrefix, err, imp.token) + log.Errorf(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) return "", err } diff --git a/pkg/generator/hashivault_test.go b/internal/store/hashivault_test.go similarity index 85% rename from pkg/generator/hashivault_test.go rename to internal/store/hashivault_test.go index 9734131..75a4eaf 100644 --- a/pkg/generator/hashivault_test.go +++ b/internal/store/hashivault_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" vault "github.com/hashicorp/vault/api" ) @@ -28,8 +29,8 @@ func TestMountPathExtract(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - strippedToken := stripPrefix(tt.token, HashicorpVaultPrefix, tt.tokenSeparator, tt.keySeparator) - got := splitToken(strippedToken) + token, _ := config.NewParsedTokenConfig(tt.token, *config.NewConfig().WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) + got := splitToken(token.StoreToken()) if got.path != tt.expect { t.Errorf("got %q, expected %q", got, tt.expect) } @@ -53,12 +54,12 @@ func (m mockVaultApi) GetVersion(ctx context.Context, secretPath string, version func TestVaultScenarios(t *testing.T) { ttests := map[string]struct { token string - conf GenVarsConfig + conf *config.GenVarsConfig expect string mockClient func(t *testing.T) hashiVaultApi setupEnv func() func() }{ - "happy return": {"VAULT://secret___/foo", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `{"foo":"test2130-9sd-0ds"}`, + "happy return": {"VAULT://secret___/foo", config.NewConfig(), `{"foo":"test2130-9sd-0ds"}`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { @@ -79,7 +80,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "incorrect json": {"VAULT://secret___/foo", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `json: unsupported type: func() error`, + "incorrect json": {"VAULT://secret___/foo", config.NewConfig(), `json: unsupported type: func() error`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { @@ -102,7 +103,7 @@ func TestVaultScenarios(t *testing.T) { }, "another return": { "VAULT://secret/engine1___/some/other/foo2", - GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, + config.NewConfig(), `{"foo1":"test2130-9sd-0ds","foo2":"dsfsdf3454456"}`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} @@ -125,7 +126,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "not found": {"VAULT://secret___/foo", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `secret not found`, + "not found": {"VAULT://secret___/foo", config.NewConfig(), `secret not found`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { @@ -144,7 +145,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "403": {"VAULT://secret___/some/other/foo2", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `client 403`, + "403": {"VAULT://secret___/some/other/foo2", config.NewConfig(), `client 403`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { @@ -163,7 +164,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "found but empty": {"VAULT://secret___/some/other/foo2", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `{}`, func(t *testing.T) hashiVaultApi { + "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() @@ -182,7 +183,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "found but nil returned": {"VAULT://secret___/some/other/foo2", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, "", func(t *testing.T) hashiVaultApi { + "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() @@ -200,7 +201,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "version provided correctly": {"VAULT://secret___/some/other/foo2[version=1]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, `{"foo2":"dsfsdf3454456"}`, func(t *testing.T) hashiVaultApi { + "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() @@ -220,7 +221,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "version provided but unable to parse": {"VAULT://secret___/some/other/foo2[version=1a]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, "unable to parse version into an integer: strconv.Atoi: parsing \"1a\": invalid syntax", func(t *testing.T) hashiVaultApi { + "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() @@ -238,7 +239,7 @@ func TestVaultScenarios(t *testing.T) { } }, }, - "vault rate limit incorrect": {"VAULT://secret___/some/other/foo2", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, "unable to initialize Vault client: error encountered setting up default configuration: VAULT_RATE_LIMIT was provided but incorrectly formatted", func(t *testing.T) hashiVaultApi { + "vault rate limit incorrect": {"VAULT://secret___/some/other/foo2", config.NewConfig(), "unable to initialize Vault client: error encountered setting up default configuration: VAULT_RATE_LIMIT was provided but incorrectly formatted", func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} mv.g = func(ctx context.Context, secretPath string) (*vault.KVSecret, error) { t.Helper() @@ -263,7 +264,9 @@ func TestVaultScenarios(t *testing.T) { t.Run(name, func(t *testing.T) { tearDown := tt.setupEnv() defer tearDown() - impl, err := NewVaultStore(context.TODO(), tt.token, tt.conf) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.conf) + + impl, err := NewVaultStore(context.TODO(), token) if err != nil { if err.Error() != tt.expect { t.Fatalf("failed to init hashivault, %v", err.Error()) @@ -272,9 +275,7 @@ func TestVaultScenarios(t *testing.T) { } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), tt.conf) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) @@ -291,14 +292,14 @@ func TestVaultScenarios(t *testing.T) { func TestAwsIamAuth(t *testing.T) { ttests := map[string]struct { token string - conf GenVarsConfig + conf *config.GenVarsConfig expect string mockClient func(t *testing.T) hashiVaultApi 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]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, + "VAULT://secret___/some/other/foo2[version:1]", config.NewConfig(), "role provided is empty, EC2 auth not supported", func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} @@ -326,7 +327,7 @@ func TestAwsIamAuth(t *testing.T) { }, }, "aws_iam auth incorrectly formatted request": { - "VAULT://secret___/some/other/foo2[version=1,iam_role=not_a_role]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, + "VAULT://secret___/some/other/foo2[version=1,iam_role=not_a_role]", config.NewConfig(), `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 @@ -367,7 +368,7 @@ 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]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, + "VAULT://secret___/some/other/foo2[iam_role=arn:aws:iam::1111111:role/i-orchestration]", config.NewConfig(), `{"foo2":"dsfsdf3454456"}`, func(t *testing.T) hashiVaultApi { mv := mockVaultApi{} @@ -404,7 +405,7 @@ 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]", GenVarsConfig{tokenSeparator: "://", keySeparator: "|"}, + "VAULT://secret___/some/other/foo2[iam_role=arn:aws:iam::1111111:role/i-orchestration]", config.NewConfig(), `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 { mv := mockVaultApi{} @@ -448,7 +449,9 @@ incorrect values supplied. failed to initialize the client`, ts := httptest.NewServer(tt.mockHanlder(t)) tearDown := tt.setupEnv(ts.URL) defer tearDown() - impl, err := NewVaultStore(context.TODO(), tt.token, tt.conf) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.conf) + + impl, err := NewVaultStore(context.TODO(), token) if err != nil { // WHAT A CRAP way to do this... if err.Error() != strings.Split(fmt.Sprintf(tt.expect, ts.URL), `%!`)[0] { @@ -459,9 +462,7 @@ incorrect values supplied. failed to initialize the client`, } impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), tt.conf) - rs.setImplementation(impl) - got, err := rs.getTokenValue() + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/pkg/generator/paramstore.go b/internal/store/paramstore.go similarity index 68% rename from pkg/generator/paramstore.go rename to internal/store/paramstore.go index ce070e3..826a188 100644 --- a/pkg/generator/paramstore.go +++ b/internal/store/paramstore.go @@ -1,11 +1,12 @@ -package generator +package store import ( "context" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" + awsConf "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" ) @@ -17,7 +18,7 @@ type ParamStore struct { svc paramStoreApi ctx context.Context config *ParamStrConfig - token string + token *config.ParsedTokenConfig } type ParamStrConfig struct { @@ -25,7 +26,7 @@ type ParamStrConfig struct { } func NewParamStore(ctx context.Context) (*ParamStore, error) { - cfg, err := config.LoadDefaultConfig(ctx) + cfg, err := awsConf.LoadDefaultConfig(ctx) if err != nil { log.Errorf("unable to load SDK config, %v", err) return nil, err @@ -38,20 +39,19 @@ func NewParamStore(ctx context.Context) (*ParamStore, error) { }, nil } -func (imp *ParamStore) setTokenVal(token string) { +func (imp *ParamStore) SetToken(token *config.ParsedTokenConfig) { storeConf := &ParamStrConfig{} - initialToken := ParseMetadata(token, storeConf) - + token.ParseMetadata(storeConf) + imp.token = token imp.config = storeConf - imp.token = initialToken } -func (imp *ParamStore) tokenVal(v *retrieveStrategy) (string, error) { +func (imp *ParamStore) Token() (string, error) { log.Infof("%s", "Concrete implementation ParameterStore") - log.Infof("ParamStore Token: %s", imp.token) + log.Infof("ParamStore Token: %s", imp.token.String()) input := &ssm.GetParameterInput{ - Name: aws.String(v.stripPrefix(imp.token, ParamStorePrefix)), + Name: aws.String(imp.token.StoreToken()), WithDecryption: aws.Bool(true), } ctx, cancel := context.WithCancel(imp.ctx) @@ -59,7 +59,7 @@ func (imp *ParamStore) tokenVal(v *retrieveStrategy) (string, error) { result, err := imp.svc.GetParameter(ctx, input) if err != nil { - log.Errorf(implementationNetworkErr, ParamStorePrefix, err, imp.token) + log.Errorf(implementationNetworkErr, config.ParamStorePrefix, err, imp.token.StoreToken()) return "", err } diff --git a/pkg/generator/paramstore_test.go b/internal/store/paramstore_test.go similarity index 81% rename from pkg/generator/paramstore_test.go rename to internal/store/paramstore_test.go index 3a140ce..41c7355 100644 --- a/pkg/generator/paramstore_test.go +++ b/internal/store/paramstore_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -8,13 +8,14 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" ) -var ( - tsuccessParam = "someVal" - tsuccessObj map[string]string = map[string]string{"AWSPARAMSTR#/token/1": "someVal"} -) +// 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) @@ -31,7 +32,7 @@ func awsParamtStoreCommonGetChecker(t *testing.T, params *ssm.GetParameterInput) t.Errorf("incorrectly stripped token separator") } - if strings.Contains(*params.Name, string(ParamStorePrefix)) { + if strings.Contains(*params.Name, string(config.ParamStorePrefix)) { t.Errorf("incorrectly stripped prefix") } @@ -41,13 +42,17 @@ func awsParamtStoreCommonGetChecker(t *testing.T, params *ssm.GetParameterInput) } 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 string keySeparator string tokenSeparator string expect string mockClient func(t *testing.T) paramStoreApi - config *GenVarsConfig + config *config.GenVarsConfig }{ "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) { @@ -57,7 +62,7 @@ func Test_GetParamStore(t *testing.T) { Parameter: &types.Parameter{Value: &tsuccessParam}, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, "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) { @@ -72,7 +77,7 @@ func Test_GetParamStore(t *testing.T) { Parameter: &types.Parameter{Value: &tsuccessParam}, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, "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) { @@ -80,7 +85,7 @@ func Test_GetParamStore(t *testing.T) { awsParamtStoreCommonGetChecker(t, params) return nil, fmt.Errorf("unable to retrieve") }) - }, NewConfig(), + }, config.NewConfig(), }, "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) { @@ -90,21 +95,21 @@ func Test_GetParamStore(t *testing.T) { Parameter: &types.Parameter{Value: nil}, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) + + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) + impl, err := NewParamStore(context.TODO()) if err != nil { t.Errorf(testutils.TestPhrase, err.Error(), nil) } impl.svc = tt.mockClient(t) - rs.setImplementation(impl) - rs.setTokenVal(tt.token) - got, err := rs.getTokenValue() + impl.SetToken(token) + got, err := impl.Token() if err != nil { if err.Error() != tt.expect { t.Errorf(testutils.TestPhrase, err.Error(), tt.expect) diff --git a/pkg/generator/secretsmanager.go b/internal/store/secretsmanager.go similarity index 70% rename from pkg/generator/secretsmanager.go rename to internal/store/secretsmanager.go index 87cb1f8..7d0c856 100644 --- a/pkg/generator/secretsmanager.go +++ b/internal/store/secretsmanager.go @@ -1,11 +1,12 @@ -package generator +package store import ( "context" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" + awsConf "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/pkg/log" ) @@ -17,7 +18,7 @@ type SecretsMgr struct { svc secretsMgrApi ctx context.Context config *SecretsMgrConfig - token string + token *config.ParsedTokenConfig } type SecretsMgrConfig struct { @@ -25,7 +26,7 @@ type SecretsMgrConfig struct { } func NewSecretsMgr(ctx context.Context) (*SecretsMgr, error) { - cfg, err := config.LoadDefaultConfig(ctx) + cfg, err := awsConf.LoadDefaultConfig(ctx) if err != nil { log.Errorf("unable to load SDK config, %v", err) return nil, err @@ -39,17 +40,16 @@ func NewSecretsMgr(ctx context.Context) (*SecretsMgr, error) { } -func (imp *SecretsMgr) setTokenVal(token string) { +func (imp *SecretsMgr) SetToken(token *config.ParsedTokenConfig) { storeConf := &SecretsMgrConfig{} - initialToken := ParseMetadata(token, storeConf) - + token.ParseMetadata(storeConf) + imp.token = token imp.config = storeConf - imp.token = initialToken } -func (imp *SecretsMgr) tokenVal(v *retrieveStrategy) (string, error) { - - log.Infof("%s", "Concrete implementation SecretsManager") +func (imp *SecretsMgr) Token() (string, error) { + log.Infof("Concrete implementation SecretsManager") + log.Infof("SecretsManager Token: %s", imp.token.String()) version := "AWSCURRENT" if imp.config.Version != "" { @@ -59,7 +59,7 @@ func (imp *SecretsMgr) tokenVal(v *retrieveStrategy) (string, error) { log.Infof("Getting Secret: %s @version: %s", imp.token, version) input := &secretsmanager.GetSecretValueInput{ - SecretId: aws.String(v.stripPrefix(imp.token, SecretMgrPrefix)), + SecretId: aws.String(imp.token.StoreToken()), VersionStage: aws.String(version), } @@ -68,7 +68,7 @@ func (imp *SecretsMgr) tokenVal(v *retrieveStrategy) (string, error) { result, err := imp.svc.GetSecretValue(ctx, input) if err != nil { - log.Errorf(implementationNetworkErr, SecretMgrPrefix, err, imp.token) + log.Errorf(implementationNetworkErr, imp.token.Prefix(), err, imp.token.String()) return "", err } diff --git a/pkg/generator/secretsmanager_test.go b/internal/store/secretsmanager_test.go similarity index 80% rename from pkg/generator/secretsmanager_test.go rename to internal/store/secretsmanager_test.go index 627eb1a..bd65761 100644 --- a/pkg/generator/secretsmanager_test.go +++ b/internal/store/secretsmanager_test.go @@ -1,4 +1,4 @@ -package generator +package store import ( "context" @@ -7,13 +7,10 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/dnitsch/configmanager/internal/config" "github.com/dnitsch/configmanager/internal/testutils" ) -var ( - tsuccessSecret = "someVal" -) - 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) { @@ -29,21 +26,23 @@ func awsSecretsMgrGetChecker(t *testing.T, params *secretsmanager.GetSecretValue t.Errorf("incorrectly stripped token separator") } - if strings.Contains(*params.SecretId, string(SecretMgrPrefix)) { + 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 string keySeparator string tokenSeparator string expect string mockClient func(t *testing.T) secretsMgrApi - config *GenVarsConfig + config *config.GenVarsConfig }{ - "success": {"AWSSECRETS#/token/1", "|", "#", tsuccessParam, func(t *testing.T) secretsMgrApi { + "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) @@ -51,9 +50,9 @@ func Test_GetSecretMgr(t *testing.T) { SecretString: &tsuccessSecret, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, - "success with version": {"AWSSECRETS#/token/1[version:123]", "|", "#", tsuccessParam, func(t *testing.T) secretsMgrApi { + "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) @@ -61,17 +60,17 @@ func Test_GetSecretMgr(t *testing.T) { SecretString: &tsuccessSecret, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, - "success with binary": {"AWSSECRETS#/token/1", "|", "#", tsuccessParam, func(t *testing.T) secretsMgrApi { + "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(tsuccessParam), + SecretBinary: []byte(tsuccessSecret), }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, "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) { @@ -79,7 +78,7 @@ func Test_GetSecretMgr(t *testing.T) { awsSecretsMgrGetChecker(t, params) return nil, fmt.Errorf("unable to retrieve secret") }) - }, NewConfig(), + }, config.NewConfig(), }, "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) { @@ -89,19 +88,19 @@ func Test_GetSecretMgr(t *testing.T) { SecretString: nil, }, nil }) - }, NewConfig(), + }, config.NewConfig(), }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator) + + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config.WithTokenSeparator(tt.tokenSeparator).WithKeySeparator(tt.keySeparator)) + impl, _ := NewSecretsMgr(context.TODO()) impl.svc = tt.mockClient(t) - rs := newRetrieveStrategy(NewDefatultStrategy(), *tt.config) - rs.setImplementation(impl) - rs.setTokenVal(tt.token) - got, err := rs.getTokenValue() + impl.SetToken(token) + got, err := impl.Token() 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 new file mode 100644 index 0000000..913a200 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,39 @@ +package store + +import ( + "errors" + "fmt" + + "github.com/dnitsch/configmanager/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") +) + +// Strategy iface that all store implementations +// must conform to, in order to be be used by the retrieval implementation +type Strategy interface { + Token() (s string, e error) + SetToken(s *config.ParsedTokenConfig) +} + +type DefaultStrategy struct { +} + +func NewDefatultStrategy() *DefaultStrategy { + return &DefaultStrategy{} +} + +// SetToken on default strategy +func (implmt *DefaultStrategy) SetToken(token *config.ParsedTokenConfig) {} + +// Token +func (implmt *DefaultStrategy) Token() (string, error) { + return "", fmt.Errorf("default strategy does not implement token retrieval") +} diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..76de451 --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,24 @@ +package store_test + +import ( + "testing" + + "github.com/dnitsch/configmanager/internal/store" +) + +func Test_StoreDefault(t *testing.T) { + + t.Run("Default Shoudl not errror", func(t *testing.T) { + rs := store.NewDefatultStrategy() + if rs == nil { + t.Fatal("unable to init default strategy") + } + }) + t.Run("Token method should error", func(t *testing.T) { + rs := store.NewDefatultStrategy() + if _, err := rs.Token(); err == nil { + t.Fatal("Token should return not implemented error") + } + }) + +} diff --git a/internal/strategy/strategy.go b/internal/strategy/strategy.go new file mode 100644 index 0000000..9e4a3e4 --- /dev/null +++ b/internal/strategy/strategy.go @@ -0,0 +1,126 @@ +package strategy + +import ( + "context" + "errors" + "fmt" + + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/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 + +var defaultStrategyFuncMap = map[config.ImplementationPrefix]StrategyFunc{ + config.AzTableStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewAzTableStore(ctx, token) + }, + config.AzAppConfigPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewAzAppConf(ctx, token) + }, + config.GcpSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewGcpSecrets(ctx) + }, + config.SecretMgrPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewSecretsMgr(ctx) + }, + config.ParamStorePrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewParamStore(ctx) + }, + config.AzKeyVaultSecretsPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewKvScrtStore(ctx, token) + }, + config.HashicorpVaultPrefix: func(ctx context.Context, token *config.ParsedTokenConfig) (store.Strategy, error) { + return store.NewVaultStore(ctx, token) + }, +} + +type RetrieveStrategy struct { + implementation store.Strategy + config config.GenVarsConfig + strategyFuncMap StrategyFuncMap + token string +} + +// New +func New(s store.Strategy, config config.GenVarsConfig) *RetrieveStrategy { + // make a copy of the map + // as it's a special type of a pointer + defaultFuncMapCopy := make(StrategyFuncMap) + for prefix, strateyFunc := range defaultStrategyFuncMap { + defaultFuncMapCopy[prefix] = strateyFunc + } + return &RetrieveStrategy{implementation: s, config: config, strategyFuncMap: defaultFuncMapCopy} +} + +// WithStrategyFuncMap Adds custom implementations for prefix +// +// Mainly used for testing +// NOTE: this may lead to eventual optional configurations by users +func (rs *RetrieveStrategy) WithStrategyFuncMap(funcMap StrategyFuncMap) *RetrieveStrategy { + for prefix, implementation := range funcMap { + rs.strategyFuncMap[config.ImplementationPrefix(prefix)] = implementation + } + return rs +} + +func (rs *RetrieveStrategy) setImplementation(strategy store.Strategy) { + rs.implementation = strategy +} + +func (rs *RetrieveStrategy) setTokenVal(s *config.ParsedTokenConfig) { + rs.implementation.SetToken(s) +} + +func (rs *RetrieveStrategy) getTokenValue() (string, error) { + return rs.implementation.Token() +} + +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 +} + +// 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 + } + 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[token.Prefix()]; found { + return store(ctx, token) + } + + return nil, fmt.Errorf("implementation not found for input string: %s", token) +} diff --git a/internal/strategy/strategy_test.go b/internal/strategy/strategy_test.go new file mode 100644 index 0000000..433720e --- /dev/null +++ b/internal/strategy/strategy_test.go @@ -0,0 +1,381 @@ +package strategy_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/internal/store" + "github.com/dnitsch/configmanager/internal/strategy" + "github.com/dnitsch/configmanager/internal/testutils" + "github.com/go-test/deep" +) + +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 +} + +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 + }{ + "with mocked implementation AZTABLESTORAGE": { + func(t *testing.T) store.Strategy { + return &mockGenerate{"AZTABLESTORE://mountPath/token", "bar", nil} + }, + config.NewConfig().WithOutputPath("stdout").WithTokenSeparator("://"), + "AZTABLESTORE://mountPath/token", + "bar", + }, + // "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) { + rs := strategy.New(store.NewDefatultStrategy(), *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + got := rs.RetrieveByToken(context.TODO(), 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) + } + }) + } +} + +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.NewParsedTokenConfig("AZTABLESTORE://mountPath/token", *genVarsConf) + + 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(store.NewDefatultStrategy(), *genVarsConf) + s.WithStrategyFuncMap(strategy.StrategyFuncMap{config.AzTableStorePrefix: custFunc}) + + store, _ := s.SelectImplementation(context.TODO(), token) + _ = s.RetrieveByToken(context.TODO(), store, token) + + if called != 1 { + t.Errorf(testutils.TestPhraseWithContext, "custom func not called", called, 1) + } + }) + } +} + +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 + }{ + "unknown": { + func() func() { + return func() { + } + }, + "UNKNOWN#foo/bar", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { return nil }, + fmt.Errorf("implementation not found for input string: UNKNOWN#foo/bar"), + }, + "success AZTABLESTORE": { + func() func() { + os.Setenv("AZURE_stuff", "foo") + return func() { + os.Clearenv() + } + }, + "AZTABLESTORE#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + token, _ := config.NewParsedTokenConfig("AZTABLESTORE#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + s, _ := store.NewAzTableStore(context.TODO(), token) + return s + }, + nil, + }, + "success AWSPARAMSTR": { + func() func() { + os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") + os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") + return func() { + os.Clearenv() + } + }, + "AWSPARAMSTR#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + s, _ := store.NewParamStore(context.TODO()) + return s + }, + nil, + }, + "success AWSSECRETS": { + func() func() { + os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") + os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") + return func() { + os.Clearenv() + } + }, + "AWSSECRETS#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + s, _ := store.NewSecretsMgr(context.TODO()) + return s + }, + nil, + }, + "success AZKVSECRET": { + func() func() { + os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") + os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") + return func() { + os.Clearenv() + } + }, + "AZKVSECRET#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + token, _ := config.NewParsedTokenConfig("AZKVSECRET#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + s, _ := store.NewKvScrtStore(context.TODO(), token) + return s + }, + nil, + }, + "success AZAPPCONF": { + func() func() { + return func() { + os.Clearenv() + } + }, + "AZAPPCONF#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + token, _ := config.NewParsedTokenConfig("AZAPPCONF#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + s, _ := store.NewAzAppConf(context.TODO(), token) + return s + }, + nil, + }, + "success VAULT": { + func() func() { + os.Setenv("VAULT_", "AAAAAAAAAAAAAAA") + return func() { + os.Clearenv() + } + }, + "VAULT#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + token, _ := config.NewParsedTokenConfig("VAULT#foo/bar1", *config.NewConfig().WithTokenSeparator("#")) + s, _ := store.NewVaultStore(context.TODO(), token) + return s + }, + nil, + }, + "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() + } + }, + "GCPSECRETS#foo/bar1", + config.NewConfig().WithTokenSeparator("#"), + func() store.Strategy { + s, _ := store.NewGcpSecrets(context.TODO()) + return s + }, + nil, + }, + // "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) { + tearDown := tt.setUpTearDown() + defer tearDown() + want := tt.expect() + rs := strategy.New(store.NewDefatultStrategy(), *tt.config) + token, _ := config.NewParsedTokenConfig(tt.token, *tt.config) + got, err := rs.SelectImplementation(context.TODO(), token) + + 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)) + } + }) + } +} + +// "success AWSSEcretsMgr": { +// func() func() { +// os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") +// os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") +// return func() { +// os.Clearenv() +// } +// }, +// context.TODO(), +// SecretMgrPrefix, "AWSSECRETS://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), +// func(t *testing.T, ctx context.Context, conf GenVarsConfig) store.Strategy { +// imp, err := NewSecretsMgr(ctx) +// if err != nil { +// t.Errorf(testutils.TestPhraseWithContext, "aws secrets init impl error", err.Error(), nil) +// } +// return imp +// }, +// }, +// "success AWSParamStore": { +// func() func() { +// os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") +// os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") +// return func() { +// os.Clearenv() +// } +// }, +// context.TODO(), +// ParamStorePrefix, "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, "paramstore init impl error", err.Error(), nil) +// } +// return imp +// }, +// }, +// "success GCPSecrets": { +// func() func() { +// tmp, _ := os.CreateTemp(".", "gcp-creds-*") +// tmp.Write(TEST_GCP_CREDS) +// os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", tmp.Name()) +// return func() { +// os.Clearenv() +// os.Remove(tmp.Name()) +// } +// }, +// context.TODO(), +// GcpSecretsPrefix, "GCPSECRETS://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), +// func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { +// imp, err := NewGcpSecrets(ctx) +// if err != nil { +// t.Errorf(testutils.TestPhraseWithContext, "gcp secrets init impl error", err.Error(), nil) +// } +// return imp +// }, +// }, +// "success AZKV": { +// func() func() { +// os.Setenv("AZURE_STUFF", "foo") +// return func() { +// os.Clearenv() +// } +// }, +// context.TODO(), +// AzKeyVaultSecretsPrefix, "AZKVSECRET://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), +// func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { +// imp, err := NewKvScrtStore(ctx, "AZKVSECRET://foo/bar", conf) +// if err != nil { +// t.Errorf(testutils.TestPhraseWithContext, "azkv init impl error", err.Error(), nil) +// } +// return imp +// }, +// }, +// "success Vault": { +// func() func() { +// os.Setenv("VAULT_TOKEN", "foo") +// os.Setenv("VAULT_ADDR", "http://127.0.0.1:8200") +// return func() { +// os.Clearenv() +// } +// }, +// context.TODO(), +// HashicorpVaultPrefix, "VAULT://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), +// func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { +// imp, err := NewVaultStore(ctx, "VAULT://foo/bar", conf) +// if err != nil { +// t.Errorf(testutils.TestPhraseWithContext, "vault init impl error", err.Error(), nil) +// } +// return imp +// }, +// }, diff --git a/pkg/generator/config.go b/pkg/generator/config.go deleted file mode 100644 index 9d32e92..0000000 --- a/pkg/generator/config.go +++ /dev/null @@ -1,137 +0,0 @@ -package generator - -import ( - "encoding/json" - "fmt" - "strings" -) - -// TokenConfigVars -type TokenConfigVars struct { - Token string - // AWS IAM Role for Vault AWS IAM Auth - Role string - // where supported a version of the secret can be specified - // - // e.g. HashiVault or AWS SecretsManager or AzAppConfig Label - // - Version string -} - -// GenVarsConfig defines the input config object to be passed -type GenVarsConfig struct { - outpath string - tokenSeparator string - keySeparator string - // parseAdditionalVars func(token string) TokenConfigVars -} - -// NewConfig -func NewConfig() *GenVarsConfig { - return &GenVarsConfig{ - tokenSeparator: tokenSeparator, - keySeparator: keySeparator, - } -} - -// WithOutputPath -func (c *GenVarsConfig) WithOutputPath(out string) *GenVarsConfig { - c.outpath = out - return c -} - -// WithTokenSeparator adds a custom token separator -// token is the actual value of the parameter/secret in the -// provider store -func (c *GenVarsConfig) WithTokenSeparator(tokenSeparator string) *GenVarsConfig { - c.tokenSeparator = tokenSeparator - return c -} - -// WithKeySeparator adds a custom key separotor -func (c *GenVarsConfig) WithKeySeparator(keySeparator string) *GenVarsConfig { - c.keySeparator = keySeparator - return c -} - -// OutputPath returns the outpath set in the config -func (c *GenVarsConfig) OutputPath() string { - return c.outpath -} - -// TokenSeparator returns the tokenSeparator set in the config -func (c *GenVarsConfig) TokenSeparator() string { - return c.tokenSeparator -} - -// KeySeparator returns the keySeparator set in the config -func (c *GenVarsConfig) KeySeparator() string { - return c.keySeparator -} - -// ParseMetadata parses the metadata of each token into the provided pointer. -// All data inside the `[` `]` is considered metadata about the token. -// -// returns the token without the metadata `[` `]` -// -// Further processing down the line will remove other elements of the token. -func ParseMetadata[T comparable](token string, typ T) string { - metadataStr, metaLessToken, found := extractMetadataStr(token) - if !found { - return token - } - - token = metaLessToken - - // 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.Split(metadataStr, ",") { - mapKeyVal := strings.Split(keyVal, "=") - if len(mapKeyVal) == 2 { - 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 - b := []byte(fmt.Sprintf(`{%s}`, strings.Join(metaMap, ","))) - if err := json.Unmarshal(b, typ); 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 token - } - return token -} - -const startMetaStr string = `[` -const endMetaStr string = `]` - -// extractMetadataStr returns anything between the start and end -// metadata markers in the token string itself -func extractMetadataStr(token string) (metaString string, tokenWithoutMeta string, found bool) { - - startIndex := strings.Index(token, startMetaStr) - // token has no startMetaStr - if startIndex == -1 { - return metaString, token, false - } - newS := token[startIndex+len(startMetaStr):] - - endIndex := strings.Index(newS, endMetaStr) - // token has no meta end - if endIndex == -1 { - return metaString, token, false - } - // metastring extracted - metaString = newS[:endIndex] - - // complete [key=value] has been found - // Remove from the token - metaLessToken := strings.Replace(token, startMetaStr+metaString+endMetaStr, "", -1) - - return metaString, metaLessToken, true -} diff --git a/pkg/generator/config_test.go b/pkg/generator/config_test.go deleted file mode 100644 index 34b9195..0000000 --- a/pkg/generator/config_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package generator_test - -import ( - "testing" - - "github.com/dnitsch/configmanager/internal/testutils" - "github.com/dnitsch/configmanager/pkg/generator" -) - -func Test_MarshalMetadata_with_label_struct_succeeds(t *testing.T) { - type labelMeta struct { - Label string `json:"label"` - } - - ttests := map[string]struct { - config *generator.GenVarsConfig - rawToken string - wantLabel string - wantToken string - }{ - "when provider expects label on token and label exists": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123|d88[label=dev]`, - "dev", - "FOO://basjh/dskjuds/123|d88", - }, - "when provider expects label on token and label does not exist": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123|d88[someother=dev]`, - "", - "FOO://basjh/dskjuds/123|d88", - }, - "no metadata found": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123|d88`, - "", - "FOO://basjh/dskjuds/123|d88", - }, - "no metadata found incorrect marker placement": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123|d88]asdas=bar[`, - "", - "FOO://basjh/dskjuds/123|d88]asdas=bar[", - }, - "no metadata found incorrect marker placement and no key separator": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123]asdas=bar[`, - "", - "FOO://basjh/dskjuds/123]asdas=bar[", - }, - "no end found incorrect marker placement and no key separator": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123[asdas=bar`, - "", - "FOO://basjh/dskjuds/123[asdas=bar", - }, - "no start found incorrect marker placement and no key separator": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123]asdas=bar]`, - "", - "FOO://basjh/dskjuds/123]asdas=bar]", - }, - "metadata is in the middle of path lookup": { - generator.NewConfig(), - `FOO://basjh/dskjuds/123[label=bar]|lookup`, - "bar", - "FOO://basjh/dskjuds/123|lookup", - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - inputTyp := &labelMeta{} - got := generator.ParseMetadata(tt.rawToken, inputTyp) - - if got != tt.wantToken { - t.Errorf(testutils.TestPhraseWithContext, "Token does not match", got, tt.wantToken) - } - - if inputTyp.Label != tt.wantLabel { - t.Errorf(testutils.TestPhraseWithContext, "Metadata Label does not match", inputTyp.Label, tt.wantLabel) - } - }) - } -} diff --git a/pkg/generator/defaultstrategy.go b/pkg/generator/defaultstrategy.go deleted file mode 100644 index fc471e0..0000000 --- a/pkg/generator/defaultstrategy.go +++ /dev/null @@ -1,30 +0,0 @@ -package generator - -import ( - "errors" - "fmt" -) - -type DefaultStrategy struct { -} - -const implementationNetworkErr string = "implementation %s error: %v for token: %s" - -var ( - ErrEmptyResponse = errors.New("value retrieved but empty for token") - ErrServiceCallFailed = errors.New("failed to complete the service call") -) - -func NewDefatultStrategy() *DefaultStrategy { - return &DefaultStrategy{} -} - -// setToken on default strategy -func (implmt *DefaultStrategy) setTokenVal(token string) {} - -// setValue on default strategy -func (implmt *DefaultStrategy) setValue(val string) {} - -func (implmt *DefaultStrategy) tokenVal(v *retrieveStrategy) (string, error) { - return "", fmt.Errorf("default strategy does not implement token retrieval") -} diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index cebe541..28bbf24 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -4,73 +4,27 @@ import ( "context" "encoding/json" "fmt" - "io" "strconv" - "strings" "sync" + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/internal/store" + "github.com/dnitsch/configmanager/internal/strategy" "github.com/dnitsch/configmanager/pkg/log" "github.com/spyzhov/ajson" ) -type ImplementationPrefix string - -const ( - // AWS SecretsManager prefix - SecretMgrPrefix ImplementationPrefix = "AWSSECRETS" - // AWS Parameter Store prefix - ParamStorePrefix ImplementationPrefix = "AWSPARAMSTR" - // Azure Key Vault Secrets prefix - AzKeyVaultSecretsPrefix ImplementationPrefix = "AZKVSECRET" - // Azure Key Vault Secrets prefix - AzTableStorePrefix ImplementationPrefix = "AZTABLESTORE" - // Azure App Config prefix - AzAppConfigPrefix ImplementationPrefix = "AZAPPCONF" - // Hashicorp Vault prefix - HashicorpVaultPrefix ImplementationPrefix = "VAULT" - // GcpSecrets - GcpSecretsPrefix ImplementationPrefix = "GCPSECRETS" -) - -const ( - // tokenSeparator used for identifying the end of a prefix and beginning of token - // see notes about special consideration for AZKVSECRET tokens - tokenSeparator = "#" - // keySeparator used for accessing nested objects within the retrieved map - keySeparator = "|" -) - -var ( - // default varPrefix used by the replacer function - // any token must beging with one of these else - // it will be skipped as not a replaceable token - VarPrefix = map[ImplementationPrefix]bool{ - SecretMgrPrefix: true, ParamStorePrefix: true, AzKeyVaultSecretsPrefix: true, - GcpSecretsPrefix: true, HashicorpVaultPrefix: true, AzTableStorePrefix: true, - AzAppConfigPrefix: true, - } -) - -// Generatoriface describes the exported methods -// on the GenVars struct. -type Generatoriface interface { - Generate(tokens []string) (ParsedMap, error) - ConvertToExportVar() []string - FlushToFile(w io.Writer, outString []string) error - StrToFile(w io.Writer, str string) error -} - -// GenVarsiface stores strategy and GenVars implementation behaviour -type GenVarsiface interface { - Generatoriface - Config() *GenVarsConfig -} - type muRawMap struct { mu sync.RWMutex tokenMap ParsedMap } +type retrieveIface interface { + WithStrategyFuncMap(funcMap strategy.StrategyFuncMap) *strategy.RetrieveStrategy + RetrieveByToken(ctx context.Context, impl store.Strategy, in *config.ParsedTokenConfig) *strategy.TokenResponse + SelectImplementation(ctx context.Context, in *config.ParsedTokenConfig) (store.Strategy, error) +} + // GenVars is the main struct holding the // strategy patterns iface // any initialised config if overridded with withers @@ -78,10 +32,9 @@ type muRawMap struct { // 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 { - Generatoriface - ctx context.Context - config GenVarsConfig - outString []string + strategy retrieveIface + 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 @@ -102,20 +55,27 @@ func NewGenerator() *GenVars { func newGenVars() *GenVars { m := make(ParsedMap) - defaultConf := GenVarsConfig{ - tokenSeparator: tokenSeparator, - keySeparator: keySeparator, - } + conf := config.NewConfig() return &GenVars{ rawMap: muRawMap{tokenMap: m}, ctx: context.TODO(), // return using default config - config: defaultConf, + config: *conf, + // using a default Strategy + strategy: strategy.New(store.NewDefatultStrategy(), *conf), } } +// WithStrategyMap +// +// Adds addtional funcs for storageRetrieval +func (c *GenVars) WithStrategyMap(sm strategy.StrategyFuncMap) *GenVars { + c.strategy.WithStrategyFuncMap(sm) + return c +} + // WithConfig uses custom config -func (c *GenVars) WithConfig(cfg *GenVarsConfig) *GenVars { +func (c *GenVars) WithConfig(cfg *config.GenVarsConfig) *GenVars { // backwards compatibility if cfg != nil { c.config = *cfg @@ -130,14 +90,16 @@ func (c *GenVars) WithContext(ctx context.Context) *GenVars { } // Config gets Config on the GenVars -func (c *GenVars) Config() *GenVarsConfig { +func (c *GenVars) Config() *config.GenVarsConfig { return &c.config } -func (c *GenVars) RawMap() ParsedMap { +func (c *GenVars) parsedMap() ParsedMap { c.rawMap.mu.RLock() defer c.rawMap.mu.RUnlock() // make a copy of the map + // so it cannot be passed + // as a pointer to live data m := make(ParsedMap) for k, v := range c.rawMap.tokenMap { m[k] = v @@ -145,52 +107,45 @@ func (c *GenVars) RawMap() ParsedMap { return m } -func (c *GenVars) AddRawMap(key, val string) { +func (c *GenVars) addRawMap(key *config.ParsedTokenConfig, val string) { c.rawMap.mu.Lock() defer c.rawMap.mu.Unlock() - // strip the metadata from token - strippedToken := ParseMetadata(key, &struct{}{}) - // still use the metadata in the key + // 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 - c.rawMap.tokenMap[key] = c.keySeparatorLookup(strippedToken, val) + c.rawMap.tokenMap[key.String()] = c.keySeparatorLookup(key, val) } +type rawTokenMap map[string]*config.ParsedTokenConfig + // 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 +// the standard pattern of a token should follow a path like string func (c *GenVars) Generate(tokens []string) (ParsedMap, error) { - rawTokenPrefixMap := make(map[string]string) + parsedTokenMap := make(rawTokenMap) for _, token := range tokens { - prefix := strings.Split(token, c.config.tokenSeparator)[0] - if found := VarPrefix[ImplementationPrefix(prefix)]; found { - rawTokenPrefixMap[token] = prefix + // 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 { + parsedTokenMap[token] = parsedToken + continue } + log.Infof(err.Error()) } - rs := newRetrieveStrategy(NewDefatultStrategy(), c.config) // pass in default initialised retrieveStrategy - if err := c.generate(rawTokenPrefixMap, rs); err != nil { + // input should be + if err := c.generate(parsedTokenMap); err != nil { return nil, err } - return c.RawMap(), nil -} - -type chanResp struct { - value string - key string - err error -} - -type retrieveIface interface { - RetrieveByToken(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) chanResp - SelectImplementation(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) + return c.parsedMap(), 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 map[string]string, rs retrieveIface) error { +func (c *GenVars) generate(rawMap rawTokenMap) error { if len(rawMap) < 1 { log.Debug("no replaceable tokens found in input strings") return nil @@ -200,23 +155,22 @@ func (c *GenVars) generate(rawMap map[string]string, rs retrieveIface) error { // build an exact size channel var wg sync.WaitGroup initChanLen := len(rawMap) - outCh := make(chan chanResp, initChanLen) + outCh := make(chan *strategy.TokenResponse, initChanLen) wg.Add(initChanLen) // TODO: initialise the singleton serviceContainer // pass into each goroutine - for token, prefix := range rawMap { + for _, parsedToken := range rawMap { // take value from config allocation on a per iteration basis - conf := c.Config() - go func(prfx ImplementationPrefix, tkn string, conf GenVarsConfig) { + go func(token *config.ParsedTokenConfig) { defer wg.Done() - strategy, err := rs.SelectImplementation(c.ctx, prfx, tkn, conf) + storeStrategy, err := c.strategy.SelectImplementation(c.ctx, token) if err != nil { - outCh <- chanResp{err: err} + outCh <- &strategy.TokenResponse{Err: err} return } - outCh <- rs.RetrieveByToken(c.ctx, strategy, prfx, tkn) - }(ImplementationPrefix(prefix), token, *conf) + outCh <- c.strategy.RetrieveByToken(c.ctx, storeStrategy, token) + }(parsedToken) } go func() { @@ -227,13 +181,13 @@ func (c *GenVars) generate(rawMap map[string]string, rs retrieveIface) error { for cro := range outCh { cr := cro log.Debugf("cro: %+v", cr) - if cr.err != nil { - log.Debugf("cr.err %v, for token: %s", cr.err, cr.key) - errors = append(errors, cr.err) + if cr.Err != nil { + log.Debugf("cr.err %v, for token: %s", cr.Err, cr.Key()) + errors = append(errors, cr.Err) // Skip adding not found key to the RawMap continue } - c.AddRawMap(cr.key, cr.value) + c.addRawMap(cr.Key(), cr.Value()) } if len(errors) > 0 { @@ -244,12 +198,12 @@ func (c *GenVars) generate(rawMap map[string]string, rs retrieveIface) error { return nil } -// isParsed will try to parse the return found string into +// 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(res any, trm *ParsedMap) bool { - str := fmt.Sprint(res) +func IsParsed(v any, trm *ParsedMap) bool { + str := fmt.Sprint(v) if err := json.Unmarshal([]byte(str), &trm); err != nil { log.Info("unable to parse into a k/v map returning a string instead") return false @@ -261,16 +215,15 @@ func isParsed(res any, trm *ParsedMap) bool { // keySeparatorLookup checks if the key contains // keySeparator character // If it does contain one then it tries to parse -func (c *GenVars) keySeparatorLookup(key, val string) string { +func (c *GenVars) keySeparatorLookup(key *config.ParsedTokenConfig, val string) string { // key has separator - kl := strings.Split(key, c.config.keySeparator) - log.Debugf("key list: %v", kl) - if len(kl) < 2 { + k := key.LookupKeys() + if k == "" { log.Infof("no keyseparator found") return val } - keys, err := ajson.JSONPath([]byte(val), fmt.Sprintf("$..%s", kl[1])) + keys, err := ajson.JSONPath([]byte(val), fmt.Sprintf("$..%s", k)) if err != nil { log.Debugf("unable to parse as json object %v", err.Error()) return val @@ -293,80 +246,3 @@ func (c *GenVars) keySeparatorLookup(key, val string) string { log.Infof("no value found in json using path expression") return "" } - -// ConvertToExportVar assigns the k/v out -// as unix style export key=val pairs separated by `\n` -func (c *GenVars) ConvertToExportVar() []string { - for k, v := range c.RawMap() { - rawKeyToken := strings.Split(k, "/") // assumes a path like token was used - topLevelKey := rawKeyToken[len(rawKeyToken)-1] - trm := make(ParsedMap) - if parsedOk := isParsed(v, &trm); parsedOk { - // if is a map - // try look up on key if separator defined - normMap := c.envVarNormalize(trm) - c.exportVars(normMap) - continue - } - c.exportVars(ParsedMap{topLevelKey: v}) - } - return c.outString -} - -// envVarNormalize -func (c *GenVars) envVarNormalize(pmap ParsedMap) ParsedMap { - normalizedMap := make(ParsedMap) - for k, v := range pmap { - normalizedMap[c.normalizeKey(k)] = v - } - return normalizedMap -} - -func (c *GenVars) exportVars(exportMap ParsedMap) { - - for k, v := range exportMap { - // NOTE: \n line ending is not totally cross platform - t := fmt.Sprintf("%T", v) - switch t { - case "string": - c.outString = append(c.outString, fmt.Sprintf("export %s='%s'", c.normalizeKey(k), v)) - default: - c.outString = append(c.outString, fmt.Sprintf("export %s=%v", c.normalizeKey(k), v)) - } - } -} - -// normalizeKeys returns env var compatible key -func (c *GenVars) normalizeKey(k string) string { - // the order of replacer pairs matters less - // as the Replace builds a node tree without overlapping matches - replacer := strings.NewReplacer([]string{" ", "", "@", "", "!", "", "-", "_", c.config.keySeparator, "__"}...) - return strings.ToUpper(replacer.Replace(k)) -} - -// FlushToFile saves contents to file provided -// in the config input into the generator -// default location is ./app.env -func (c *GenVars) FlushToFile(w io.Writer, out []string) error { - return c.flushToFile(w, listToString(c.outString)) -} - -// StrToFile writes a provided string to the writer -func (c *GenVars) StrToFile(w io.Writer, str string) error { - return c.flushToFile(w, str) -} - -func (c *GenVars) flushToFile(f io.Writer, str string) error { - - _, e := f.Write([]byte(str)) - - if e != nil { - return e - } - - return nil -} - -func listToString(strList []string) string { - return strings.Join(strList, "\n") -} diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index 90aed07..b6b4548 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -1,433 +1,544 @@ -package generator +package generator_test import ( "context" "fmt" - "strings" "testing" + "github.com/dnitsch/configmanager/internal/config" + "github.com/dnitsch/configmanager/internal/store" + "github.com/dnitsch/configmanager/internal/strategy" "github.com/dnitsch/configmanager/internal/testutils" + "github.com/dnitsch/configmanager/pkg/generator" ) -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) - } +type mockGenerate struct { + inToken, value string + err error } -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 (m *mockGenerate) SetToken(s *config.ParsedTokenConfig) { } - -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 (m *mockGenerate) Token() (s string, e error) { + return m.value, m.err } -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_Generate(t *testing.T) { -func Test_KeyLookup(t *testing.T) { - f := newFixture(t) - f.configGenVars(standardop, standardts) + 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 + } - 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) - } - }) - } -} + g := generator.NewGenerator() + g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) + got, err := g.Generate([]string{"UNKNOWN://mountPath/token"}) -func Test_ConvertToExportVars(t *testing.T) { - tests := map[string]struct { - rawMap ParsedMap - expectStr string - expectLength int - }{ - "number included": {ParsedMap{"foo": "BAR", "num": 123}, `export FOO='BAR'`, 2}, - "strings only": {ParsedMap{"foo": "BAR", "num": "a123"}, `export FOO='BAR'`, 2}, - "numbers only": {ParsedMap{"foo": 123, "num": 456}, `export FOO=123`, 2}, - "map inside response": {ParsedMap{"map": `{"foo":"bar","baz":"qux"}`, "num": 123}, `export FOO='bar'`, 3}, - } + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 1 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 1) + } + }) - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - f := newFixture(t) - f.configGenVars(standardop, standardts) - f.c.rawMap = muRawMap{tokenMap: tt.rawMap} - f.c.ConvertToExportVar() - got := f.c.outString - if got == nil { - t.Errorf(testutils.TestPhrase, got, "not nil") - } - if len(got) != tt.expectLength { - t.Errorf(testutils.TestPhrase, len(got), tt.expectLength) - } - st := strings.Join(got, "\n") - if !strings.Contains(st, tt.expectStr) { - t.Errorf(testutils.TestPhrase, st, tt.expectStr) - } - }) - } -} + 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 + } -func Test_listToString(t *testing.T) { - tests := map[string]struct { - in []string - expect string - }{ - "1 item slice": {[]string{"export ONE=foo"}, "export ONE=foo"}, - "0 item slice": {[]string{}, ""}, - "4 item slice": {[]string{"123", "123", "123", "123"}, `123 -123 -123 -123`, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - got := listToString(tt.in) - if got != tt.expect { - t.Errorf(testutils.TestPhrase, tt.expect, got) - } - }) - } -} + g := generator.NewGenerator() + g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) + got, err := g.Generate([]string{"UNKNOWN://mountPath/token"}) -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) -} + if err != nil { + t.Fatal("errored on generate") + } + if len(got) != 0 { + t.Errorf(testutils.TestPhraseWithContext, "incorect number in a map", len(got), 0) + } + }) -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) -} + 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 + } -type mockImpl struct { - token, value string - err error -} + g := generator.NewGenerator() + g.WithStrategyMap(strategy.StrategyFuncMap{config.UnknownPrefix: custFunc}) + got, err := g.Generate([]string{"UNKNOWN://mountPath/token|key1.key2"}) -func (m *mockImpl) tokenVal(rs *retrieveStrategy) (s string, e error) { - return m.value, m.err -} -func (m *mockImpl) setTokenVal(s string) { - m.token = s + 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_rawmap_of_tokens_mapped_to_values(t *testing.T) { +func Test_generate_withKeys_lookup(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 + custFunc strategy.StrategyFunc + token string + expectVal 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 + "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", }, - // 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 + "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", }, - "error in imp selection": { - func(t *testing.T) map[string]string { - rm := make(map[string]string) - rm["foo"] = "bar" - return rm + "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 }, - 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 + 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) { - 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())) + g := generator.NewGenerator() + 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 TestGenerate(t *testing.T) { +func Test_IsParsed(t *testing.T) { ttests := map[string]struct { - tokens func(t *testing.T) []string - expectLength int + val any + isParsed bool }{ - "success without correct prefix": { - func(t *testing.T) []string { - return []string{"WRONGIMPL://bar-vault/token1", "AZKVNOTSECRET://bar-vault/token1"} - }, - 0, + "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) { - 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) + 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/dnitsch/configmanager/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) +// } +// }) +// } +// } diff --git a/pkg/generator/strategy.go b/pkg/generator/strategy.go deleted file mode 100644 index 24f4fee..0000000 --- a/pkg/generator/strategy.go +++ /dev/null @@ -1,94 +0,0 @@ -package generator - -import ( - "context" - "errors" - "fmt" - "regexp" - "strings" -) - -var ( - ErrRetrieveFailed = errors.New("failed to retrieve config item") - ErrClientInitialization = errors.New("failed to initialize the client") -) - -type retrieveStrategy struct { - implementation genVarsStrategy - config GenVarsConfig - token string -} - -func newRetrieveStrategy(s genVarsStrategy, config GenVarsConfig) *retrieveStrategy { - return &retrieveStrategy{implementation: s, config: config} -} - -type genVarsStrategy interface { - // getTokenConfig() AdditionalVars - // setTokenConfig(AdditionalVars) - tokenVal(rs *retrieveStrategy) (s string, e error) - setTokenVal(s string) -} - -func (rs *retrieveStrategy) setImplementation(strategy genVarsStrategy) { - rs.implementation = strategy -} - -func (rs *retrieveStrategy) setTokenVal(s string) { - rs.implementation.setTokenVal(s) -} - -func (rs *retrieveStrategy) getTokenValue() (string, error) { - return rs.implementation.tokenVal(rs) -} - -// retrieveSpecificCh wraps around the specific strategy implementation -// and publishes results to a channel -func (rs *retrieveStrategy) RetrieveByToken(ctx context.Context, impl genVarsStrategy, prefix ImplementationPrefix, in string) chanResp { - cr := chanResp{} - cr.err = nil - cr.key = in - rs.setImplementation(impl) - rs.setTokenVal(in) - s, err := rs.getTokenValue() - if err != nil { - cr.err = err - return cr - } - cr.value = s - return cr -} - -func (rs *retrieveStrategy) SelectImplementation(ctx context.Context, prefix ImplementationPrefix, in string, config GenVarsConfig) (genVarsStrategy, error) { - switch prefix { - case SecretMgrPrefix: - return NewSecretsMgr(ctx) - case ParamStorePrefix: - return NewParamStore(ctx) - case AzKeyVaultSecretsPrefix: - return NewKvScrtStore(ctx, in, config) - case GcpSecretsPrefix: - return NewGcpSecrets(ctx) - case HashicorpVaultPrefix: - return NewVaultStore(ctx, in, config) - case AzTableStorePrefix: - return NewAzTableStore(ctx, in, config) - case AzAppConfigPrefix: - return NewAzAppConf(ctx, in, config) - default: - return nil, fmt.Errorf("implementation not found for input string: %s", in) - } -} - -// stripPrefix returns the token which the config/secret store -// expects to find in a provided vault/paramstore -func (rs *retrieveStrategy) stripPrefix(in string, prefix ImplementationPrefix) string { - return stripPrefix(in, prefix, rs.config.tokenSeparator, rs.config.keySeparator) -} - -// stripPrefix -func stripPrefix(in string, prefix ImplementationPrefix, tokenSeparator, keySeparator string) string { - t := in - b := regexp.MustCompile(fmt.Sprintf(`[%s].*`, keySeparator)).ReplaceAllString(t, "") - return strings.Replace(b, fmt.Sprintf("%s%s", prefix, tokenSeparator), "", 1) -} diff --git a/pkg/generator/strategy_test.go b/pkg/generator/strategy_test.go deleted file mode 100644 index 005d156..0000000 --- a/pkg/generator/strategy_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package generator - -import ( - "context" - "fmt" - "os" - "sync" - "testing" - - "github.com/dnitsch/configmanager/internal/testutils" - "github.com/go-test/deep" -) - -type mockGenerate struct { - token, value string - err error -} - -func (m *mockGenerate) setTokenVal(s string) { -} -func (m *mockGenerate) tokenVal(rs *retrieveStrategy) (s string, e error) { - return m.value, m.err -} - -func Test_rsRetrieve(t *testing.T) { - ttests := map[string]struct { - impl func(t *testing.T) genVarsStrategy - config GenVarsConfig - token []string - implPrefix ImplementationPrefix - expect string - }{ - "success retrieval": { - func(t *testing.T) genVarsStrategy { - return &mockGenerate{token: "SOME://mountPath/token", value: "bar", err: nil} - }, - GenVarsConfig{keySeparator: "|", tokenSeparator: "://", outpath: "stdout"}, - []string{"SOME://token"}, - HashicorpVaultPrefix, - "bar", - }, "error in retrieval": { - func(t *testing.T) genVarsStrategy { - return &mockGenerate{token: "SOME://mountPath/token", value: "bar", err: fmt.Errorf("unable to perform getTokenValue")} - }, - GenVarsConfig{keySeparator: "|", tokenSeparator: "://", outpath: "stdout"}, - []string{"SOME://token"}, - HashicorpVaultPrefix, - "unable to perform getTokenValue", - }, - } - for name, tt := range ttests { - t.Run(name, func(t *testing.T) { - got := make(chan chanResp, len(tt.token)) - rs := &retrieveStrategy{NewDefatultStrategy(), tt.config, ""} - var wg sync.WaitGroup - - wg.Add(len(tt.token)) - go func() { - defer wg.Done() - got <- rs.RetrieveByToken(context.TODO(), tt.impl(t), tt.implPrefix, tt.token[0]) - }() - - go func() { - wg.Wait() - close(got) - }() - for g := range got { - if g.err != nil { - if g.err.Error() != tt.expect { - t.Errorf(testutils.TestPhraseWithContext, "channel errored not expected", g.err.Error(), tt.expect) - } - return - } - if g.value != tt.expect { - t.Errorf(testutils.TestPhraseWithContext, "channel value", g.value, tt.expect) - } - } - }) - } -} - -var UnknownPrefix ImplementationPrefix = "WRONG" - -func TestSelectImpl(t *testing.T) { - ttests := map[string]struct { - setUpTearDown func() func() - ctx context.Context - prefix ImplementationPrefix - in string - config *GenVarsConfig - expect func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy - }{ - "success AWSSEcretsMgr": { - func() func() { - os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - return func() { - os.Clearenv() - } - }, - context.TODO(), - SecretMgrPrefix, "AWSSECRETS://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), - func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { - imp, err := NewSecretsMgr(ctx) - if err != nil { - t.Errorf(testutils.TestPhraseWithContext, "aws secrets init impl error", err.Error(), nil) - } - return imp - }, - }, - "success AWSParamStore": { - func() func() { - os.Setenv("AWS_ACCESS_KEY", "AAAAAAAAAAAAAAA") - os.Setenv("AWS_SECRET_ACCESS_KEY", "00000000000000000000111111111") - return func() { - os.Clearenv() - } - }, - context.TODO(), - ParamStorePrefix, "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, "paramstore init impl error", err.Error(), nil) - } - return imp - }, - }, - "success GCPSecrets": { - func() func() { - tmp, _ := os.CreateTemp(".", "gcp-creds-*") - tmp.Write(TEST_GCP_CREDS) - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", tmp.Name()) - return func() { - os.Clearenv() - os.Remove(tmp.Name()) - } - }, - context.TODO(), - GcpSecretsPrefix, "GCPSECRETS://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), - func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { - imp, err := NewGcpSecrets(ctx) - if err != nil { - t.Errorf(testutils.TestPhraseWithContext, "gcp secrets init impl error", err.Error(), nil) - } - return imp - }, - }, - "success AZKV": { - func() func() { - os.Setenv("AZURE_STUFF", "foo") - return func() { - os.Clearenv() - } - }, - context.TODO(), - AzKeyVaultSecretsPrefix, "AZKVSECRET://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), - func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { - imp, err := NewKvScrtStore(ctx, "AZKVSECRET://foo/bar", conf) - if err != nil { - t.Errorf(testutils.TestPhraseWithContext, "azkv init impl error", err.Error(), nil) - } - return imp - }, - }, - "success Vault": { - func() func() { - os.Setenv("VAULT_TOKEN", "foo") - os.Setenv("VAULT_ADDR", "http://127.0.0.1:8200") - return func() { - os.Clearenv() - } - }, - context.TODO(), - HashicorpVaultPrefix, "VAULT://foo/bar", (&GenVarsConfig{}).WithKeySeparator("|").WithTokenSeparator("://"), - func(t *testing.T, ctx context.Context, conf GenVarsConfig) genVarsStrategy { - imp, err := NewVaultStore(ctx, "VAULT://foo/bar", conf) - if err != nil { - t.Errorf(testutils.TestPhraseWithContext, "vault init impl error", err.Error(), nil) - } - return imp - }, - }, - "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) { - tearDown := tt.setUpTearDown() - defer tearDown() - rs := &retrieveStrategy{} - want := tt.expect(t, tt.ctx, *tt.config) - got, err := rs.SelectImplementation(tt.ctx, tt.prefix, tt.in, *tt.config) - if err != nil { - if err.Error() != fmt.Sprintf("implementation not found for input string: %s", tt.in) { - t.Errorf(testutils.TestPhraseWithContext, "uncaught error", err.Error(), fmt.Sprintf("implementation not found for input string: %s", tt.in)) - } - 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)) - } - }) - } -}