From fe9158f2bd332fb99d8cdef2fa2644213dc36a3a Mon Sep 17 00:00:00 2001 From: Theron Voran Date: Tue, 3 Sep 2024 00:07:10 -0700 Subject: [PATCH] HVS: basic dynamic secrets support Everything but caching dynamic secret responses --- api/v1beta1/hcpvaultsecretsapp_types.go | 30 ++++ api/v1beta1/zz_generated.deepcopy.go | 62 ++++++++- ...ets.hashicorp.com_hcpvaultsecretsapps.yaml | 35 +++++ ...ets.hashicorp.com_hcpvaultsecretsapps.yaml | 35 +++++ controllers/hcpvaultsecretsapp_controller.go | 117 ++++++++++++++-- .../hcpvaultsecretsapp_controller_test.go | 130 ++++++++++++++++++ docs/api/api-reference.md | 53 +++++++ internal/helpers/secrets.go | 9 +- 8 files changed, 460 insertions(+), 11 deletions(-) create mode 100644 controllers/hcpvaultsecretsapp_controller_test.go diff --git a/api/v1beta1/hcpvaultsecretsapp_types.go b/api/v1beta1/hcpvaultsecretsapp_types.go index 4c9500f61..9ca8229b4 100644 --- a/api/v1beta1/hcpvaultsecretsapp_types.go +++ b/api/v1beta1/hcpvaultsecretsapp_types.go @@ -32,6 +32,33 @@ type HCPVaultSecretsAppSpec struct { // Destination provides configuration necessary for syncing the HCP Vault // Application secrets to Kubernetes. Destination Destination `json:"destination"` + // SyncConfig configures sync behavior from HVS to VSO + SyncConfig *HVSSyncConfig `json:"syncConfig,omitempty"` +} + +// HVSSyncConfig configures sync behavior from HVS to VSO +type HVSSyncConfig struct { + // Dynamic configures sync behavior for dynamic secrets. + Dynamic *HVSDynamicSyncConfig `json:"dynamic,omitempty"` +} + +// HVSDynamicSyncConfig configures sync behavior for HVS dynamic secrets. +type HVSDynamicSyncConfig struct { + // RenewalPercent is the percent out of 100 of a dynamic secret's TTL when + // new secrets are generated. Defaults to 67 percent plus jitter. + // +kubebuilder:default=67 + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=90 + RenewalPercent int `json:"renewalPercent,omitempty"` +} + +// HVSDynamicStatus defines the observed state of a dynamic secret within an HCP +// Vault Secrets App +type HVSDynamicStatus struct { + Name string `json:"name,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + ExpiresAt string `json:"expiresAt,omitempty"` + TTL string `json:"ttl,omitempty"` } // HCPVaultSecretsAppStatus defines the observed state of HCPVaultSecretsApp @@ -47,6 +74,9 @@ type HCPVaultSecretsAppStatus struct { // The SecretMac is also used to detect drift in the Destination Secret's Data. // If drift is detected the data will be synced to the Destination. SecretMAC string `json:"secretMAC,omitempty"` + // DynamicSecrets lists the last observed state of any dynamic secrets + // within the HCP Vault Secrets App + DynamicSecrets []HVSDynamicStatus `json:"dynamicSecrets,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index c2d54c524..4f48fef97 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -167,7 +167,7 @@ func (in *HCPVaultSecretsApp) DeepCopyInto(out *HCPVaultSecretsApp) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPVaultSecretsApp. @@ -229,6 +229,11 @@ func (in *HCPVaultSecretsAppSpec) DeepCopyInto(out *HCPVaultSecretsAppSpec) { copy(*out, *in) } in.Destination.DeepCopyInto(&out.Destination) + if in.SyncConfig != nil { + in, out := &in.SyncConfig, &out.SyncConfig + *out = new(HVSSyncConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPVaultSecretsAppSpec. @@ -244,6 +249,11 @@ func (in *HCPVaultSecretsAppSpec) DeepCopy() *HCPVaultSecretsAppSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HCPVaultSecretsAppStatus) DeepCopyInto(out *HCPVaultSecretsAppStatus) { *out = *in + if in.DynamicSecrets != nil { + in, out := &in.DynamicSecrets, &out.DynamicSecrets + *out = make([]HVSDynamicStatus, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPVaultSecretsAppStatus. @@ -256,6 +266,56 @@ func (in *HCPVaultSecretsAppStatus) DeepCopy() *HCPVaultSecretsAppStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HVSDynamicStatus) DeepCopyInto(out *HVSDynamicStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HVSDynamicStatus. +func (in *HVSDynamicStatus) DeepCopy() *HVSDynamicStatus { + if in == nil { + return nil + } + out := new(HVSDynamicStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HVSDynamicSyncConfig) DeepCopyInto(out *HVSDynamicSyncConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HVSDynamicSyncConfig. +func (in *HVSDynamicSyncConfig) DeepCopy() *HVSDynamicSyncConfig { + if in == nil { + return nil + } + out := new(HVSDynamicSyncConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HVSSyncConfig) DeepCopyInto(out *HVSSyncConfig) { + *out = *in + if in.Dynamic != nil { + in, out := &in.Dynamic, &out.Dynamic + *out = new(HVSDynamicSyncConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HVSSyncConfig. +func (in *HVSSyncConfig) DeepCopy() *HVSSyncConfig { + if in == nil { + return nil + } + out := new(HVSSyncConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MergeStrategy) DeepCopyInto(out *MergeStrategy) { *out = *in diff --git a/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml b/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml index 801894f22..a244a3454 100644 --- a/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml +++ b/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml @@ -244,6 +244,22 @@ spec: - name type: object type: array + syncConfig: + description: SyncConfig configures sync behavior from HVS to VSO + properties: + dynamic: + description: Dynamic configures sync behavior for dynamic secrets. + properties: + renewalPercent: + default: 67 + description: |- + RenewalPercent is the percent out of 100 of a dynamic secret's TTL when + new secrets are generated. Defaults to 67 percent plus jitter. + maximum: 90 + minimum: 0 + type: integer + type: object + type: object required: - appName - destination @@ -251,6 +267,25 @@ spec: status: description: HCPVaultSecretsAppStatus defines the observed state of HCPVaultSecretsApp properties: + dynamicSecrets: + description: |- + DynamicSecrets lists the last observed state of any dynamic secrets + within the HCP Vault Secrets App + items: + description: |- + HVSDynamicStatus defines the observed state of a dynamic secret within an HCP + Vault Secrets App + properties: + createdAt: + type: string + expiresAt: + type: string + name: + type: string + ttl: + type: string + type: object + type: array lastGeneration: description: LastGeneration is the Generation of the last reconciled resource. diff --git a/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml b/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml index 801894f22..a244a3454 100644 --- a/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml +++ b/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml @@ -244,6 +244,22 @@ spec: - name type: object type: array + syncConfig: + description: SyncConfig configures sync behavior from HVS to VSO + properties: + dynamic: + description: Dynamic configures sync behavior for dynamic secrets. + properties: + renewalPercent: + default: 67 + description: |- + RenewalPercent is the percent out of 100 of a dynamic secret's TTL when + new secrets are generated. Defaults to 67 percent plus jitter. + maximum: 90 + minimum: 0 + type: integer + type: object + type: object required: - appName - destination @@ -251,6 +267,25 @@ spec: status: description: HCPVaultSecretsAppStatus defines the observed state of HCPVaultSecretsApp properties: + dynamicSecrets: + description: |- + DynamicSecrets lists the last observed state of any dynamic secrets + within the HCP Vault Secrets App + items: + description: |- + HVSDynamicStatus defines the observed state of a dynamic secret within an HCP + Vault Secrets App + properties: + createdAt: + type: string + expiresAt: + type: string + name: + type: string + ttl: + type: string + type: object + type: array lastGeneration: description: LastGeneration is the Generation of the last reconciled resource. diff --git a/controllers/hcpvaultsecretsapp_controller.go b/controllers/hcpvaultsecretsapp_controller.go index b72b5a6a6..95c39fb2e 100644 --- a/controllers/hcpvaultsecretsapp_controller.go +++ b/controllers/hcpvaultsecretsapp_controller.go @@ -12,6 +12,7 @@ import ( httptransport "github.com/go-openapi/runtime/client" hvsclient "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" hcpconfig "github.com/hashicorp/hcp-sdk-go/config" hcpclient "github.com/hashicorp/hcp-sdk-go/httpclient" "github.com/hashicorp/hcp-sdk-go/profile" @@ -40,6 +41,7 @@ const ( headerUserAgent = "User-Agent" hcpVaultSecretsAppFinalizer = "hcpvaultsecretsapp.secrets.hashicorp.com/finalizer" + defaultDyanmicRenewPercent = 67 ) var userAgent = fmt.Sprintf("vso/%s", version.Version().String()) @@ -130,25 +132,43 @@ func (r *HCPVaultSecretsAppReconciler) Reconcile(ctx context.Context, req ctrl.R resp, err := c.OpenAppSecrets(params, nil) if err != nil { - logger.Error(err, "Get App Secret", "appName", o.Spec.AppName) + logger.Error(err, "Get App Secrets", "appName", o.Spec.AppName) entry, _ := r.BackOffRegistry.Get(req.NamespacedName) return ctrl.Result{ RequeueAfter: entry.NextBackOff(), }, nil - } else { - r.BackOffRegistry.Delete(req.NamespacedName) } + dynamicSecrets, err := getHVSDynamicSecrets(ctx, c, o.Spec.AppName) + if err != nil { + logger.Error(err, "Get Dynamic Secrets", "appName", o.Spec.AppName) + entry, _ := r.BackOffRegistry.Get(req.NamespacedName) + return ctrl.Result{ + RequeueAfter: entry.NextBackOff(), + }, nil + } + // Add the dynamic secrets to the OpenAppSecrets response to be processed + // along with the rest of the App secrets + resp.Payload.Secrets = append(resp.Payload.Secrets, dynamicSecrets...) + + // Calculate next requeue time based on whichever comes first between the + // current `requeueAfter` and all the dynamic secret renewal times. Also set + // the dynamic secret statuses while looping through the dynamic secrets. + renewPercent := makeDyanmicRenewPercent(o.Spec.SyncConfig) + o.Status.DynamicSecrets = nil + for _, secret := range dynamicSecrets { + requeueAfter = getNextRequeue(requeueAfter, secret.DynamicInstance, renewPercent) + o.Status.DynamicSecrets = append(o.Status.DynamicSecrets, makeHVSDynamicStatus(secret)) + } + + // Remove this app from the backoff registry now that we're done with HVS + // API calls + r.BackOffRegistry.Delete(req.NamespacedName) + r.referenceCache.Set(SecretTransformation, req.NamespacedName, helpers.GetTransformationRefObjKeys( o.Spec.Destination.Transformation, o.Namespace)...) - if err != nil { - r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonTransformationError, - "Failed setting up SecretTransformationOption: %s", err) - return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil - } - data, err := r.SecretDataBuilder.WithHVSAppSecrets(resp, transOption) if err != nil { r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretDataBuilderError, @@ -318,3 +338,82 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { func injectRequestInformation(runtime *httptransport.Runtime) { runtime.Transport = &transport{child: runtime.Transport} } + +// getHVSDynamicSecrets +func getHVSDynamicSecrets(ctx context.Context, c hvsclient.ClientService, appName string) ([]*models.Secrets20231128OpenSecret, error) { + // Fetch the unopened AppSecrets to get the full list of secrets (including + // dynamic) + secretsListParams := &hvsclient.ListAppSecretsParams{ + Context: ctx, + AppName: appName, + // Type is currently non-functional, so we have to filter the list + // ourselves + // Type: ptr.To(helpers.HVSSecretTypeDynamic), + } + appSecretsList, err := c.ListAppSecrets(secretsListParams, nil) + if err != nil { + return nil, fmt.Errorf("failed to list app Secrets for app %s: %w", appName, err) + } + + dynamicSecrets := make([]*models.Secrets20231128OpenSecret, 0) + + if appSecretsList.Payload != nil { + // TODO(tvoran): only fetch/create dynamic secrets that are new or are + // due for rotation + for _, appSecret := range appSecretsList.Payload.Secrets { + if appSecret.Type != helpers.HVSSecretTypeDynamic { + continue + } + secretParams := &hvsclient.OpenAppSecretParams{ + Context: ctx, + AppName: appName, + SecretName: appSecret.Name, + } + dynamicResp, err := c.OpenAppSecret(secretParams, nil) + if err != nil { + return nil, fmt.Errorf("failed to get dynamic secret %s in app %s: %w", + appSecret.Name, appName, err) + } + if dynamicResp != nil && dynamicResp.Payload != nil { + dynamicSecrets = append(dynamicSecrets, dynamicResp.Payload.Secret) + } + } + } + + return dynamicSecrets, nil +} + +func makeDyanmicRenewPercent(syncConfig *secretsv1beta1.HVSSyncConfig) int { + renewPercent := defaultDyanmicRenewPercent + if syncConfig != nil && syncConfig.Dynamic != nil && syncConfig.Dynamic.RenewalPercent != 0 { + renewPercent = syncConfig.Dynamic.RenewalPercent + } + return renewPercent +} + +func makeHVSDynamicStatus(secret *models.Secrets20231128OpenSecret) secretsv1beta1.HVSDynamicStatus { + status := secretsv1beta1.HVSDynamicStatus{ + Name: secret.Name, + } + if secret.DynamicInstance != nil { + status.CreatedAt = secret.DynamicInstance.CreatedAt.String() + status.ExpiresAt = secret.DynamicInstance.ExpiresAt.String() + status.TTL = secret.DynamicInstance.TTL + } + return status +} + +// getNextRequeue compares returns whichever is less between the current +// `requeueAfter` and the `ExpiresAt` of the dynamic secret instance +func getNextRequeue(requeueAfter time.Duration, dynamicInstance *models.Secrets20231128OpenSecretDynamicInstance, renewPercent int) time.Duration { + if dynamicInstance == nil { + return requeueAfter + } + nextRequeue := requeueAfter + next := time.Until(time.Time(dynamicInstance.ExpiresAt)) + renewalTime := computeDynamicHorizonWithJitter(next, renewPercent) + if renewalTime < requeueAfter { + nextRequeue = renewalTime + } + return nextRequeue +} diff --git a/controllers/hcpvaultsecretsapp_controller_test.go b/controllers/hcpvaultsecretsapp_controller_test.go new file mode 100644 index 000000000..920379e28 --- /dev/null +++ b/controllers/hcpvaultsecretsapp_controller_test.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package controllers + +import ( + "context" + "fmt" + "testing" + + "github.com/go-openapi/runtime" + hvsclient "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type fakeHVSTransport struct { + secrets []*models.Secrets20231128OpenSecret +} + +func (f *fakeHVSTransport) Submit(operation *runtime.ClientOperation) (interface{}, error) { + if operation.ID == "ListAppSecrets" { + respSecrets := []*models.Secrets20231128Secret{} + for _, secret := range f.secrets { + mb, err := secret.MarshalBinary() + if err != nil { + return nil, err + } + closedSecret := &models.Secrets20231128Secret{} + err = closedSecret.UnmarshalBinary(mb) + if err != nil { + return nil, err + } + fmt.Printf("closed secret is %+v", closedSecret) + respSecrets = append(respSecrets, closedSecret) + } + return &hvsclient.ListAppSecretsOK{ + Payload: &models.Secrets20231128ListAppSecretsResponse{ + Secrets: respSecrets, + }, + }, nil + } + + if operation.ID == "OpenAppSecret" { + params := operation.Params.(*hvsclient.OpenAppSecretParams) + for _, secret := range f.secrets { + if secret.Name == params.SecretName { + return &hvsclient.OpenAppSecretOK{ + Payload: &models.Secrets20231128OpenAppSecretResponse{ + Secret: secret, + }, + }, nil + } + } + return nil, fmt.Errorf("secret %q not found", params.SecretName) + } + + return nil, fmt.Errorf("unsupported operation ID: %s", operation.ID) +} + +func NewFakeHVSTransport(secrets []*models.Secrets20231128OpenSecret) *fakeHVSTransport { + return &fakeHVSTransport{secrets: secrets} +} + +func Test_getHVSDynamicSecrets(t *testing.T) { + t.Parallel() + + exampleStatic := &models.Secrets20231128OpenSecret{ + Name: "static", + StaticVersion: &models.Secrets20231128OpenSecretStaticVersion{ + Value: "value", + }, + Type: "static", + } + + exampleDynamic1 := &models.Secrets20231128OpenSecret{ + Name: "dynamic1", + DynamicInstance: &models.Secrets20231128OpenSecretDynamicInstance{ + Values: map[string]string{ + "secret_key": "key1", + "secret_id": "id1", + }, + }, + Type: "dynamic", + } + + exampleDynamic2 := &models.Secrets20231128OpenSecret{ + Name: "dynamic2", + DynamicInstance: &models.Secrets20231128OpenSecretDynamicInstance{ + Values: map[string]string{ + "secret_key": "key2", + "secret_id": "id2", + }, + }, + Type: "dynamic", + } + + tests := map[string]struct { + hvsSecrets []*models.Secrets20231128OpenSecret + expected []*models.Secrets20231128OpenSecret + }{ + "mixed": { + hvsSecrets: []*models.Secrets20231128OpenSecret{ + exampleStatic, + exampleDynamic1, + exampleDynamic2, + }, + expected: []*models.Secrets20231128OpenSecret{ + exampleDynamic1, + exampleDynamic2, + }, + }, + "no dynamic": { + hvsSecrets: []*models.Secrets20231128OpenSecret{ + exampleStatic, + }, + expected: []*models.Secrets20231128OpenSecret{}, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + transport := NewFakeHVSTransport(tt.hvsSecrets) + client := hvsclient.New(transport, nil) + resp, err := getHVSDynamicSecrets(context.Background(), client, "appName") + require.NoError(t, err) + assert.Equal(t, tt.expected, resp) + }) + } +} diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md index dd0f3bcb7..ab7253f9b 100644 --- a/docs/api/api-reference.md +++ b/docs/api/api-reference.md @@ -187,10 +187,63 @@ _Appears in:_ | `refreshAfter` _string_ | RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h | 600s | Pattern: `^([0-9]+(\.[0-9]+)?(s|m|h))$`
Type: string
| | `rolloutRestartTargets` _[RolloutRestartTarget](#rolloutrestarttarget) array_ | RolloutRestartTargets should be configured whenever the application(s)
consuming the HCP Vault Secrets App does not support dynamically reloading a
rotated secret. In that case one, or more RolloutRestartTarget(s) can be
configured here. The Operator will trigger a "rollout-restart" for each target
whenever the Vault secret changes between reconciliation events. See
RolloutRestartTarget for more details. | | | | `destination` _[Destination](#destination)_ | Destination provides configuration necessary for syncing the HCP Vault
Application secrets to Kubernetes. | | | +| `syncConfig` _[HVSSyncConfig](#hvssyncconfig)_ | SyncConfig configures sync behavior from HVS to VSO | | | +#### HVSDynamicStatus + + + +HVSDynamicStatus defines the observed state of a dynamic secret within an HCP +Vault Secrets App + + + +_Appears in:_ +- [HCPVaultSecretsAppStatus](#hcpvaultsecretsappstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | | | | +| `createdAt` _string_ | | | | +| `expiresAt` _string_ | | | | +| `ttl` _string_ | | | | + + +#### HVSDynamicSyncConfig + + + +HVSDynamicSyncConfig configures sync behavior for HVS dynamic secrets. + + + +_Appears in:_ +- [HVSSyncConfig](#hvssyncconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `renewalPercent` _integer_ | RenewalPercent is the percent out of 100 of a dynamic secret's TTL when
new secrets are generated. Defaults to 67 percent plus jitter. | 67 | Maximum: 90
Minimum: 0
| + + +#### HVSSyncConfig + + + +HVSSyncConfig configures sync behavior from HVS to VSO + + + +_Appears in:_ +- [HCPVaultSecretsAppSpec](#hcpvaultsecretsappspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `dynamic` _[HVSDynamicSyncConfig](#hvsdynamicsyncconfig)_ | Dynamic configures sync behavior for dynamic secrets. | | | + + #### MergeStrategy diff --git a/internal/helpers/secrets.go b/internal/helpers/secrets.go index c60c4dc32..3945e0780 100644 --- a/internal/helpers/secrets.go +++ b/internal/helpers/secrets.go @@ -28,6 +28,7 @@ const ( SecretDataKeyRaw = "_raw" HVSSecretTypeKV = "kv" HVSSecretTypeRotating = "rotating" + HVSSecretTypeDynamic = "dynamic" ) var SecretDataErrorContainsRaw = fmt.Errorf("key '%s' not permitted in Secret data", SecretDataKeyRaw) @@ -498,7 +499,7 @@ func (s *SecretDataBuilder) WithHVSAppSecrets(resp *hvsclient.OpenAppSecretsOK, data := make(map[string][]byte) hasTemplates := len(opt.KeyedTemplates) > 0 for _, v := range p.Secrets { - if v.StaticVersion == nil && v.RotatingVersion == nil { + if v.StaticVersion == nil && v.RotatingVersion == nil && v.DynamicInstance == nil { continue } @@ -512,6 +513,12 @@ func (s *SecretDataBuilder) WithHVSAppSecrets(resp *hvsclient.OpenAppSecretsOK, rName := fmt.Sprintf("%s_%s", v.Name, rvk) secrets[rName] = rvv } + case HVSSecretTypeDynamic: + // Since dynamic secrets have multiple values, prefix each key with + // the secret name to avoid collisions. + for dynamicKey, dynamicValue := range v.DynamicInstance.Values { + secrets[v.Name+"_"+dynamicKey] = dynamicValue + } default: continue }