Skip to content

Commit

Permalink
introduce getParametersbyPath and do other refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
oujonny committed May 15, 2023
1 parent 0fe104c commit 8ef3eda
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 28 deletions.
86 changes: 86 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,92 @@ For cross account access there is the need to configure the correct permissions
https://aws.amazon.com/premiumsupport/knowledge-center/secrets-manager-share-between-accounts
https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_examples_cross.html

### AWS System Manager Parameter Store

##### AWS Authentication
Refer to the [AWS SDK for Go V2
documentation](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) for
supplying AWS credentials. Supported credentials and the order in which they are loaded are
described [here](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials).

These are the parameters for AWS:
```
AVP_TYPE: awsssmparameterstore
AWS_REGION: Your AWS Region (Optional: defaults to us-east-2)
```

##### Examples

###### Path Annotation

```yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-ssmps-example
annotations:
avp.kubernetes.io/path: "test-aws-secret" # The name of your AWS Secret
stringData:
sample-secret: <test-secret>
type: Opaque
```

###### Inline Path

```yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-ssmps-example
stringData:
sample-secret: <path:test-aws-secret#test-secret>
type: Opaque
```

###### Versioned secrets

```yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-ssmps-example
annotations:
avp.kubernetes.io/path: "some-path/secret"
avp.kubernetes.io/secret-version: "123"
stringData:
sample-secret: <test-secret>
sample-secret-again: <path:some-path/secret#test-secret#223>
type: Opaque
```

###### Secret in the same account

The 'friendly' name of the secret can be used in this case.

```yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-example
stringData:
sample-secret: <path:test-aws-secret#test-secret>
type: Opaque
```

###### Secret in a different account

The arn of the secret needs to be used in this case:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: aws-example
stringData:
sample-secret: <path:arn:aws:secretsmanager:<REGION>:<ACCOUNT_NUMBER>:<SECRET_ID>#<key>>
type: Opaque
```

### GCP Secret Manager

##### GCP Authentication
Expand Down
67 changes: 46 additions & 21 deletions pkg/backends/awsparameterstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package backends

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/argoproj-labs/argocd-vault-plugin/pkg/utils"
"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -16,6 +16,10 @@ const (
)

type AWSSSMParameterStoreIface interface {
GetParametersByPath(ctx context.Context,
params *ssm.GetParametersByPathInput,
optFns ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error)

GetParameter(ctx context.Context,
params *ssm.GetParameterInput,
optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
Expand All @@ -39,45 +43,66 @@ func (a *AWSSSMParameterStore) Login() error {
}

// GetSecrets gets secrets from aws secrets manager and returns the formatted data
func (a *AWSSSMParameterStore) GetParameters(path, version string, annotations map[string]string) (map[string]interface{}, error) {
input := &ssm.GetParameterInput{
Name: aws.String(path),
WithDecryption: bool(true),
}

if version != "" {
*input.Name = fmt.Sprintf("%v:%v", *input.Name, version)
func (a *AWSSSMParameterStore) GetSecrets(path, version string, annotations map[string]string) (map[string]interface{}, error) {
input := &ssm.GetParametersByPathInput{
Path: aws.String(path),
Recursive: aws.Bool(false),
WithDecryption: aws.Bool(true),
}

utils.VerboseToStdErr("AWS SSM Parameter Store getting secret %s", path)
result, err := a.Client.GetParameter(context.TODO(), input)
utils.VerboseToStdErr("AWS SSM Parameter Store getting secrets by path %s", path)
result, err := a.Client.GetParametersByPath(context.TODO(), input)
if err != nil {
return nil, err
}

utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", result)
utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", &result)

var dat map[string]interface{}
data := make(map[string]interface{})

if result.Parameter.Value != nil {
err := json.Unmarshal([]byte(*result.Parameter.Value), &dat)
if err != nil {
return nil, err
if result.Parameters != nil {
for _, parameter := range result.Parameters {
// extract the parameter name from the path
split := strings.Split(*parameter.Name, "/")
parameterName := split[len(split)-1]

data[parameterName] = *parameter.Value
}
} else {
return nil, fmt.Errorf("Could not find secret %s", path)
return nil, fmt.Errorf("Could not find secret by path %s", path)
}

return dat, nil
return data, nil
}

// GetIndividualSecret will get the specific secret (placeholder) from the SM backend
// For AWS, we only support placeholders replaced from the k/v pairs of a secret which cannot be individually addressed
// So, we use GetSecrets and extract the specific placeholder we want
func (a *AWSSSMParameterStore) GetIndividualSecret(kvpath, secret, version string, annotations map[string]string) (interface{}, error) {
data, err := a.GetParameters(kvpath, version, annotations)
input := &ssm.GetParameterInput{
Name: aws.String(kvpath),
WithDecryption: aws.Bool(true),
}

if version != "" {
*input.Name = fmt.Sprintf("%v:%v", *input.Name, version)
}

utils.VerboseToStdErr("AWS SSM Parameter Store getting secret %s", kvpath)
result, err := a.Client.GetParameter(context.TODO(), input)
if err != nil {
return nil, err
}
return data[secret], nil

utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", &result)

data := make(map[string]interface{})

if result.Parameter.Value != nil {
data["parameter"] = *result.Parameter.Value
} else {
return nil, fmt.Errorf("Could not find secret %s", kvpath)
}

return data["parameter"], nil
}
37 changes: 30 additions & 7 deletions pkg/backends/awsparameterstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backends_test

import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
"reflect"
"strings"
Expand Down Expand Up @@ -37,11 +38,35 @@ func (m *mockSSMParameterStoreClient) GetParameter(ctx context.Context, input *s
return data, nil
}

func (m *mockSSMParameterStoreClient) GetParametersByPath(ctx context.Context, input *ssm.GetParametersByPathInput, options ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) {

data := &ssm.GetParametersByPathOutput{
Parameters: []types.Parameter{},
}

switch *input.Path {
case "test":
parameters := []types.Parameter{
{
Name: aws.String("test-secret"),
Value: aws.String("current-value"),
Type: types.ParameterTypeSecureString,
},
}

data = &ssm.GetParametersByPathOutput{
Parameters: parameters,
}
}

return data, nil
}

func TestAWSSSMParameterStoreGetSecrets(t *testing.T) {
ps := backends.NewAWSSSMParameterStoreBackend(&mockSSMParameterStoreClient{})

t.Run("Get secrets", func(t *testing.T) {
data, err := ps.GetParameters("test", "", map[string]string{})
data, err := ps.GetSecrets("test", "", map[string]string{})
if err != nil {
t.Fatalf("expected 0 errors but got: %s", err)
}
Expand All @@ -61,22 +86,20 @@ func TestAWSSSMParameterStoreGetSecrets(t *testing.T) {
t.Fatalf("expected 0 errors but got: %s", err)
}

expected := "previous-value"
expected := "{\"test-secret\":\"previous-value\"}"

if !reflect.DeepEqual(expected, secret) {
t.Errorf("expected: %s, got: %s.", expected, secret)
}
})

t.Run("Get secrets at specific version", func(t *testing.T) {
data, err := ps.GetParameters("test", "123", map[string]string{})
data, err := ps.GetIndividualSecret("test", "test-secret", "123", map[string]string{})
if err != nil {
t.Fatalf("expected 0 errors but got: %s", err)
}

expected := map[string]interface{}{
"test-secret": "previous-value",
}
expected := "{\"test-secret\":\"previous-value\"}"

if !reflect.DeepEqual(expected, data) {
t.Errorf("expected: %s, got: %s.", expected, data)
Expand All @@ -87,7 +110,7 @@ func TestAWSSSMParameterStoreGetSecrets(t *testing.T) {
func TestAWSSSMParameterStoreEmptyIfNoSecret(t *testing.T) {
sm := backends.NewAWSSSMParameterStoreBackend(&mockSSMParameterStoreClient{})

_, err := sm.GetParameters("empty", "", map[string]string{})
_, err := sm.GetSecrets("empty", "", map[string]string{})
if err == nil {
t.Fatalf("expected an error but got nil")
}
Expand Down

0 comments on commit 8ef3eda

Please sign in to comment.