diff --git a/api/v1beta1/vaultdynamicsecret_types.go b/api/v1beta1/vaultdynamicsecret_types.go index 54f1b564..4a0a3eba 100644 --- a/api/v1beta1/vaultdynamicsecret_types.go +++ b/api/v1beta1/vaultdynamicsecret_types.go @@ -60,6 +60,11 @@ type VaultDynamicSecretSpec struct { RolloutRestartTargets []RolloutRestartTarget `json:"rolloutRestartTargets,omitempty"` // Destination provides configuration necessary for syncing the Vault secret to Kubernetes. Destination Destination `json:"destination"` + // Delay adds an artifical delay between fetching the secret and writing it to Kubernetes. + // This is to cater for services that are eventually consistent + // +kubebuilder:default=0 + // +kubebuilder:validation:Minimum=0 + Delay int `json:"delay,omitempty"` } // VaultDynamicSecretStatus defines the observed state of VaultDynamicSecret diff --git a/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml b/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml index 5dc5e8b1..812a0c5c 100644 --- a/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml +++ b/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml @@ -45,6 +45,13 @@ spec: roles", or "static credentials", with a request path that contains "static-creds". type: boolean + delay: + default: 0 + description: Delay adds an artifical delay between fetching the secret + and writing it to Kubernetes. This is to cater for services that + are eventually consistent + minimum: 0 + type: integer destination: description: Destination provides configuration necessary for syncing the Vault secret to Kubernetes. diff --git a/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml index 5dc5e8b1..812a0c5c 100644 --- a/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml +++ b/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml @@ -45,6 +45,13 @@ spec: roles", or "static credentials", with a request path that contains "static-creds". type: boolean + delay: + default: 0 + description: Delay adds an artifical delay between fetching the secret + and writing it to Kubernetes. This is to cater for services that + are eventually consistent + minimum: 0 + type: integer destination: description: Destination provides configuration necessary for syncing the Vault secret to Kubernetes. diff --git a/controllers/vaultdynamicsecret_controller.go b/controllers/vaultdynamicsecret_controller.go index 8998dbb0..1493c381 100644 --- a/controllers/vaultdynamicsecret_controller.go +++ b/controllers/vaultdynamicsecret_controller.go @@ -372,6 +372,10 @@ func (r *VaultDynamicSecretReconciler) syncSecret(ctx context.Context, c vault.C } } + if o.Spec.Delay > 0 { + sleepTime := time.Duration(o.Spec.Delay) * time.Second + time.Sleep(sleepTime) + } if err := helpers.SyncSecret(ctx, r.Client, o, data); err != nil { logger.Error(err, "Destination sync failed") return nil, false, err diff --git a/controllers/vaultdynamicsecret_controller_test.go b/controllers/vaultdynamicsecret_controller_test.go index a66469d5..806a1582 100644 --- a/controllers/vaultdynamicsecret_controller_test.go +++ b/controllers/vaultdynamicsecret_controller_test.go @@ -686,6 +686,105 @@ func Test_computeRotationTime(t *testing.T) { } } +func Test_artificialDelay(t *testing.T) { + type fields struct { + Client client.Client + runtimePodUID types.UID + } + type args struct { + ctx context.Context + vClient *vault.MockRecordingVaultClient + o *secretsv1beta1.VaultDynamicSecret + } + tests := []struct { + name string + fields fields + args args + want int + wantErr assert.ErrorAssertionFunc + }{ + { + name: "no-delay", + fields: fields{ + Client: fake.NewClientBuilder().Build(), + runtimePodUID: "", + }, + args: args{ + ctx: nil, + vClient: &vault.MockRecordingVaultClient{}, + o: &secretsv1beta1.VaultDynamicSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: secretsv1beta1.VaultDynamicSecretSpec{ + Mount: "baz", + Path: "foo", + Destination: secretsv1beta1.Destination{ + Name: "baz", + Create: true, + }, + Delay: 0, + }, + Status: secretsv1beta1.VaultDynamicSecretStatus{}, + }, + }, + wantErr: assert.NoError, + want: 0, + }, + { + name: "with-ten-second-delay", + fields: fields{ + Client: fake.NewClientBuilder().Build(), + runtimePodUID: "", + }, + args: args{ + ctx: nil, + vClient: &vault.MockRecordingVaultClient{}, + o: &secretsv1beta1.VaultDynamicSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: secretsv1beta1.VaultDynamicSecretSpec{ + Mount: "baz", + Path: "foo", + Destination: secretsv1beta1.Destination{ + Name: "baz", + Create: true, + }, + Delay: 10, + }, + + Status: secretsv1beta1.VaultDynamicSecretStatus{}, + }, + }, + want: 10, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &VaultDynamicSecretReconciler{ + Client: tt.fields.Client, + } + start := time.Now() + _, _, err := r.syncSecret(tt.args.ctx, tt.args.vClient, tt.args.o) + if !tt.wantErr(t, err, fmt.Sprintf("syncSecret(%v, %v, %v)", tt.args.ctx, tt.args.vClient, tt.args.o)) { + return + } + + duration := time.Since(start).Round(time.Second).Seconds() + + fmt.Println(duration) + if duration != float64(tt.want) { + fmt.Println(duration != float64(tt.want)) + t.Error("Sleep length not correct") + } + }) + } +} + func Test_computeRelativeHorizonWithJitter(t *testing.T) { staticNow := time.Unix(nowFunc().Unix(), 0) defaultNowFunc := func() time.Time { return staticNow } diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md index 2c5c9302..2447f3ef 100644 --- a/docs/api/api-reference.md +++ b/docs/api/api-reference.md @@ -459,6 +459,7 @@ _Appears in:_ | `allowStaticCreds` _boolean_ | AllowStaticCreds should be set when syncing credentials that are periodically rotated by the Vault server, rather than created upon request. These secrets are sometimes referred to as "static roles", or "static credentials", with a request path that contains "static-creds". | | `rolloutRestartTargets` _[RolloutRestartTarget](#rolloutrestarttarget) array_ | RolloutRestartTargets should be configured whenever the application(s) consuming the Vault secret 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 Vault secret to Kubernetes. | +| `delay` _integer_ | Delay adds an artifical delay between fetching the secret and writing it to Kubernetes. This is to cater for services that are eventually consistent |