diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7b77550..16b5adc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,15 +11,14 @@ jobs: name: Build runs-on: ubuntu-latest steps: + - name: Check out code into the Go module directory + uses: actions/checkout@main - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.14 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + go-version-file: 'go.mod' + check-latest: true - name: Get dependencies run: | @@ -32,6 +31,6 @@ jobs: run: go test -race -cover -v ./... - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v6 with: - version: v1.49 + version: v1.62.2 diff --git a/README.md b/README.md index f2df119..b3ca51a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Binder is a configuration reader that parses different types of configurations and adds the possibility to bind them to one or many typed instances. -It can read configuration values from files, environment variables, `flag` flags, `spf13/pflags` flags, remote URLs, Kubernetes volumes, and is flexible enough to enable custom configuration parsers. Binder is also able to listen for file changes/volume changes, and re-bind configurations when a backing file or backing volume has been updated. +It can read configuration values from files, environment variables, `flag` flags, `spf13/pflags` flags, remote URLs, Kubernetes volumes, Azure App Configs with backing Azure Key Vaults, and is flexible enough to enable custom configuration parsers. Binder is also able to listen for file changes/volume changes, and re-bind configurations when a backing file or backing volume has been updated. Example: ```go @@ -71,24 +71,49 @@ package main var ( theCommand = &cobra.Command{ Use: "cmd", - RunE: func(cmd *cobra.Command, args []string) error { - bnd := binder.New( - binder.WithFlagSet(cmd.Flags())) + RunE: func(cmd *cobra.Command, args []string) error { + bnd := binder.New( + binder.WithFlagSet(cmd.Flags())) - cmd.ParseFlags(args) + cmd.ParseFlags(args) - var cfg Config - bnd.Bind(&cfg) + var cfg Config + bnd.Bind(&cfg) - fmt.Printf("Key: %s\n", cfg.Key) + fmt.Printf("Key: %s\n", cfg.Key) - return nil - } - } + return nil + } + } ) func init() { - theCommand.Flags().String("key", "", "the key to use") + theCommand.Flags().String("key", "", "the key to use") +} + +``` + +Azure App Config parser, usable for Azure App Configs: +```go +package main + +import "github.com/ourstudio-se/binder" + +type Config struct { + KeyOne string `config:"external_key_one"` + KeyTwo string `config:"external_key_two"` +} + +func main() { + bnd := binder.New( + binder.WithAzureConfig("https://appconfig-name.azconfig.io", []string{"additional-tenant-id"})) + defer bnd.Close() + + var cfg Config + bnd.Bind(&cfg); + + fmt.Printf("KeyOne: %s\n", cfg.KeyOne) + fmt.Printf("KeyTwo: %d\n", cfg.KeyTwo) } ``` diff --git a/go.mod b/go.mod index 9906d97..cc320ee 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,30 @@ module github.com/ourstudio-se/binder -go 1.14 +go 1.23 require ( + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 + github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 github.com/radovskyb/watcher v1.0.7 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 959b2b1..779a421 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,64 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0/go.mod h1:6tpINME7dnF7bLlb8Ubj6FtM9CFZrCn7aT02pcYrklM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/options.go b/options.go index f9433a0..65be758 100644 --- a/options.go +++ b/options.go @@ -72,6 +72,18 @@ func WithValue(key string, value interface{}) Option { return WithParser(parsers.NewKeyValueParser(r, parsers.WithKeyValueSeparator("="))) } +// WithAzureConfig is an Option to instantiate +// a parser which reads an App Config in Azure +// when instantiating a Config. +func WithAzureConfig(appConfig string, additionallyAllowTenants []string) Option { + azureConfigParser, err := parsers.NewAzureConfigParser(appConfig, additionallyAllowTenants) + if err != nil { + return nil + } + + return WithParser(azureConfigParser) +} + // WithWatch adds a file path watch, which can be // used to reload configuration values that originates // from a FileParser or a KubernetesVolumeParser when diff --git a/options_test.go b/options_test.go index a9f2d02..075bd74 100644 --- a/options_test.go +++ b/options_test.go @@ -66,3 +66,11 @@ func Test_WithWatch(t *testing.T) { assert.NotNil(t, c.watch) } + +func Test_WithAzureConfig(t *testing.T) { + _, err := parsers.NewAzureConfigParser("", nil) + c := New(WithAzureConfig("", nil)) + + assert.NoError(t, err) + assert.Len(t, c.parsers, 1) +} diff --git a/parsers/azure-app-config.go b/parsers/azure-app-config.go new file mode 100644 index 0000000..7ecbdf1 --- /dev/null +++ b/parsers/azure-app-config.go @@ -0,0 +1,167 @@ +package parsers + +import ( + "context" + "encoding/json" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" + "strings" + "time" +) + +const ( + keyVaultRef = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + secretLengthWithVersion = 3 +) + +type AppConfigPager interface { + More() bool + NextPage(ctx context.Context) (azappconfig.ListSettingsPageResponse, error) +} + +type KeyVaultClient interface { + GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) +} + +// AzureConfigParser is a configuration parser +// which reads configuration-values from an Azure AppConfig and the backing KeyVault if config values is +// key vault reference. +type AzureConfigParser struct { + settingsPager AppConfigPager + secretClientFactory func(url string) (KeyVaultClient, error) +} + +// NewAzureConfigParser returns a new AzureConfigParser. +// A AzureConfigParser reads an Azure AppConfig store and returns a map +// with key/value pairs. +func NewAzureConfigParser(appConfig string, additionallyAllowedTenants []string) (*AzureConfigParser, error) { + cred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ + AdditionallyAllowedTenants: additionallyAllowedTenants, + }) + if err != nil { + return nil, err + } + + client, err := azappconfig.NewClient(appConfig, cred, nil) + if err != nil { + return nil, err + } + + pager := client.NewListSettingsPager(azappconfig.SettingSelector{}, nil) + + keyVaultClientFactoryFunc := func(url string) (KeyVaultClient, error) { + kvClient, err := azsecrets.NewClient(url, cred, nil) + if err != nil { + return nil, err + } + return kvClient, nil + } + + return &AzureConfigParser{pager, keyVaultClientFactoryFunc}, nil +} + +// Parse retrieves all settings from AppConfig and resolves any KeyVault references. +// TODO: Parse with context as parameter. +func (p *AzureConfigParser) Parse() (map[string]interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + settings := make(map[string]interface{}) + + for p.settingsPager.More() { + settingsPage, err := p.settingsPager.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get next page of pager %w", err) + } + + for _, setting := range settingsPage.Settings { + if setting.Key == nil || setting.Value == nil { + continue + } + + var finalValue string + if setting.ContentType != nil && strings.EqualFold(*setting.ContentType, keyVaultRef) { + secretValue, err := p.resolveKeyVaultSecret(ctx, *setting.Value) + if err != nil { + return nil, fmt.Errorf("failed to get secret value: %w", err) + } + + finalValue = secretValue + } else { + finalValue = *setting.Value + } + + settings[*setting.Key] = finalValue + } + } + + return settings, nil +} + +func (p *AzureConfigParser) resolveKeyVaultSecret(ctx context.Context, keyVaultReference string) (string, error) { + var kvRef struct { + URI string `json:"uri"` + } + if err := json.Unmarshal([]byte(keyVaultReference), &kvRef); err != nil { + return "", fmt.Errorf("failed to parse config value %s value that is Key Vault reference: %w", keyVaultReference, err) + } + + reqObj, err := parseKeyVaultURI(kvRef.URI) + if err != nil { + return "", fmt.Errorf("failed to get secret request object: %w", err) + } + + keyVaultClient, err := p.secretClientFactory(reqObj.vaultURL) + if err != nil { + return "", fmt.Errorf("failed to create KeyVault client for %s: %w", reqObj.vaultURL, err) + } + + secret, err := fetchSecretValue(ctx, keyVaultClient, reqObj.secretName, reqObj.secretVersion, nil) + if err != nil { + return "", fmt.Errorf("failed to get secret value: %w", err) + } + + return secret, nil +} + +type keyVaultRequestObject struct { + vaultURL string + secretName string + secretVersion string +} + +// parseKeyVaultURI extracts vault URL, secret name and optionally secret version from a KeyVault URI. +// Example URI: https://myvault.vault.azure.net/secrets/mysecret/myversion +func parseKeyVaultURI(kVRef string) (keyVaultRequestObject, error) { + parts := strings.Split(strings.TrimPrefix(kVRef, "https://"), "/") + + if len(parts) < 3 { + return keyVaultRequestObject{}, fmt.Errorf("invalid Key Vault reference: %s", kVRef) + } + + kv := keyVaultRequestObject{ + vaultURL: "https://" + parts[0], + secretName: parts[2], + } + if len(parts) > secretLengthWithVersion { + kv.secretVersion = parts[3] + } + + return kv, nil +} + +// fetchSecretValue retrieves the secret value from KeyVault. +func fetchSecretValue(ctx context.Context, client KeyVaultClient, secretName, secretVersion string, options *azsecrets.GetSecretOptions) (string, error) { + resp, err := client.GetSecret(ctx, secretName, secretVersion, options) + if err != nil { + return "", fmt.Errorf("failed to get secret from Key Vault: %w", err) + } + + if resp.Value != nil { + return *resp.Value, nil + } + + return "", fmt.Errorf("secret with name %s has no value", secretName) +} diff --git a/parsers/azure-app-config_test.go b/parsers/azure-app-config_test.go new file mode 100644 index 0000000..d93d07a --- /dev/null +++ b/parsers/azure-app-config_test.go @@ -0,0 +1,200 @@ +package parsers + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestAzureConfigParser_Parse_MultiPageAndKeyVaultRef(t *testing.T) { + firstKey := "firstKey" + firstValue := "firstValue" + + keyVaultRefSettingKey := "kvSecretKey" + secretUriValue := "{\"uri\":\"https://mykeyvault.vault.azure.net/secrets/mySecretName/0123456789abcdef\"}" //nolint + secretContentType := keyVaultRef + + thirdKey := "thirdKey" + thirdValue := "thirdValue" + + mockPager := &MockMultiPagePager{ + pages: [][]azappconfig.Setting{ + { + {Key: &firstKey, Value: &firstValue}, + {Key: &keyVaultRefSettingKey, Value: &secretUriValue, ContentType: &secretContentType}, + }, + { + {Key: &thirdKey, Value: &thirdValue}, + }, + }, + } + + parser := AzureConfigParser{ + settingsPager: mockPager, + secretClientFactory: func(url string) (KeyVaultClient, error) { + return MockSecretClient{secret: "resolvedSecretValue"}, nil + }, + } + + configValues, err := parser.Parse() + require.NoError(t, err) + + assert.Equal(t, 3, len(configValues)) + assert.Equal(t, firstValue, configValues[firstKey]) + assert.Equal(t, "resolvedSecretValue", configValues[keyVaultRefSettingKey]) + assert.Equal(t, thirdValue, configValues[thirdKey]) +} + +type MockMultiPagePager struct { + current int + pages [][]azappconfig.Setting +} + +func (m *MockMultiPagePager) More() bool { + return m.current < len(m.pages) +} + +func (m *MockMultiPagePager) NextPage(_ context.Context) (azappconfig.ListSettingsPageResponse, error) { + if !m.More() { + return azappconfig.ListSettingsPageResponse{}, fmt.Errorf("no more pages") + } + page := m.pages[m.current] + m.current++ + return azappconfig.ListSettingsPageResponse{Settings: page}, nil +} + +type MockSecretClient struct { + secret string +} + +func (m MockSecretClient) GetSecret(_ context.Context, _ string, _ string, _ *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + return azsecrets.GetSecretResponse{ + Secret: azsecrets.Secret{ + Value: &m.secret, + }, + }, nil +} + +func TestAzureConfigParser_resolveKeyVaultSecret(t *testing.T) { + secretValue := "secretValue" + type fields struct { + secretClientFactory func(url string) (KeyVaultClient, error) + } + type args struct { + ctx context.Context + keyVaultReference string + } + tests := []struct { + name string + fields fields + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Test resolveKeyVaultSecret with valid keyVaultReference", + fields: fields{ + secretClientFactory: func(url string) (KeyVaultClient, error) { + return MockSecretClient{ + secret: secretValue, + }, nil + }, + }, + args: args{ + ctx: context.Background(), + keyVaultReference: "{\"uri\":\"https://mykeyvault.vault.azure.net/secrets/mySecretName/0123456789abcdef\"}", + }, + want: secretValue, + wantErr: assert.NoError, + }, + { + name: "Test resolveKeyVaultSecret with invalid keyVaultReference", + fields: fields{ + secretClientFactory: func(url string) (KeyVaultClient, error) { + return MockSecretClient{ + secret: secretValue, + }, nil + }, + }, + + args: args{ + ctx: context.Background(), + keyVaultReference: "{\"uri\":\"https://mykeyvault.vault.azure.net/secrets\"}", + }, + want: "", + wantErr: assert.Error, + }, + { + name: "Test resolveKeyVaultSecret creating key vault client fails", + fields: fields{ + secretClientFactory: func(url string) (KeyVaultClient, error) { + return nil, fmt.Errorf("failed to create key vault client") + }, + }, + args: args{ + ctx: context.Background(), + keyVaultReference: "{\"uri\":\"https://mykeyvault.vault.azure.net/secrets/mySecretName/0123456789abcdef\"}", + }, + want: "", + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &AzureConfigParser{ + secretClientFactory: tt.fields.secretClientFactory, + } + got, err := p.resolveKeyVaultSecret(tt.args.ctx, tt.args.keyVaultReference) + if !tt.wantErr(t, err, fmt.Sprintf("resolveKeyVaultSecret(%v, %v)", tt.args.ctx, tt.args.keyVaultReference)) { + return + } + assert.Equalf(t, tt.want, got, "resolveKeyVaultSecret(%v, %v)", tt.args.ctx, tt.args.keyVaultReference) + }) + } +} + +func Test_getSecretRequestObject(t *testing.T) { + type args struct { + kVRef string + } + tests := []struct { + name string + args args + want keyVaultRequestObject + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Test parseKeyVaultURI with valid kVRef", + args: args{ + kVRef: "https://vault-name.vault.azure.net/secrets/secret-name/secret-version", + }, + want: keyVaultRequestObject{ + vaultURL: "https://vault-name.vault.azure.net", + secretName: "secret-name", + secretVersion: "secret-version", + }, + wantErr: assert.NoError, + }, + { + name: "Test parseKeyVaultURI with invalid kVRef", + args: args{ + kVRef: "https://vault-name.vault.azure.net/secrets", + }, + want: keyVaultRequestObject{}, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseKeyVaultURI(tt.args.kVRef) + if !tt.wantErr(t, err, fmt.Sprintf("parseKeyVaultURI(%v)", tt.args.kVRef)) { + return + } + assert.Equalf(t, tt.want, got, "parseKeyVaultURI(%v)", tt.args.kVRef) + }) + } +}