From 9e83dee449312becbb63988946f771d7f53e8b81 Mon Sep 17 00:00:00 2001 From: Tim Bauer <30375389+bimtauer@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:54:00 +0200 Subject: [PATCH] Vault Secret Manager #patch (#343) * Start adding Vaul Secret manager Signed-off-by: Tim Bauer * Auto-update enumer Signed-off-by: Tim Bauer * Make verbose Signed-off-by: Tim Bauer * Revert to print Signed-off-by: Tim Bauer * Mark debug statements Signed-off-by: Tim Bauer * Remove prints, simplify vault Signed-off-by: Tim Bauer * Test format env var, print more Signed-off-by: Tim Bauer * Check annotations Signed-off-by: Tim Bauer * Try to retrieve annotations Signed-off-by: Tim Bauer * Attempt append annotation Signed-off-by: Tim Bauer * Test annotation injection Signed-off-by: Tim Bauer * Pre-populate only Signed-off-by: Tim Bauer * Utils func for vault secret annotation Signed-off-by: Tim Bauer * Add shorter id to avoid 63 char limit Signed-off-by: Tim Bauer * Rm print Signed-off-by: Tim Bauer * Set vault role from config Signed-off-by: Tim Bauer * Add tests Signed-off-by: Tim Bauer * Name coreIdl import Signed-off-by: Tim Bauer * Rm duplicate import Signed-off-by: Tim Bauer * Update documentation and naming Signed-off-by: Tim Bauer * Update pkg/webhook/utils.go Co-authored-by: Haytham Abuelfutuh Signed-off-by: Tim Bauer * Update pkg/webhook/vault_secret_manager.go There is a [workaround](https://www.vaultproject.io/docs/platform/k8s/injector/examples#environment-variable-example) which involves mounting a template formatted file that contains `export API_KEY="{{ .Data.data.api_key }}"` and then sourcing this file as an extra step. But unless the user takes this extra sourcing step, this is still file mounting. So I would go with this Error message since the user should be warned that the expected result from requesting Env var will not be achieved with this. Co-authored-by: Haytham Abuelfutuh Signed-off-by: Tim Bauer * Update pkg/webhook/vault_secret_manager.go Co-authored-by: Haytham Abuelfutuh Signed-off-by: Tim Bauer * Fix naming and indent Signed-off-by: Tim Bauer * Add handling of different kv version and test Signed-off-by: Tim Bauer * Remove print Signed-off-by: Tim Bauer * Add enumer for KV version Signed-off-by: Tim Bauer * Correct kvversion type Signed-off-by: Tim Bauer * Rm newlines from vault secret template Signed-off-by: Tim Bauer * Apply suggestions from code review Co-authored-by: Ketan Umare <16888709+kumare3@users.noreply.github.com> Signed-off-by: Tim Bauer * Add docstring Signed-off-by: Tim Bauer Co-authored-by: Haytham Abuelfutuh Co-authored-by: Ketan Umare <16888709+kumare3@users.noreply.github.com> --- flytepropeller/pkg/webhook/config/config.go | 38 +++- .../pkg/webhook/config/config_flags.go | 1 + .../pkg/webhook/config/config_flags_test.go | 14 ++ .../pkg/webhook/config/kvversion_enumer.go | 85 +++++++++ .../config/secretmanagertype_enumer.go | 13 +- .../pkg/webhook/k8s_secrets_test.go | 3 +- flytepropeller/pkg/webhook/secrets.go | 1 + flytepropeller/pkg/webhook/utils.go | 31 ++++ .../pkg/webhook/vault_secret_manager.go | 95 ++++++++++ .../pkg/webhook/vault_secret_manager_test.go | 171 ++++++++++++++++++ 10 files changed, 437 insertions(+), 15 deletions(-) create mode 100644 flytepropeller/pkg/webhook/config/kvversion_enumer.go create mode 100644 flytepropeller/pkg/webhook/vault_secret_manager.go create mode 100644 flytepropeller/pkg/webhook/vault_secret_manager_test.go diff --git a/flytepropeller/pkg/webhook/config/config.go b/flytepropeller/pkg/webhook/config/config.go index 8782457a93..7d78e99996 100644 --- a/flytepropeller/pkg/webhook/config/config.go +++ b/flytepropeller/pkg/webhook/config/config.go @@ -7,6 +7,7 @@ import ( ) //go:generate enumer --type=SecretManagerType --trimprefix=SecretManagerType -json -yaml +//go:generate enumer --type=KVVersion --trimprefix=KVVersion -json -yaml //go:generate pflags Config --default-var=DefaultConfig var ( @@ -30,6 +31,10 @@ var ( }, }, }, + VaultSecretManagerConfig: VaultSecretManagerConfig{ + Role: "flyte", + KVVersion: KVVersion2, + }, } configSection = config.MustRegisterSection("webhook", DefaultConfig) @@ -49,16 +54,30 @@ const ( // SecretManagerTypeAWS defines a secret manager webhook that injects a side car to pull secrets from AWS Secret // Manager and mount them to a local file system (in memory) and share that mount with other containers in the pod. SecretManagerTypeAWS + + // SecretManagerTypeVault defines a secret manager webhook that pulls secrets from Hashicorp Vault. + SecretManagerTypeVault +) + +// Defines with KV Engine Version to use with VaultSecretManager - https://www.vaultproject.io/docs/secrets/kv#kv-secrets-engine +type KVVersion int + +const ( + // KV v1 refers to unversioned secrets + KVVersion1 KVVersion = iota + // KV v2 refers to versioned secrets + KVVersion2 ) type Config struct { - MetricsPrefix string `json:"metrics-prefix" pflag:",An optional prefix for all published metrics."` - CertDir string `json:"certDir" pflag:",Certificate directory to use to write generated certs. Defaults to /etc/webhook/certs/"` - ListenPort int `json:"listenPort" pflag:",The port to use to listen to webhook calls. Defaults to 9443"` - ServiceName string `json:"serviceName" pflag:",The name of the webhook service."` - SecretName string `json:"secretName" pflag:",Secret name to write generated certs to."` - SecretManagerType SecretManagerType `json:"secretManagerType" pflag:"-,Secret manager type to use if secrets are not found in global secrets."` - AWSSecretManagerConfig AWSSecretManagerConfig `json:"awsSecretManager" pflag:",AWS Secret Manager config."` + MetricsPrefix string `json:"metrics-prefix" pflag:",An optional prefix for all published metrics."` + CertDir string `json:"certDir" pflag:",Certificate directory to use to write generated certs. Defaults to /etc/webhook/certs/"` + ListenPort int `json:"listenPort" pflag:",The port to use to listen to webhook calls. Defaults to 9443"` + ServiceName string `json:"serviceName" pflag:",The name of the webhook service."` + SecretName string `json:"secretName" pflag:",Secret name to write generated certs to."` + SecretManagerType SecretManagerType `json:"secretManagerType" pflag:"-,Secret manager type to use if secrets are not found in global secrets."` + AWSSecretManagerConfig AWSSecretManagerConfig `json:"awsSecretManager" pflag:",AWS Secret Manager config."` + VaultSecretManagerConfig VaultSecretManagerConfig `json:"vaultSecretManager" pflag:",Vault Secret Manager config."` } type AWSSecretManagerConfig struct { @@ -66,6 +85,11 @@ type AWSSecretManagerConfig struct { Resources corev1.ResourceRequirements `json:"resources" pflag:"-,Specifies resource requirements for the init container."` } +type VaultSecretManagerConfig struct { + Role string `json:"role" pflag:",Specifies the vault role to use"` + KVVersion KVVersion `json:"kvVersion" pflag:"-,The KV Engine Version. Defaults to 2. Use 1 for unversioned secrets. Refer to - https://www.vaultproject.io/docs/secrets/kv#kv-secrets-engine."` +} + func GetConfig() *Config { return configSection.GetConfig().(*Config) } diff --git a/flytepropeller/pkg/webhook/config/config_flags.go b/flytepropeller/pkg/webhook/config/config_flags.go index c93c9c2cc7..6dea588048 100755 --- a/flytepropeller/pkg/webhook/config/config_flags.go +++ b/flytepropeller/pkg/webhook/config/config_flags.go @@ -56,5 +56,6 @@ func (cfg Config) GetPFlagSet(prefix string) *pflag.FlagSet { cmdFlags.String(fmt.Sprintf("%v%v", prefix, "serviceName"), DefaultConfig.ServiceName, "The name of the webhook service.") cmdFlags.String(fmt.Sprintf("%v%v", prefix, "secretName"), DefaultConfig.SecretName, "Secret name to write generated certs to.") cmdFlags.String(fmt.Sprintf("%v%v", prefix, "awsSecretManager.sidecarImage"), DefaultConfig.AWSSecretManagerConfig.SidecarImage, "Specifies the sidecar docker image to use") + cmdFlags.String(fmt.Sprintf("%v%v", prefix, "vaultSecretManager.role"), DefaultConfig.VaultSecretManagerConfig.Role, "Specifies the vault role to use") return cmdFlags } diff --git a/flytepropeller/pkg/webhook/config/config_flags_test.go b/flytepropeller/pkg/webhook/config/config_flags_test.go index a539e5edd5..10b69e8455 100755 --- a/flytepropeller/pkg/webhook/config/config_flags_test.go +++ b/flytepropeller/pkg/webhook/config/config_flags_test.go @@ -183,4 +183,18 @@ func TestConfig_SetFlags(t *testing.T) { } }) }) + t.Run("Test_vaultSecretManager.role", func(t *testing.T) { + + t.Run("Override", func(t *testing.T) { + testValue := "1" + + cmdFlags.Set("vaultSecretManager.role", testValue) + if vString, err := cmdFlags.GetString("vaultSecretManager.role"); err == nil { + testDecodeJson_Config(t, fmt.Sprintf("%v", vString), &actual.VaultSecretManagerConfig.Role) + + } else { + assert.FailNow(t, err.Error()) + } + }) + }) } diff --git a/flytepropeller/pkg/webhook/config/kvversion_enumer.go b/flytepropeller/pkg/webhook/config/kvversion_enumer.go new file mode 100644 index 0000000000..f9ac3fc6e2 --- /dev/null +++ b/flytepropeller/pkg/webhook/config/kvversion_enumer.go @@ -0,0 +1,85 @@ +// Code generated by "enumer --type=KVVersion --trimprefix=KVVersion -json -yaml"; DO NOT EDIT. + +// +package config + +import ( + "encoding/json" + "fmt" +) + +const _KVVersionName = "12" + +var _KVVersionIndex = [...]uint8{0, 1, 2} + +func (i KVVersion) String() string { + if i < 0 || i >= KVVersion(len(_KVVersionIndex)-1) { + return fmt.Sprintf("KVVersion(%d)", i) + } + return _KVVersionName[_KVVersionIndex[i]:_KVVersionIndex[i+1]] +} + +var _KVVersionValues = []KVVersion{0, 1} + +var _KVVersionNameToValueMap = map[string]KVVersion{ + _KVVersionName[0:1]: 0, + _KVVersionName[1:2]: 1, +} + +// KVVersionString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func KVVersionString(s string) (KVVersion, error) { + if val, ok := _KVVersionNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to KVVersion values", s) +} + +// KVVersionValues returns all values of the enum +func KVVersionValues() []KVVersion { + return _KVVersionValues +} + +// IsAKVVersion returns "true" if the value is listed in the enum definition. "false" otherwise +func (i KVVersion) IsAKVVersion() bool { + for _, v := range _KVVersionValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for KVVersion +func (i KVVersion) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for KVVersion +func (i *KVVersion) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("KVVersion should be a string, got %s", data) + } + + var err error + *i, err = KVVersionString(s) + return err +} + +// MarshalYAML implements a YAML Marshaler for KVVersion +func (i KVVersion) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +// UnmarshalYAML implements a YAML Unmarshaler for KVVersion +func (i *KVVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + var err error + *i, err = KVVersionString(s) + return err +} diff --git a/flytepropeller/pkg/webhook/config/secretmanagertype_enumer.go b/flytepropeller/pkg/webhook/config/secretmanagertype_enumer.go index a66209db15..19d3979ed4 100644 --- a/flytepropeller/pkg/webhook/config/secretmanagertype_enumer.go +++ b/flytepropeller/pkg/webhook/config/secretmanagertype_enumer.go @@ -8,9 +8,9 @@ import ( "fmt" ) -const _SecretManagerTypeName = "GlobalK8sAWS" +const _SecretManagerTypeName = "GlobalK8sAWSVault" -var _SecretManagerTypeIndex = [...]uint8{0, 6, 9, 12} +var _SecretManagerTypeIndex = [...]uint8{0, 6, 9, 12, 17} func (i SecretManagerType) String() string { if i < 0 || i >= SecretManagerType(len(_SecretManagerTypeIndex)-1) { @@ -19,12 +19,13 @@ func (i SecretManagerType) String() string { return _SecretManagerTypeName[_SecretManagerTypeIndex[i]:_SecretManagerTypeIndex[i+1]] } -var _SecretManagerTypeValues = []SecretManagerType{0, 1, 2} +var _SecretManagerTypeValues = []SecretManagerType{0, 1, 2, 3} var _SecretManagerTypeNameToValueMap = map[string]SecretManagerType{ - _SecretManagerTypeName[0:6]: 0, - _SecretManagerTypeName[6:9]: 1, - _SecretManagerTypeName[9:12]: 2, + _SecretManagerTypeName[0:6]: 0, + _SecretManagerTypeName[6:9]: 1, + _SecretManagerTypeName[9:12]: 2, + _SecretManagerTypeName[12:17]: 3, } // SecretManagerTypeString retrieves an enum value from the enum constants string name. diff --git a/flytepropeller/pkg/webhook/k8s_secrets_test.go b/flytepropeller/pkg/webhook/k8s_secrets_test.go index 8186dde5fc..7f31e97dde 100644 --- a/flytepropeller/pkg/webhook/k8s_secrets_test.go +++ b/flytepropeller/pkg/webhook/k8s_secrets_test.go @@ -6,7 +6,6 @@ import ( "github.com/go-test/deep" - "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" coreIdl "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" corev1 "k8s.io/api/core/v1" ) @@ -179,7 +178,7 @@ func TestK8sSecretInjector_Inject(t *testing.T) { ctx := context.Background() type args struct { - secret *core.Secret + secret *coreIdl.Secret p *corev1.Pod } tests := []struct { diff --git a/flytepropeller/pkg/webhook/secrets.go b/flytepropeller/pkg/webhook/secrets.go index 69deb73697..98b4d15f82 100644 --- a/flytepropeller/pkg/webhook/secrets.go +++ b/flytepropeller/pkg/webhook/secrets.go @@ -75,6 +75,7 @@ func NewSecretsMutator(cfg *config.Config, _ promutils.Scope) *SecretsMutator { NewGlobalSecrets(secretmanager.NewFileEnvSecretManager(secretmanager.GetConfig())), NewK8sSecretsInjector(), NewAWSSecretManagerInjector(cfg.AWSSecretManagerConfig), + NewVaultSecretManagerInjector(cfg.VaultSecretManagerConfig), }, } } diff --git a/flytepropeller/pkg/webhook/utils.go b/flytepropeller/pkg/webhook/utils.go index 75573c8e31..29f6643262 100644 --- a/flytepropeller/pkg/webhook/utils.go +++ b/flytepropeller/pkg/webhook/utils.go @@ -1,14 +1,17 @@ package webhook import ( + "fmt" "path/filepath" "strings" "github.com/flyteorg/flyteplugins/go/tasks/pluginmachinery/encoding" + "github.com/flyteorg/flytepropeller/pkg/webhook/config" "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/uuid" ) func hasEnvVar(envVars []corev1.EnvVar, envVarKey string) bool { @@ -115,3 +118,31 @@ func AppendVolume(volumes []corev1.Volume, volume corev1.Volume) []corev1.Volume return append(volumes, volume) } + +func CreateVaultAnnotationsForSecret(secret *core.Secret, kvversion config.KVVersion) (map[string]string, error) { + // Creates three grouped annotations "agent-inject-secret", "agent-inject-file" and "agent-inject-template" + // for a given secret request and KV engine version. The annotations respectively handle: 1. retrieving the + // secret from a vault path specified in secret.Group, 2. storing it in a file named after secret.Group/secret.Key + // and 3. creating a template that retrieves only secret.Key from the multiple k:v pairs present in a vault secret. + id := string(uuid.NewUUID()) + + // Set the consul template language query depending on the KV Secrets Engine version. + // Version 1 stores plain k:v pairs under .Data, version 2 supports versioned secrets + // and wraps the k:v pairs into an additional subfield. + var query string + if kvversion == config.KVVersion1 { + query = ".Data" + } else if kvversion == config.KVVersion2 { + query = ".Data.data" + } else { + err := fmt.Errorf("unsupported KV Version %v, supported versions are 1 and 2", kvversion) + return nil, err + } + template := fmt.Sprintf(`{{- with secret "%s" -}}{{ %s.%s }}{{- end -}}`, secret.Group, query, secret.Key) + secretVaultAnnotations := map[string]string{ + fmt.Sprintf("vault.hashicorp.com/agent-inject-secret-%s", id): secret.Group, + fmt.Sprintf("vault.hashicorp.com/agent-inject-file-%s", id): fmt.Sprintf("%s/%s", secret.Group, secret.Key), + fmt.Sprintf("vault.hashicorp.com/agent-inject-template-%s", id): template, + } + return secretVaultAnnotations, nil +} diff --git a/flytepropeller/pkg/webhook/vault_secret_manager.go b/flytepropeller/pkg/webhook/vault_secret_manager.go new file mode 100644 index 0000000000..841e3f45c7 --- /dev/null +++ b/flytepropeller/pkg/webhook/vault_secret_manager.go @@ -0,0 +1,95 @@ +package webhook + +import ( + "context" + "fmt" + "os" + "path/filepath" + + coreIdl "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" + "github.com/flyteorg/flyteplugins/go/tasks/pluginmachinery/utils" + "github.com/flyteorg/flytepropeller/pkg/webhook/config" + "github.com/flyteorg/flytestdlib/logger" + corev1 "k8s.io/api/core/v1" +) + +var ( + VaultSecretPathPrefix = []string{string(os.PathSeparator), "etc", "flyte", "secrets"} +) + +// VaultSecretManagerInjector allows injecting of secrets into pods by leveraging an existing deployment of Vault Agent +// Vault Agent functions as an additional webhook that is triggered through annotations and then retrieves and mounts +// the requested secrets from Vault. This injector parses a secret Request into vault annotations, interpreting the secret +// Group as the vault secret path and the secret Key as the key for which to extract a value from a Vault secret. +// It supports adding multiple secrets. (The common annotations will simply be overwritten if added several times) +// Note that you need to configure the Vault role that this injector will try to use and add Vault policies for +// the service account and namespaces that your workflows run under. +// Files will be mounted at /etc/flyte/secrets// +type VaultSecretManagerInjector struct { + cfg config.VaultSecretManagerConfig +} + +func (i VaultSecretManagerInjector) Type() config.SecretManagerType { + return config.SecretManagerTypeVault +} + +func (i VaultSecretManagerInjector) Inject(ctx context.Context, secret *coreIdl.Secret, p *corev1.Pod) (newP *corev1.Pod, injected bool, err error) { + if len(secret.Group) == 0 || len(secret.Key) == 0 { + return nil, false, fmt.Errorf("Vault Secrets Webhook requires both key and group to be set. "+ + "Secret: [%v]", secret) + } + + switch secret.MountRequirement { + case coreIdl.Secret_ANY: + fallthrough + case coreIdl.Secret_FILE: + // Set environment variable to let the container know where to find the mounted files. + defaultDirEnvVar := corev1.EnvVar{ + Name: SecretPathDefaultDirEnvVar, + Value: filepath.Join(VaultSecretPathPrefix...), + } + + p.Spec.InitContainers = AppendEnvVars(p.Spec.InitContainers, defaultDirEnvVar) + p.Spec.Containers = AppendEnvVars(p.Spec.Containers, defaultDirEnvVar) + + // Sets an empty prefix to let the containers know the file names will match the secret keys as-is. + prefixEnvVar := corev1.EnvVar{ + Name: SecretPathFilePrefixEnvVar, + Value: "", + } + + p.Spec.InitContainers = AppendEnvVars(p.Spec.InitContainers, prefixEnvVar) + p.Spec.Containers = AppendEnvVars(p.Spec.Containers, prefixEnvVar) + + commonVaultAnnotations := map[string]string{ + "vault.hashicorp.com/agent-inject": "true", + "vault.hashicorp.com/secret-volume-path": filepath.Join(VaultSecretPathPrefix...), + "vault.hashicorp.com/role": i.cfg.Role, + "vault.hashicorp.com/agent-pre-populate-only": "true", + } + + secretVaultAnnotations, err := CreateVaultAnnotationsForSecret(secret, i.cfg.KVVersion) + // Creating annotations can break with an unsupported KVVersion + if err != nil { + return p, false, err + } + + p.ObjectMeta.Annotations = utils.UnionMaps(p.ObjectMeta.Annotations, commonVaultAnnotations) + p.ObjectMeta.Annotations = utils.UnionMaps(p.ObjectMeta.Annotations, secretVaultAnnotations) + + case coreIdl.Secret_ENV_VAR: + return p, false, fmt.Errorf("Env_Var is not a supported mount requirement for Vault Secret Manager") + default: + err := fmt.Errorf("unrecognized mount requirement [%v] for secret [%v]", secret.MountRequirement.String(), secret.Key) + logger.Error(ctx, err) + return p, false, err + } + + return p, true, nil +} + +func NewVaultSecretManagerInjector(cfg config.VaultSecretManagerConfig) VaultSecretManagerInjector { + return VaultSecretManagerInjector{ + cfg: cfg, + } +} diff --git a/flytepropeller/pkg/webhook/vault_secret_manager_test.go b/flytepropeller/pkg/webhook/vault_secret_manager_test.go new file mode 100644 index 0000000000..9af9b6dd48 --- /dev/null +++ b/flytepropeller/pkg/webhook/vault_secret_manager_test.go @@ -0,0 +1,171 @@ +package webhook + +import ( + "context" + "fmt" + "testing" + + coreIdl "github.com/flyteorg/flyteidl/gen/pb-go/flyteidl/core" + "github.com/flyteorg/flytepropeller/pkg/webhook/config" + "github.com/go-test/deep" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + // We expect these outputs for each successful test + PodSpec = corev1.PodSpec{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{ + { + Name: "container1", + Env: []corev1.EnvVar{ + { + Name: "FLYTE_SECRETS_DEFAULT_DIR", + Value: "/etc/flyte/secrets", + }, + { + Name: "FLYTE_SECRETS_FILE_PREFIX", + }, + }, + }, + }, + } +) + +func RetrieveUUID(annotations map[string]string) string { + // helper function to retrieve the random uuid from output before comparing + var uuid string + for k := range annotations { + if len(k) > 39 && k[:39] == "vault.hashicorp.com/agent-inject-secret" { + uuid = k[40:] + } + } + return uuid +} + +func ExpectedKVv1(uuid string) *corev1.Pod { + // Injects uuid into expected output for KV v1 secrets + expected := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "vault.hashicorp.com/agent-inject": "true", + "vault.hashicorp.com/secret-volume-path": "/etc/flyte/secrets", + "vault.hashicorp.com/role": "flyte", + "vault.hashicorp.com/agent-pre-populate-only": "true", + fmt.Sprintf("vault.hashicorp.com/agent-inject-secret-%s", uuid): "foo", + fmt.Sprintf("vault.hashicorp.com/agent-inject-file-%s", uuid): "foo/bar", + fmt.Sprintf("vault.hashicorp.com/agent-inject-template-%s", uuid): `{{- with secret "foo" -}}{{ .Data.bar }}{{- end -}}`, + }, + }, + Spec: PodSpec, + } + return expected +} + +func ExpectedKVv2(uuid string) *corev1.Pod { + // Injects uuid into expected output for KV v2 secrets + expected := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "vault.hashicorp.com/agent-inject": "true", + "vault.hashicorp.com/secret-volume-path": "/etc/flyte/secrets", + "vault.hashicorp.com/role": "flyte", + "vault.hashicorp.com/agent-pre-populate-only": "true", + fmt.Sprintf("vault.hashicorp.com/agent-inject-secret-%s", uuid): "foo", + fmt.Sprintf("vault.hashicorp.com/agent-inject-file-%s", uuid): "foo/bar", + fmt.Sprintf("vault.hashicorp.com/agent-inject-template-%s", uuid): `{{- with secret "foo" -}}{{ .Data.data.bar }}{{- end -}}`, + }, + }, + Spec: PodSpec, + } + return expected +} + +func NewInputPod() *corev1.Pod { + // Need to create a new Pod for every test since annotations are otherwise appended to original reference object + p := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container1", + }, + }, + }, + } + return p +} + +func TestVaultSecretManagerInjector_Inject(t *testing.T) { + inputSecret := &coreIdl.Secret{ + Group: "foo", + Key: "bar", + } + + ctx := context.Background() + type args struct { + cfg config.VaultSecretManagerConfig + secret *coreIdl.Secret + p *corev1.Pod + } + tests := []struct { + name string + args args + want func(string) *corev1.Pod + wantErr bool + }{ + { + name: "KVv1 Secret", + args: args{ + cfg: config.VaultSecretManagerConfig{Role: "flyte", KVVersion: config.KVVersion1}, + secret: inputSecret, + p: NewInputPod(), + }, + want: ExpectedKVv1, + wantErr: false, + }, + { + name: "KVv2 Secret", + args: args{ + cfg: config.VaultSecretManagerConfig{Role: "flyte", KVVersion: config.KVVersion2}, + secret: inputSecret, + p: NewInputPod(), + }, + want: ExpectedKVv2, + wantErr: false, + }, + { + name: "Unsupported KV version", + args: args{ + cfg: config.VaultSecretManagerConfig{Role: "flyte", KVVersion: 3}, + secret: inputSecret, + p: NewInputPod(), + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := NewVaultSecretManagerInjector(tt.args.cfg) + got, _, err := i.Inject(ctx, tt.args.secret, tt.args.p) + + if (err != nil) != tt.wantErr { + t.Errorf("Inject() error = %v, wantErr %v", err, tt.wantErr) + return + } else if err != nil { + return + } + + uuid := RetrieveUUID(got.ObjectMeta.Annotations) + expected := tt.want(uuid) + if diff := deep.Equal(got, expected); diff != nil { + t.Errorf("Inject() Diff = %v\r\n got = %v\r\n want = %v", diff, got, expected) + } + }) + } +}