From b36bf5cbae4fde75945e15ea87d99f75822845d3 Mon Sep 17 00:00:00 2001 From: Yves Galante Date: Fri, 1 Sep 2023 09:32:02 +0200 Subject: [PATCH] feat(azure): Add test on AzureKeyvault backend #421 --- .github/workflows/pipeline.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 2 +- go.mod | 2 +- pkg/backends/azurekeyvault.go | 35 ++++++- pkg/backends/azurekeyvault_test.go | 156 +++++++++++++++++++++++++++++ pkg/config/config.go | 4 +- 7 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 pkg/backends/azurekeyvault_test.go diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 1a4b5a6b..e43b5c1c 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -42,7 +42,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.17.8 + go-version: 1.18.10 - name: Quality checks run: make quality diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af13d34c..57dbd107 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.17.8 + go-version: 1.18.10 - name: Install git-chglog run: go install github.com/git-chglog/git-chglog/cmd/git-chglog@latest diff --git a/Dockerfile b/Dockerfile index 915b4b86..9b77cfa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM index.docker.io/golang:1.17@sha256:55636cf1983628109e569690596b85077f45aca810a77904e8afad48b49aa500 +FROM index.docker.io/golang:1.18.10@sha256:312240b2b016c2eecb2e27a8a4265fcafe6612bd9ebd5befb42c21672ed003ee ADD go.mod go.mod ADD go.sum go.sum diff --git a/go.mod b/go.mod index bcde6429..c0ba4529 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/argoproj-labs/argocd-vault-plugin -go 1.17 +go 1.18 replace ( github.com/googleapis/gnostic v0.5.7 => github.com/googleapis/gnostic v0.5.5 diff --git a/pkg/backends/azurekeyvault.go b/pkg/backends/azurekeyvault.go index 07842431..16bf31f3 100644 --- a/pkg/backends/azurekeyvault.go +++ b/pkg/backends/azurekeyvault.go @@ -3,6 +3,8 @@ package backends import ( "context" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" @@ -11,13 +13,38 @@ import ( // AzureKeyVault is a struct for working with an Azure Key Vault backend type AzureKeyVault struct { - Credential *azidentity.DefaultAzureCredential + Credential *azidentity.DefaultAzureCredential + ClientBuilder func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (ClientProxy, error) +} + +// ClientProxy help to render it testable +type ClientProxy struct { + Client azSecretsClient +} + +type azSecretsClient interface { + GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) + NewListSecretPropertiesPager(options *azsecrets.ListSecretPropertiesOptions) *runtime.Pager[azsecrets.ListSecretPropertiesResponse] +} + +func (p *ClientProxy) NewListSecretPropertiesPager(options *azsecrets.ListSecretPropertiesOptions) *runtime.Pager[azsecrets.ListSecretPropertiesResponse] { + return p.Client.NewListSecretPropertiesPager(options) +} + +func (p *ClientProxy) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + return p.Client.GetSecret(ctx, name, version, options) } // NewAzureKeyVaultBackend initializes a new Azure Key Vault backend -func NewAzureKeyVaultBackend(credential *azidentity.DefaultAzureCredential) *AzureKeyVault { +func NewAzureKeyVaultBackend(credential *azidentity.DefaultAzureCredential, clientBuilder func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (*azsecrets.Client, error)) *AzureKeyVault { return &AzureKeyVault{ Credential: credential, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (ClientProxy, error) { + var client, err = clientBuilder(vaultURL, credential, options) + return ClientProxy{ + Client: client, + }, err + }, } } @@ -37,7 +64,7 @@ func (a *AzureKeyVault) GetSecrets(kvpath string, version string, _ map[string]s VerboseOptionalVersion("Azure Key Vault list all secrets from vault %s", version, kvpath) - client, err := azsecrets.NewClient(kvpath, a.Credential, nil) + client, err := a.ClientBuilder(kvpath, a.Credential, nil) if err != nil { return nil, err } @@ -90,7 +117,7 @@ func (a *AzureKeyVault) GetIndividualSecret(kvpath, secret, version string, anno VerboseOptionalVersion("Azure Key Vault getting individual secret %s from vault %s", version, secret, kvpath) kvpath = fmt.Sprintf("https://%s.vault.azure.net", kvpath) - client, err := azsecrets.NewClient(kvpath, a.Credential, nil) + client, err := a.ClientBuilder(kvpath, a.Credential, nil) if err != nil { return nil, err } diff --git a/pkg/backends/azurekeyvault_test.go b/pkg/backends/azurekeyvault_test.go new file mode 100644 index 00000000..fe2c7f53 --- /dev/null +++ b/pkg/backends/azurekeyvault_test.go @@ -0,0 +1,156 @@ +package backends_test + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" + "github.com/argoproj-labs/argocd-vault-plugin/pkg/backends" + "reflect" + "testing" +) + +type mockClientProxy struct { +} + +type MyError struct{} + +func (m *MyError) Error() string { + return "boom" +} + +func makeSecretProperties(id azsecrets.ID, enable bool) *azsecrets.SecretProperties { + return &azsecrets.SecretProperties{ + ID: &id, + Attributes: &azsecrets.SecretAttributes{ + Enabled: &enable, + }, + } +} + +func makeResponse(id azsecrets.ID, value string, err error) (azsecrets.GetSecretResponse, error) { + return azsecrets.GetSecretResponse{ + Secret: azsecrets.Secret{ + ID: &id, + Value: &value, + }, + }, err +} + +func (c *mockClientProxy) NewListSecretPropertiesPager(options *azsecrets.ListSecretPropertiesOptions) *runtime.Pager[azsecrets.ListSecretPropertiesResponse] { + var pageCount = 0 + pager := runtime.NewPager(runtime.PagingHandler[azsecrets.ListSecretPropertiesResponse]{ + More: func(current azsecrets.ListSecretPropertiesResponse) bool { + return pageCount == 0 + }, + Fetcher: func(ctx context.Context, current *azsecrets.ListSecretPropertiesResponse) (azsecrets.ListSecretPropertiesResponse, error) { + pageCount++ + var a []*azsecrets.SecretProperties + a = append(a, makeSecretProperties("https://myvaultname.vault.azure.net/keys/simple/v2", true)) + a = append(a, makeSecretProperties("https://myvaultname.vault.azure.net/keys/disabled/v2", false)) + return azsecrets.ListSecretPropertiesResponse{ + SecretPropertiesListResult: azsecrets.SecretPropertiesListResult{ + Value: a, + }, + }, nil + }, + }) + return pager +} + +func (c *mockClientProxy) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + if name == "simple" && (version == "" || version == "v1") { + return makeResponse("simple,v2", "a_value_v1", nil) + } else if name == "simple" && version == "v2" { + return makeResponse("simple,v2", "a_value_v2", nil) + } + return makeResponse("", "", &MyError{}) +} + +func NewAzureKeyVaultBackendMock() *backends.AzureKeyVault { + return &backends.AzureKeyVault{ + Credential: nil, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (backends.ClientProxy, error) { + return backends.ClientProxy{ + Client: &mockClientProxy{}, + }, nil + }, + } +} + +func TestAzGetSecret(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var data, err = keyVault.GetIndividualSecret("keyvault", "simple", "", nil) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := "a_value_v1" + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } +} + +func TestAzGetSecretWithVersion(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var data, err = keyVault.GetIndividualSecret("keyvault", "simple", "v2", nil) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := "a_value_v2" + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } +} + +func TestAzGetSecretWithError(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var _, err = keyVault.GetIndividualSecret("keyvault", "not_existing", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } +} + +func TestAzGetSecrets(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var res, err = keyVault.GetSecrets("keyvault", "", nil) + + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v1", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } +} + +func TestAzGetSecretsVersionV1(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var res, err = keyVault.GetSecrets("keyvault", "v1", nil) + + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v1", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } +} + +func TestAzGetSecretsVersionV2(t *testing.T) { + var keyVault = NewAzureKeyVaultBackendMock() + var res, err = keyVault.GetSecrets("keyvault", "v2", nil) + + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v2", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 7fff77e8..5dc80940 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "os" "strconv" "strings" @@ -191,7 +192,8 @@ func New(v *viper.Viper, co *Options) (*Config, error) { if err != nil { return nil, err } - backend = backends.NewAzureKeyVaultBackend(cred) + + backend = backends.NewAzureKeyVaultBackend(cred, azsecrets.NewClient) } case types.Sopsbackend: {