diff --git a/.gitignore b/.gitignore index 6d6b80124..8cb2d7880 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ bin/* *.png build .DS_Store + +.tmp* diff --git a/Makefile b/Makefile index 4b74bb974..b82253fd0 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ BUNDLE_DIR ?= $(OPERATOR_BUILD_DIR)/bundle CHART_ROOT ?= chart CHART_CRDS_DIR ?= $(CHART_ROOT)/crds +VAULT_DOCS_VERSION ?= v1.20.x VAULT_IMAGE_TAG ?= latest VAULT_IMAGE_REPO ?= K8S_VAULT_NAMESPACE ?= vault @@ -52,6 +53,7 @@ SKIP_CLEANUP ?= SKIP_AWS_TESTS ?= true SKIP_AWS_STATIC_CREDS_TEST ?= true SKIP_GCP_TESTS ?= true +SKIP_HCPVSAPPS_TESTS ?= false # filter bats unit tests to run. BATS_TESTS_FILTER ?= .\* @@ -661,7 +663,7 @@ clean: # Usage: make gen-helm-docs # If no options are given, helm.mdx from a local copy of the vault repository will be used. # Adapted from https://github.com/hashicorp/consul-k8s/tree/main/hack/helm-reference-gen -VAULT_DOCS_PATH ?= ../vault/website/content/docs/platform/k8s/vso/helm.mdx +VAULT_DOCS_PATH ?= ../web-unified-docs/content/vault/$(VAULT_DOCS_VERSION)/content/docs/deploy/kubernetes/vso/helm.mdx gen-helm-docs: @cd hack/helm-reference-gen; go run ./... --vault=$(VAULT_DOCS_PATH) diff --git a/api/v1beta1/csisecrets_types.go b/api/v1beta1/csisecrets_types.go new file mode 100644 index 000000000..fc1c5d794 --- /dev/null +++ b/api/v1beta1/csisecrets_types.go @@ -0,0 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CSISecretsSpec defines the desired state of CSISecrets. It contains the +// configuration for the CSI driver to populate the secret data. +type CSISecretsSpec struct { + // Namespace is the Vault namespace where the secret is located. + Namespace string `json:"namespace,omitempty"` + // AccessControl provides configuration for controlling access to the secret. + AccessControl AccessControl `json:"accessControl"` + // Secrets that will be synced with the CSI driver. + Secrets SecretCollection `json:"secrets"` + // SyncConfig provides configuration for syncing the secret data with the CSI driver. + SyncConfig CSISyncConfig `json:"syncConfig,omitempty"` + // VaultAuthRef is the reference to the VaultAuth resource. + VaultAuthRef *VaultAuthRef `json:"vaultAuthRef,omitempty"` +} + +type SecretCollection struct { + // Transformation provides configuration for transforming the secret data before + // it is stored in the CSI volume. + Transformation *Transformation `json:"transformation,omitempty"` + // VaultAppRoleSecretIDs is a list of AppRole secret IDs to be used to populate the secret. + VaultAppRoleSecretIDs []VaultAppRoleSecretID `json:"vaultAppRoleSecretIDs,omitempty"` + // VaultStaticSecrets is a list of static secrets to be synced by the CSI driver. + VaultStaticSecrets []VaultStaticSecretCollectable `json:"vaultStaticSecrets,omitempty"` +} + +// VaultAppRoleSecretID defines the AppRole secret ID to be used to populate the secret. +type VaultAppRoleSecretID struct { + // Mount path to the AppRole auth engine. + Mount string `json:"mount"` + // Role is the name of the AppRole. + Role string `json:"role"` + // Metadata is the metadata to be associated with the secret ID. It is set on + // the token generated by the secret ID. + Metadata map[string]string `json:"metadata,omitempty"` + // CIDRList is the list of CIDR blocks that access the secret ID. + CIDRList []string `json:"cidrList,omitempty"` + // TokenBoundCIDRs is the list of CIDR blocks that can be used to authenticate + // using tokens generated by this secret ID. + TokenBoundCIDRs []string `json:"tokenBoundCIDRs,omitempty"` + // TTL is the TTL for the secret ID, after which it becomes invalid. + // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(s|m|h))$` + TTL string `json:"ttl,omitempty"` + // NumUses is the number of times the secret ID can be used. + NumUses int `json:"numUses,omitempty"` + // WrapTTL is the TTL for the wrapped secret ID. + // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(s|m|h))$` + WrapTTL string `json:"wrapTTL,omitempty"` + // SyncRoleID is the flag to fetch the role ID from the AppRole auth engine. + // Requires that the provisioning VaultAuth has the necessary permissions to fetch the role ID. + SyncRoleID *bool `json:"syncRoleID,omitempty"` + // Transformation provides configuration for transforming the secret data before + // it is stored in the CSI volume. + Transformation *Transformation `json:"transformation,omitempty"` +} + +type CSISyncConfig struct { + // ContainerState is the state of the container that the CSI driver always sync + // on. This configuration is useful to sync when the last state of the container + // is in the terminated state and the restart count is greater than 0. + ContainerState *ContainerState `json:"containerState"` +} + +type ContainerState struct { + // NamePattern of the container. Can be expressed as a regular expression. + NamePattern string `json:"namePattern,omitempty"` + // ImagePattern of the container. Can be expressed as a regular expression. + ImagePattern string `json:"imagePattern,omitempty"` +} + +// AccessControl provides configuration for controlling access to the secret. +// It allows specifying the namespaces, service account, pod names, and pod +// labels that should be allowed to access the secret. +type AccessControl struct { + // ServiceAccountPattern is the name of the service account that should be used to + // access the secret. It can be specified as a regex pattern. + // A valid service account is always required. + ServiceAccountPattern string `json:"serviceAccountPattern"` + // NamespacePatterns is a list of namespace name regex patterns that are allowed access. + NamespacePatterns []string `json:"namespacePatterns,omitempty"` + // PodNamePatterns is a list of pod name regex patterns that should be allowed access. + PodNamePatterns []string `json:"podNamePatterns,omitempty"` + // PodLabels is a map of pod label key-value pairs that should be allowed access. + PodLabels map[string]string `json:"podLabels,omitempty"` + // MatchPolicy is the policy to use when matching the access control rules. If + // set to "any", only one of the rules should match. If set to "all", all the + // rules should match. + // + // +kubebuilder:validation:Enum=any;all + // +kubebuilder:default=all + MatchPolicy string `json:"matchPolicy,omitempty"` +} + +// CSISecretsStatus defines the observed state of CSISecrets +type CSISecretsStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// CSISecrets is the Schema for the csisecrets API +type CSISecrets struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CSISecretsSpec `json:"spec,omitempty"` + Status CSISecretsStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// CSISecretsList contains a list of CSISecrets +type CSISecretsList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CSISecrets `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CSISecrets{}, &CSISecretsList{}) +} diff --git a/api/v1beta1/secrettransformation_types.go b/api/v1beta1/secrettransformation_types.go index 01a68f28e..cfc3948b8 100644 --- a/api/v1beta1/secrettransformation_types.go +++ b/api/v1beta1/secrettransformation_types.go @@ -28,18 +28,18 @@ type SecretTransformation struct { // SecretTransformationSpec defines the desired state of SecretTransformation type SecretTransformationSpec struct { // Templates maps a template name to its Template. Templates are always included - // in the rendered K8s Secret with the specified key. + // in the rendered secret with the specified key. Templates map[string]Template `json:"templates,omitempty"` - // SourceTemplates are never included in the rendered K8s Secret, they can be + // SourceTemplates are never included in the rendered secret, they can be // used to provide common template definitions, etc. SourceTemplates []SourceTemplate `json:"sourceTemplates,omitempty"` // Includes contains regex patterns used to filter top-level source secret data - // fields for inclusion in the final K8s Secret data. These pattern filters are + // fields for inclusion in the final secret data. These pattern filters are // never applied to templated fields as defined in Templates. They are always // applied last. Includes []string `json:"includes,omitempty"` // Excludes contains regex patterns used to filter top-level source secret data - // fields for exclusion from the final K8s Secret data. These pattern filters are + // fields for exclusion from the final secret data. These pattern filters are // never applied to templated fields as defined in Templates. They are always // applied before any inclusion patterns. To exclude all source secret data // fields, you can configure the single pattern ".*". diff --git a/api/v1beta1/vaultauth_types.go b/api/v1beta1/vaultauth_types.go index bfdeba36c..9b9f2b50d 100644 --- a/api/v1beta1/vaultauth_types.go +++ b/api/v1beta1/vaultauth_types.go @@ -323,7 +323,7 @@ type VaultAuthGlobalRef struct { Name string `json:"name,omitempty"` // Namespace of the VaultAuthGlobal resource. If not provided, the namespace of // the referring VaultAuth resource is used. - // +kubebuilder:validation:Pattern=`^([a-z0-9.-]{1,253})$` + // +kubebuilder:validation:Pattern=`^([a-z0-9-]{1,63})$` Namespace string `json:"namespace,omitempty"` // MergeStrategy configures the merge strategy for HTTP headers and parameters // that are included in all Vault authentication requests. @@ -456,6 +456,18 @@ type StorageEncryption struct { KeyName string `json:"keyName"` } +type VaultAuthRef struct { + // Name of the VaultAuth resource. + Name string `json:"name"` + // Namespace of the VaultAuth resource. + Namespace string `json:"namespace,omitempty"` + // TrustNamespace of the referring VaultAuth resource. This means that any Vault + // credentials will be provided by resources in the same namespace as the + // VaultAuth resource. Otherwise, the credentials will be provided by the secret + // resource's namespace. + TrustNamespace bool `json:"trustNamespace,omitempty"` +} + // +kubebuilder:object:root=true // VaultAuthList contains a list of VaultAuth diff --git a/api/v1beta1/vaultstaticsecret_types.go b/api/v1beta1/vaultstaticsecret_types.go index e1b95ed18..70831959a 100644 --- a/api/v1beta1/vaultstaticsecret_types.go +++ b/api/v1beta1/vaultstaticsecret_types.go @@ -20,19 +20,6 @@ type VaultStaticSecretSpec struct { // Namespace of the secrets engine mount in Vault. If not set, the namespace that's // part of VaultAuth resource will be inferred. Namespace string `json:"namespace,omitempty"` - // Mount for the secret in Vault - Mount string `json:"mount"` - // Path of the secret in Vault, corresponds to the `path` parameter for, - // kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret - // kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version - Path string `json:"path"` - // Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter: - // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version - // +kubebuilder:validation:Minimum=0 - Version int `json:"version,omitempty"` - // Type of the Vault static secret - // +kubebuilder:validation:Enum={kv-v1,kv-v2} - Type string `json:"type"` // RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h // +kubebuilder:validation:Type=string // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(s|m|h))$` @@ -55,6 +42,31 @@ type VaultStaticSecretSpec struct { Destination Destination `json:"destination"` // SyncConfig configures sync behavior from Vault to VSO SyncConfig *SyncConfig `json:"syncConfig,omitempty"` + + VaultStaticSecretCommon `json:",inline"` +} + +type VaultStaticSecretCommon struct { + // Mount for the secret in Vault + Mount string `json:"mount"` + // Path of the secret in Vault, corresponds to the `path` parameter for: + // kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret + // kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version + Path string `json:"path"` + // Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter: + // https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version + // +kubebuilder:validation:Minimum=0 + Version int `json:"version,omitempty"` + // Type of the Vault static secret + // +kubebuilder:validation:Enum={kv-v1,kv-v2} + Type string `json:"type"` +} + +type VaultStaticSecretCollectable struct { + VaultStaticSecretCommon `json:",inline"` + // Transformation provides configuration for transforming the secret data before + // it is stored in the CSI volume. + Transformation *Transformation `json:"transformation,omitempty"` } // SyncConfig configures sync behavior from Vault to VSO diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index dd288c0ed..e9eaa77a4 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -12,6 +12,170 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessControl) DeepCopyInto(out *AccessControl) { + *out = *in + if in.NamespacePatterns != nil { + in, out := &in.NamespacePatterns, &out.NamespacePatterns + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PodNamePatterns != nil { + in, out := &in.PodNamePatterns, &out.PodNamePatterns + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PodLabels != nil { + in, out := &in.PodLabels, &out.PodLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessControl. +func (in *AccessControl) DeepCopy() *AccessControl { + if in == nil { + return nil + } + out := new(AccessControl) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSISecrets) DeepCopyInto(out *CSISecrets) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecrets. +func (in *CSISecrets) DeepCopy() *CSISecrets { + if in == nil { + return nil + } + out := new(CSISecrets) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CSISecrets) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSISecretsList) DeepCopyInto(out *CSISecretsList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CSISecrets, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecretsList. +func (in *CSISecretsList) DeepCopy() *CSISecretsList { + if in == nil { + return nil + } + out := new(CSISecretsList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CSISecretsList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSISecretsSpec) DeepCopyInto(out *CSISecretsSpec) { + *out = *in + in.AccessControl.DeepCopyInto(&out.AccessControl) + in.Secrets.DeepCopyInto(&out.Secrets) + in.SyncConfig.DeepCopyInto(&out.SyncConfig) + if in.VaultAuthRef != nil { + in, out := &in.VaultAuthRef, &out.VaultAuthRef + *out = new(VaultAuthRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecretsSpec. +func (in *CSISecretsSpec) DeepCopy() *CSISecretsSpec { + if in == nil { + return nil + } + out := new(CSISecretsSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSISecretsStatus) DeepCopyInto(out *CSISecretsStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecretsStatus. +func (in *CSISecretsStatus) DeepCopy() *CSISecretsStatus { + if in == nil { + return nil + } + out := new(CSISecretsStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSISyncConfig) DeepCopyInto(out *CSISyncConfig) { + *out = *in + if in.ContainerState != nil { + in, out := &in.ContainerState, &out.ContainerState + *out = new(ContainerState) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISyncConfig. +func (in *CSISyncConfig) DeepCopy() *CSISyncConfig { + if in == nil { + return nil + } + out := new(CSISyncConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerState) DeepCopyInto(out *ContainerState) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerState. +func (in *ContainerState) DeepCopy() *ContainerState { + if in == nil { + return nil + } + out := new(ContainerState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Destination) DeepCopyInto(out *Destination) { *out = *in @@ -346,6 +510,40 @@ func (in *RolloutRestartTarget) DeepCopy() *RolloutRestartTarget { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretCollection) DeepCopyInto(out *SecretCollection) { + *out = *in + if in.Transformation != nil { + in, out := &in.Transformation, &out.Transformation + *out = new(Transformation) + (*in).DeepCopyInto(*out) + } + if in.VaultAppRoleSecretIDs != nil { + in, out := &in.VaultAppRoleSecretIDs, &out.VaultAppRoleSecretIDs + *out = make([]VaultAppRoleSecretID, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VaultStaticSecrets != nil { + in, out := &in.VaultStaticSecrets, &out.VaultStaticSecrets + *out = make([]VaultStaticSecretCollectable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretCollection. +func (in *SecretCollection) DeepCopy() *SecretCollection { + if in == nil { + return nil + } + out := new(SecretCollection) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretTransformation) DeepCopyInto(out *SecretTransformation) { *out = *in @@ -596,6 +794,48 @@ func (in *TransformationRef) DeepCopy() *TransformationRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAppRoleSecretID) DeepCopyInto(out *VaultAppRoleSecretID) { + *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.CIDRList != nil { + in, out := &in.CIDRList, &out.CIDRList + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TokenBoundCIDRs != nil { + in, out := &in.TokenBoundCIDRs, &out.TokenBoundCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SyncRoleID != nil { + in, out := &in.SyncRoleID, &out.SyncRoleID + *out = new(bool) + **out = **in + } + if in.Transformation != nil { + in, out := &in.Transformation, &out.Transformation + *out = new(Transformation) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAppRoleSecretID. +func (in *VaultAppRoleSecretID) DeepCopy() *VaultAppRoleSecretID { + if in == nil { + return nil + } + out := new(VaultAppRoleSecretID) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultAuth) DeepCopyInto(out *VaultAuth) { *out = *in @@ -1048,6 +1288,21 @@ func (in *VaultAuthList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultAuthRef) DeepCopyInto(out *VaultAuthRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuthRef. +func (in *VaultAuthRef) DeepCopy() *VaultAuthRef { + if in == nil { + return nil + } + out := new(VaultAuthRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultAuthSpec) DeepCopyInto(out *VaultAuthSpec) { *out = *in @@ -1547,6 +1802,42 @@ func (in *VaultStaticSecret) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultStaticSecretCollectable) DeepCopyInto(out *VaultStaticSecretCollectable) { + *out = *in + out.VaultStaticSecretCommon = in.VaultStaticSecretCommon + if in.Transformation != nil { + in, out := &in.Transformation, &out.Transformation + *out = new(Transformation) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultStaticSecretCollectable. +func (in *VaultStaticSecretCollectable) DeepCopy() *VaultStaticSecretCollectable { + if in == nil { + return nil + } + out := new(VaultStaticSecretCollectable) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VaultStaticSecretCommon) DeepCopyInto(out *VaultStaticSecretCommon) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultStaticSecretCommon. +func (in *VaultStaticSecretCommon) DeepCopy() *VaultStaticSecretCommon { + if in == nil { + return nil + } + out := new(VaultStaticSecretCommon) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultStaticSecretList) DeepCopyInto(out *VaultStaticSecretList) { *out = *in @@ -1598,6 +1889,7 @@ func (in *VaultStaticSecretSpec) DeepCopyInto(out *VaultStaticSecretSpec) { *out = new(SyncConfig) **out = **in } + out.VaultStaticSecretCommon = in.VaultStaticSecretCommon } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultStaticSecretSpec. diff --git a/chart/crds/secrets.hashicorp.com_csisecrets.yaml b/chart/crds/secrets.hashicorp.com_csisecrets.yaml new file mode 100644 index 000000000..69b0676f8 --- /dev/null +++ b/chart/crds/secrets.hashicorp.com_csisecrets.yaml @@ -0,0 +1,561 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: csisecrets.secrets.hashicorp.com +spec: + group: secrets.hashicorp.com + names: + kind: CSISecrets + listKind: CSISecretsList + plural: csisecrets + singular: csisecrets + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: CSISecrets is the Schema for the csisecrets API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + CSISecretsSpec defines the desired state of CSISecrets. It contains the + configuration for the CSI driver to populate the secret data. + properties: + accessControl: + description: AccessControl provides configuration for controlling + access to the secret. + properties: + matchPolicy: + default: all + description: |- + MatchPolicy is the policy to use when matching the access control rules. If + set to "any", only one of the rules should match. If set to "all", all the + rules should match. + enum: + - any + - all + type: string + namespacePatterns: + description: NamespacePatterns is a list of namespace name regex + patterns that are allowed access. + items: + type: string + type: array + podLabels: + additionalProperties: + type: string + description: PodLabels is a map of pod label key-value pairs that + should be allowed access. + type: object + podNamePatterns: + description: PodNamePatterns is a list of pod name regex patterns + that should be allowed access. + items: + type: string + type: array + serviceAccountPattern: + description: |- + ServiceAccountPattern is the name of the service account that should be used to + access the secret. It can be specified as a regex pattern. + A valid service account is always required. + type: string + required: + - serviceAccountPattern + type: object + namespace: + description: Namespace is the Vault namespace where the secret is + located. + type: string + secrets: + description: Secrets that will be synced with the CSI driver. + properties: + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation resource. + type: string + namespace: + description: Namespace of the SecretTransformation resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + vaultAppRoleSecretIDs: + description: VaultAppRoleSecretIDs is a list of AppRole secret + IDs to be used to populate the secret. + items: + description: VaultAppRoleSecretID defines the AppRole secret + ID to be used to populate the secret. + properties: + cidrList: + description: CIDRList is the list of CIDR blocks that access + the secret ID. + items: + type: string + type: array + metadata: + additionalProperties: + type: string + description: |- + Metadata is the metadata to be associated with the secret ID. It is set on + the token generated by the secret ID. + type: object + mount: + description: Mount path to the AppRole auth engine. + type: string + numUses: + description: NumUses is the number of times the secret ID + can be used. + type: integer + role: + description: Role is the name of the AppRole. + type: string + syncRoleID: + description: |- + SyncRoleID is the flag to fetch the role ID from the AppRole auth engine. + Requires that the provisioning VaultAuth has the necessary permissions to fetch the role ID. + type: boolean + tokenBoundCIDRs: + description: |- + TokenBoundCIDRs is the list of CIDR blocks that can be used to authenticate + using tokens generated by this secret ID. + items: + type: string + type: array + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation + resource. + type: string + namespace: + description: Namespace of the SecretTransformation + resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + ttl: + description: TTL is the TTL for the secret ID, after which + it becomes invalid. + pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$ + type: string + wrapTTL: + description: WrapTTL is the TTL for the wrapped secret ID. + pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$ + type: string + required: + - mount + - role + type: object + type: array + vaultStaticSecrets: + description: VaultStaticSecrets is a list of static secrets to + be synced by the CSI driver. + items: + properties: + mount: + description: Mount for the secret in Vault + type: string + path: + description: |- + Path of the secret in Vault, corresponds to the `path` parameter for: + kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret + kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version + type: string + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation + resource. + type: string + namespace: + description: Namespace of the SecretTransformation + resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + type: + description: Type of the Vault static secret + enum: + - kv-v1 + - kv-v2 + type: string + version: + description: |- + Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter: + https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version + minimum: 0 + type: integer + required: + - mount + - path + - type + type: object + type: array + type: object + syncConfig: + description: SyncConfig provides configuration for syncing the secret + data with the CSI driver. + properties: + containerState: + description: |- + ContainerState is the state of the container that the CSI driver always sync + on. This configuration is useful to sync when the last state of the container + is in the terminated state and the restart count is greater than 0. + properties: + imagePattern: + description: ImagePattern of the container. Can be expressed + as a regular expression. + type: string + namePattern: + description: NamePattern of the container. Can be expressed + as a regular expression. + type: string + type: object + required: + - containerState + type: object + vaultAuthRef: + description: VaultAuthRef is the reference to the VaultAuth resource. + properties: + name: + description: Name of the VaultAuth resource. + type: string + namespace: + description: Namespace of the VaultAuth resource. + type: string + trustNamespace: + description: |- + TrustNamespace of the referring VaultAuth resource. This means that any Vault + credentials will be provided by resources in the same namespace as the + VaultAuth resource. Otherwise, the credentials will be provided by the secret + resource's namespace. + type: boolean + required: + - name + type: object + required: + - accessControl + - secrets + type: object + status: + description: CSISecretsStatus defines the observed state of CSISecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/chart/crds/secrets.hashicorp.com_secrettransformations.yaml b/chart/crds/secrets.hashicorp.com_secrettransformations.yaml index 021ed6b40..96ad84937 100644 --- a/chart/crds/secrets.hashicorp.com_secrettransformations.yaml +++ b/chart/crds/secrets.hashicorp.com_secrettransformations.yaml @@ -46,7 +46,7 @@ spec: excludes: description: |- Excludes contains regex patterns used to filter top-level source secret data - fields for exclusion from the final K8s Secret data. These pattern filters are + fields for exclusion from the final secret data. These pattern filters are never applied to templated fields as defined in Templates. They are always applied before any inclusion patterns. To exclude all source secret data fields, you can configure the single pattern ".*". @@ -56,7 +56,7 @@ spec: includes: description: |- Includes contains regex patterns used to filter top-level source secret data - fields for inclusion in the final K8s Secret data. These pattern filters are + fields for inclusion in the final secret data. These pattern filters are never applied to templated fields as defined in Templates. They are always applied last. items: @@ -64,7 +64,7 @@ spec: type: array sourceTemplates: description: |- - SourceTemplates are never included in the rendered K8s Secret, they can be + SourceTemplates are never included in the rendered secret, they can be used to provide common template definitions, etc. items: description: SourceTemplate provides source templating configuration. @@ -99,7 +99,7 @@ spec: type: object description: |- Templates maps a template name to its Template. Templates are always included - in the rendered K8s Secret with the specified key. + in the rendered secret with the specified key. type: object type: object status: diff --git a/chart/crds/secrets.hashicorp.com_vaultauths.yaml b/chart/crds/secrets.hashicorp.com_vaultauths.yaml index 4086afeee..b5146c510 100644 --- a/chart/crds/secrets.hashicorp.com_vaultauths.yaml +++ b/chart/crds/secrets.hashicorp.com_vaultauths.yaml @@ -313,7 +313,7 @@ spec: description: |- Namespace of the VaultAuthGlobal resource. If not provided, the namespace of the referring VaultAuth resource is used. - pattern: ^([a-z0-9.-]{1,253})$ + pattern: ^([a-z0-9-]{1,63})$ type: string type: object vaultConnectionRef: diff --git a/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml b/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml index f916e2222..3978f9eb5 100644 --- a/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml +++ b/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml @@ -207,7 +207,7 @@ spec: type: string path: description: |- - Path of the secret in Vault, corresponds to the `path` parameter for, + Path of the secret in Vault, corresponds to the `path` parameter for: kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version type: string diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl index a14040a9f..797e62873 100644 --- a/chart/templates/_helpers.tpl +++ b/chart/templates/_helpers.tpl @@ -150,7 +150,7 @@ imagePullSecrets generates pull secrets from either string or map values. A map value must be indexable by the key 'name'. */}} {{- define "vso.imagePullSecrets" -}} -{{ with .Values.controller.imagePullSecrets -}} +{{ with . -}} imagePullSecrets: {{- range . -}} {{- if typeIs "string" . }} @@ -162,7 +162,6 @@ imagePullSecrets: {{- end }} {{- end }} - {{/* globalTransformationOptions configures the manager's --global-transformation-options flag. */}} @@ -215,6 +214,32 @@ secret source error occurs. {{- end -}} {{- end -}} +{{/* +csiBackoffArgs provides the backoff options for the CSI driver when a +secret source error occurs. +*/}} +{{- define "vso.csiBackoffArgs" -}} +{{- $opts := list -}} +{{- with .Values.csi.driver.backoffOnSecretSourceError -}} +{{- with .initialInterval -}} +{{- $opts = mustAppend $opts (printf "--backoff-initial-interval=%s" .) -}} +{{- end -}} +{{- with .maxInterval -}} +{{- $opts = mustAppend $opts (printf "--backoff-max-interval=%s" .) -}} +{{- end -}} +{{- with .maxElapsedTime -}} +{{- $opts = mustAppend $opts (printf "--backoff-max-elapsed-time=%s" .) -}} +{{- end -}} +{{- with .multiplier -}} +{{- $opts = mustAppend $opts (printf "--backoff-multiplier=%.2f" (. | float64)) -}} +{{- end -}} +{{- with .randomizationFactor -}} +{{- $opts = mustAppend $opts (printf "--backoff-randomization-factor=%.2f" (. | float64)) -}} +{{- end -}} +{{- $opts | toYaml | nindent 8 -}} +{{- end -}} +{{- end -}} + {{/* aggregateRoleMatchLabelsViewer generates the matchLabels for the viewer cluster roles. */}} @@ -346,6 +371,102 @@ clientCache numLocks {{- end -}} {{- end -}} +{{/* +logging args +*/}} +{{- define "vso.csiControllerLoggingArgs" -}} +{{- $extraArgs := dict -}} +{{- with .Values.csi.driver.extraArgs -}} +{{- range . -}} +{{ $parts := splitList "=" . -}} +{{ $arg := (($parts | first) | trimPrefix "-") }} +{{- $_ := set $extraArgs ( $arg | trimPrefix "-") . -}} +{{- end -}} +{{- end -}} +{{- $ret := list -}} +{{- with .Values.csi.driver.logging -}} +{{- if $level := .level -}} +{{ $arg := "zap-log-level" -}} +{{- if not (hasKey $extraArgs $arg) -}} +{{- if eq $level "debug-extended" -}} +{{- $level = "5" -}} +{{- end -}} +{{- if eq .level "trace" -}} +{{- $level = "6" -}} +{{- end -}} +{{- $ret = append $ret (printf "--%s=%s" $arg $level) -}} +{{- end -}} +{{- end -}} +{{- if .timeEncoding -}} +{{ $arg := "zap-time-encoding" -}} +{{- if not (hasKey $extraArgs $arg) -}} +{{- $ret = append $ret (printf "--%s=%s" $arg .timeEncoding) -}} +{{- end -}} +{{- end -}} +{{- if .stacktraceLevel -}} +{{ $arg := "zap-stacktrace-level" -}} +{{- if not (hasKey $extraArgs $arg) -}} +{{- $ret = append $ret (printf "--%s=%s" $arg .stacktraceLevel) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $ret -}} +{{- $ret | toYaml | nindent 8 -}} +{{- end -}} +{{- end -}} + +{{/* +nodeSelector generates labels for the CSI Driver +*/}} +{{- define "vso.csi.nodeSelector" -}} +{{- $labels := dict -}} +{{- with .Values.csi.nodeSelector -}} +{{- range $k, $v := . -}} +{{- $_ := set $labels $k $v -}} +{{- end -}} +{{- end -}} +{{- $_ := set $labels "kubernetes.io/os" "linux" -}} +{{- $labels | toYaml -}} +{{- end -}} + +{{/* +annotations generates labels for the CSI Driver +*/}} +{{- define "vso.csi.annotations" -}} +{{- $labels := dict -}} +{{- with .Values.csi.annotations -}} +{{- range $k, $v := . -}} +{{- $_ := set $labels $k $v -}} +{{- end -}} +{{- end -}} +{{- $_ := set $labels "kubectl.kubernetes.io/default-container" "secrets-store" -}} +{{- $labels | toYaml -}} +{{- end -}} + +{{/* +toleration generates toleration settings for the CSI Driver. +*/}} +{{- define "vso.csi.tolerations" -}} +{{- $tolerations := list -}} +{{- if .Values.csi.tolerations }} + {{- range .Values.csi.tolerations }} + {{- $tolerations = append $tolerations . }} + {{- end }} +{{- end }} +{{- $existsToleration := false -}} +{{- range $t := $tolerations }} + {{- if and (hasKey $t "operator") (eq $t.operator "Exists") }} + {{- $existsToleration = true -}} + {{- end -}} +{{- end -}} +{{- if not $existsToleration }} + {{- $tolerations = append $tolerations (dict "operator" "Exists") }} +{{- end -}} +{{- if $tolerations -}} +{{- $tolerations | toYaml -}} +{{- end -}} +{{- end -}} + {{/* topologySpreadConstraints appends the "vso.chart.selectorLabels" to .Values.controller.topologySpreadConstraints if no labelSelector was specified */}} diff --git a/chart/templates/cluster-role-binding-csi-driver.yaml b/chart/templates/cluster-role-binding-csi-driver.yaml new file mode 100644 index 000000000..6c69ec582 --- /dev/null +++ b/chart/templates/cluster-role-binding-csi-driver.yaml @@ -0,0 +1,55 @@ +{{- /* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +*/ -}} + +{{- if .Values.csi.enabled -}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "vso.chart.fullname" . }}-manager-rolebinding-csi + labels: + app.kubernetes.io/component: csi-driver + {{- include "vso.chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "aggregate-role-viewer-csi-driver" }} +subjects: + - kind: ServiceAccount + name: {{ include "vso.chart.fullname" . }}-csi + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "vso.chart.fullname" . }}-tokenreview-csi + labels: + app.kubernetes.io/component: csi-driver + {{- include "vso.chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: {{ include "vso.chart.fullname" . }}-csi + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "vso.chart.fullname" . }}-viewer-csi + labels: + app.kubernetes.io/component: csi-driver + {{- include "vso.chart.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "csi-driver-role" }} +subjects: + - kind: ServiceAccount + name: {{ include "vso.chart.fullname" . }}-csi + namespace: {{ .Release.Namespace }} +{{- end -}} diff --git a/chart/templates/cluster-role-csi-driver.yaml b/chart/templates/cluster-role-csi-driver.yaml new file mode 100644 index 000000000..2353752f5 --- /dev/null +++ b/chart/templates/cluster-role-csi-driver.yaml @@ -0,0 +1,49 @@ +{{/* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +*/}} + +{{- if .Values.csi.enabled -}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "csi-driver-role" }} + labels: + app.kubernetes.io/component: rbac + # allow for selecting on the canonical name + {{- include "vso.chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - pods + - serviceaccounts + - configmaps + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods/status + verbs: + - get +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - get + - list + - watch +{{- end -}} diff --git a/chart/templates/clusterrole-aggregated-viewer-csi-driver.yaml b/chart/templates/clusterrole-aggregated-viewer-csi-driver.yaml new file mode 100644 index 000000000..eb49f807d --- /dev/null +++ b/chart/templates/clusterrole-aggregated-viewer-csi-driver.yaml @@ -0,0 +1,20 @@ +{{- /* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +*/ -}} + +{{- if .Values.csi.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "aggregate-role-viewer-csi-driver" }} + labels: + app.kubernetes.io/component: rbac + vso.hashicorp.com/role-instance: aggregate-role-viewer + rbac.authorization.k8s.io/aggregate-to-view: "true" +{{- include "vso.chart.labels" . | nindent 4 }} +aggregationRule: + clusterRoleSelectors: + - matchLabels: + vso.hashicorp.com/aggregate-to-viewer: "true" +{{- end -}} diff --git a/chart/templates/csi-driver.yaml b/chart/templates/csi-driver.yaml new file mode 100644 index 000000000..9b6ce1f3c --- /dev/null +++ b/chart/templates/csi-driver.yaml @@ -0,0 +1,228 @@ +{{/* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +*/}} + +{{- if .Values.csi.enabled -}} +--- +apiVersion: storage.k8s.io/v1 +kind: CSIDriver +metadata: + name: csi.vso.hashicorp.com + labels: + app.kubernetes.io/component: csi-driver + app: vault-secrets-operator-csi + {{- include "vso.chart.labels" . | nindent 4 }} +{{ include "vso.imagePullSecrets" .Values.csi.imagePullSecrets }} +spec: + podInfoOnMount: true + attachRequired: false + # republishing is handled by the csi driver's internal Pod controller, + # so it should always remain disabled. + requiresRepublish: false + volumeLifecycleModes: + - Ephemeral + tokenRequests: + - audience: csi.vso.hashicorp.com +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "vso.chart.fullname" . }}-csi + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/component: csi-driver + {{- include "vso.chart.labels" . | nindent 4 }} +{{ include "vso.imagePullSecrets" .Values.csi.imagePullSecrets }} +--- +kind: DaemonSet +apiVersion: apps/v1 +metadata: + name: {{ include "vso.chart.fullname" . }}-csi-node + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/component: csi-driver + app: vault-secrets-operator-csi + {{- include "vso.chart.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: vault-secrets-operator-csi + {{- include "vso.chart.selectorLabels" . | nindent 6 }} + {{- with .Values.csi.updateStrategy }} + updateStrategy: {{ toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + app.kubernetes.io/component: csi-driver + app: vault-secrets-operator-csi + {{- include "vso.chart.labels" . | nindent 8 }} + annotations: + {{- include "vso.csi.annotations" . | nindent 8 }} + spec: + serviceAccountName: {{ include "vso.chart.fullname" . }}-csi + {{- with .Values.csi.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.csi.affinity }} + affinity: + {{- toYaml .Values.csi.affinity | nindent 8 }} + {{- end }} + containers: + - name: node-driver-registrar + image: {{ .Values.csi.nodeDriverRegistrar.image.repository }}:{{ .Values.csi.nodeDriverRegistrar.image.tag }} + imagePullPolicy: {{ .Values.csi.livenessProbe.image.pullPolicy }} + args: + - --v=5 + - --csi-address=/csi/csi.sock + - --kubelet-registration-path=/var/lib/kubelet/plugins/vso-csi/csi.sock + {{- with .Values.csi.nodeDriverRegistrar.extraArgs }} + {{- toYaml . | nindent 8 }} + {{- end }} + livenessProbe: + exec: + command: + - /csi-node-driver-registrar + - --kubelet-registration-path=/var/lib/kubelet/plugins/vso-csi/csi.sock + - --mode=kubelet-registration-probe + initialDelaySeconds: 30 + timeoutSeconds: 15 + volumeMounts: + - name: plugin-dir + mountPath: /csi + - name: registration-dir + mountPath: /registration + resources: + limits: + cpu: 100m + memory: 100Mi + requests: + cpu: 10m + memory: 20Mi + - name: driver + image: {{ .Values.csi.driver.image.repository }}:{{ .Values.csi.driver.image.tag }} + command: + - /vault-secrets-operator-csi + args: + - "--endpoint=$(CSI_ENDPOINT)" + - "--nodeid=$(KUBE_NODE_NAME)" + {{- with include "vso.csiControllerLoggingArgs" . }} + {{- . }} + {{- end }} + {{- with include "vso.csiBackoffArgs" . }} + {{- . -}} + {{- end }} + {{- with .Values.csi.driver.extraArgs }} + {{- toYaml . | nindent 8 }} + {{- end }} + env: + - name: CSI_ENDPOINT + value: unix:///csi/csi.sock + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + {{- range .Values.csi.driver.extraEnv }} + - name: {{ .name }} + value: {{ .value }} + {{- end }} + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 30 + timeoutSeconds: 10 + periodSeconds: 15 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 30 + timeoutSeconds: 10 + periodSeconds: 15 + volumeMounts: + # TODO drop unnecessary mounts and volumes + - name: plugin-dir + mountPath: /csi + - name: mountpoint-dir + mountPath: /var/lib/kubelet/pods + # /var/run/vso-csi/csi-ccf6c4a4e77db8ef85a8ca4b67ba8f82213aad2f27cbdb5c66373a271b1c6135 + mountPropagation: Bidirectional + - name: providers-dir + mountPath: /var/run/vso-csi + - mountPath: /var/run/podinfo + name: podinfo + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 50m + memory: 100Mi + - name: liveness-probe + image: {{ .Values.csi.livenessProbe.image.repository }}:{{ .Values.csi.livenessProbe.image.tag }} + imagePullPolicy: {{ .Values.csi.livenessProbe.image.pullPolicy }} + args: + - --csi-address=/csi/csi.sock + - --probe-timeout=3s + - --http-endpoint=0.0.0.0:9808 + - -v=2 + {{- with .Values.csi.livenessProbe.extraArgs }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumeMounts: + - name: plugin-dir + mountPath: /csi + resources: + limits: + cpu: 100m + memory: 100Mi + requests: + cpu: 10m + memory: 20Mi + volumes: + - name: mountpoint-dir + hostPath: + path: /var/lib/kubelet/pods + type: DirectoryOrCreate + - name: registration-dir + hostPath: + path: /var/lib/kubelet/plugins_registry/ + type: Directory + - name: plugin-dir + hostPath: + path: /var/lib/kubelet/plugins/vso-csi/ + type: DirectoryOrCreate + - name: providers-dir + hostPath: + path: /var/run/vso-csi-providers + type: DirectoryOrCreate + - name: providers-dir-0 + hostPath: + path: "/etc/kubernetes/vso-csi-providers" + type: DirectoryOrCreate + - name: podinfo + downwardAPI: + items: + - fieldRef: + fieldPath: metadata.namespace + path: namespace + - fieldRef: + fieldPath: metadata.name + path: name + - fieldRef: + fieldPath: metadata.uid + path: uid + nodeSelector: + {{- include "vso.csi.nodeSelector" . | nindent 8 }} + tolerations: + {{- include "vso.csi.tolerations" . | nindent 8 }} +{{- end -}} diff --git a/chart/templates/csisecrets_editor_role.yaml b/chart/templates/csisecrets_editor_role.yaml new file mode 100644 index 000000000..a7f24ae3d --- /dev/null +++ b/chart/templates/csisecrets_editor_role.yaml @@ -0,0 +1,36 @@ +{{- /* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# auto generated by sync-rbac.sh from ./config/rbac/csisecrets_editor_role.yaml -- do not edit +*/ -}} + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "csisecrets-editor-role" }} + labels: + app.kubernetes.io/component: rbac + # allow for selecting on the canonical name + vso.hashicorp.com/role-instance: csisecrets-editor-role + vso.hashicorp.com/aggregate-to-editor: "true" + {{- include "vso.chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets/status + verbs: + - get diff --git a/chart/templates/csisecrets_viewer_role.yaml b/chart/templates/csisecrets_viewer_role.yaml new file mode 100644 index 000000000..e43526a3a --- /dev/null +++ b/chart/templates/csisecrets_viewer_role.yaml @@ -0,0 +1,32 @@ +{{- /* +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# auto generated by sync-rbac.sh from ./config/rbac/csisecrets_viewer_role.yaml -- do not edit +*/ -}} + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "csisecrets-viewer-role" }} + labels: + app.kubernetes.io/component: rbac + # allow for selecting on the canonical name + vso.hashicorp.com/role-instance: csisecrets-viewer-role + vso.hashicorp.com/aggregate-to-viewer: "true" + {{- include "vso.chart.labels" . | nindent 4 }} +rules: +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets + verbs: + - get + - list + - watch +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets/status + verbs: + - get diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index b9e7c71de..9ce542d1b 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -11,7 +11,7 @@ metadata: labels: app.kubernetes.io/component: controller-manager {{- include "vso.chart.labels" . | nindent 4 }} -{{ include "vso.imagePullSecrets" . }} +{{ include "vso.imagePullSecrets" .Values.controller.imagePullSecrets }} --- apiVersion: apps/v1 kind: Deployment diff --git a/chart/templates/hook-upgrade-crds.yaml b/chart/templates/hook-upgrade-crds.yaml index 699806e71..2cac5d28b 100644 --- a/chart/templates/hook-upgrade-crds.yaml +++ b/chart/templates/hook-upgrade-crds.yaml @@ -17,7 +17,7 @@ metadata: helm.sh/hook: pre-upgrade helm.sh/hook-delete-policy: "hook-succeeded,before-hook-creation" helm.sh/hook-weight: "1" -{{ include "vso.imagePullSecrets" . }} +{{ include "vso.imagePullSecrets" .Values.controller.imagePullSecrets }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/chart/templates/role.yaml b/chart/templates/role.yaml index fdff19680..e3554bd3a 100644 --- a/chart/templates/role.yaml +++ b/chart/templates/role.yaml @@ -77,6 +77,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets - hcpauths - hcpvaultsecretsapps - secrettransformations @@ -97,6 +98,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets/finalizers - hcpauths/finalizers - hcpvaultsecretsapps/finalizers - secrettransformations/finalizers @@ -111,6 +113,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets/status - hcpauths/status - hcpvaultsecretsapps/status - secrettransformations/status diff --git a/chart/values.yaml b/chart/values.yaml index 81f60ee4d..e65f20417 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -912,6 +912,183 @@ hooks: # @type: string executionTimeout: 30s +csi: + # Only supports Vault Enterprise servers. + # Toggles the deployment of the Vault Secrets Operator CSI driver. This will deploy the driver and the necessary + # resources to the cluster. + # @type: boolean + enabled: false + + # Host Aliases settings for the vault-secrets-operator-csi pods. + # The value is an array of PodSpec HostAlias maps. + # ref: https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ + # Example: + # hostAliases: + # - ip: 192.168.1.100 + # hostnames: + # - vault.example.com + # @type: array + hostAliases: [] + + # nodeSelector labels for vault-secrets-operator-csi pod assignment. + # @type: map + # ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + # Example: + # nodeSelector: + # beta.kubernetes.io/arch: amd64 + nodeSelector: {} + # Toleration Settings for vault-secrets-operator-csi pods. + # The value is an array of PodSpec Toleration maps. + # ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ + # @type: array + # Example: + # tolerations: + # - key: "key1" + # operator: "Equal" + # value: "value1" + # effect: "NoSchedule" + tolerations: [] + + # Affinity settings for vault-secrets-operator-csi pods. + # The value is a map of PodSpec Affinity maps. + # ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity + # Example: + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: topology.kubernetes.io/zone + # operator: In + # values: + # - antarctica-east1 + # - antarctica-west1 + affinity: {} + + # Image pull secret to use for private container registry authentication which will be applied to the controllers + # service account. Alternatively, the value may be specified as an array of strings. + # Example: + # ```yaml + # imagePullSecrets: + # - name: pull-secret-name-1 + # - name: pull-secret-name-2 + # ``` + # Refer to https://kubernetes.io/docs/concepts/containers/images/#using-a-private-registry. + # @type: array + imagePullSecrets: [] + + # Extra labels to attach to the deployment. This should be formatted as a YAML object (map) + extraLabels: {} + + # This value defines additional annotations for the deployment. This should be formatted as a YAML object (map) + annotations: {} + + driver: + # Image sets the repo and tag of the vault-secrets-operator-csi image to use for the driver. + image: + pullPolicy: IfNotPresent + repository: hashicorp/vault-secrets-operator-csi + tag: 0.0.0-dev + + # Defines additional environment variables to be added to the + # CSI driver container. + # @type: array + extraEnv: [] + + # Extra arguments to pass to the driver. + # @type: array + extraArgs: [] + + # logging + logging: + # Sets the log level for the driver. + # Builtin levels are: info, error, debug, debug-extended, trace + # Default: info + # @type: string + level: info + + # Sets the time encoding for the driver. + # Options are: epoch, millis, nano, iso8601, rfc3339, rfc3339nano + # Default: rfc3339 + # @type: string + timeEncoding: rfc3339 + + # Sets the stacktrace level for the driver. + # Options are: info, error, panic + # Default: panic + # @type: string + stacktraceLevel: panic + + # Backoff settings for the CSI driver. These settings control the backoff behavior + # when the driver encounters an error while fetching secrets from the SecretSource. + # For example given the following settings: + # initialInterval: 5s + # maxInterval: 60s + # randomizationFactor: 0.5 + # multiplier: 1.5 + # + # The backoff retry sequence might be something like: + # 5.5s, 7.5s, 11.25s, 16.87s, 25.3125s, 37.96s, 56.95, 60.95s... + # @type: object + backoffOnSecretSourceError: + # Initial interval between retries. + # @type: duration + initialInterval: "5s" + # Maximum interval between retries. + # @type: duration + maxInterval: "60s" + # Maximum elapsed time without a successful sync from the secret's source. + # It's important to note that setting this option to anything other than + # its default will result in the secret sync no longer being retried after + # reaching the max elapsed time. + # @type: duration + maxElapsedTime: "0s" + # Randomization factor randomizes the backoff interval between retries. + # This helps to spread out the retries to avoid a thundering herd. + # If the value is 0, then the backoff interval will not be randomized. + # It is recommended to set this to a value that is greater than 0. + # @type: float + randomizationFactor: 0.5 + # Sets the multiplier that is used to increase the backoff interval between retries. + # This value should always be set to a value greater than 0. + # The value must be greater than zero. + # @type: float + multiplier: 1.5 + + livenessProbe: + # Image sets the repo and tag of the image to use for the liveness probe. + image: + pullPolicy: IfNotPresent + repository: registry.k8s.io/sig-storage/livenessprobe + tag: v2.10.0 + + # Extra arguments to pass to the liveness probe container. + # @type: array + extraArgs: [] + + nodeDriverRegistrar: + # Image sets the repo and tag of the image to use for the node driver registrar. + image: + pullPolicy: IfNotPresent + repository: registry.k8s.io/sig-storage/csi-node-driver-registrar + tag: v2.8.0 + + # Extra arguments to pass to the node driver registrar container. + # @type: array + extraArgs: [] + + # Configure update strategy for the DaemonSet + # Kubernetes supports types Recreate, and RollingUpdate + # ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + # Example: + # strategy: {} + # rollingUpdate: + # maxSurge: 1 + # maxUnavailable: 0 + # type: RollingUpdate + # @type: object + updateStrategy: {} + ## Used by unit tests, and will not be rendered except when using `helm template`, this can be safely ignored. tests: # @type: boolean diff --git a/common/common.go b/common/common.go index d422e7c5d..69f93550b 100644 --- a/common/common.go +++ b/common/common.go @@ -21,6 +21,7 @@ import ( secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1" "github.com/hashicorp/vault-secrets-operator/consts" vaultcredsconsts "github.com/hashicorp/vault-secrets-operator/credentials/vault/consts" + "github.com/hashicorp/vault-secrets-operator/internal/version" "github.com/hashicorp/vault-secrets-operator/utils" ) @@ -30,6 +31,7 @@ var ( InvalidObjectKeyError = fmt.Errorf("invalid objectKey") InvalidObjectKeyErrorEmptyName = fmt.Errorf("%w, empty name", InvalidObjectKeyError) InvalidObjectKeyErrorEmptyNamespace = fmt.Errorf("%w, empty namespace", InvalidObjectKeyError) + DefaultVSOUserAgent = fmt.Sprintf("vso/%s", version.Version().String()) defaultMaxRetries uint64 = 60 defaultRetryDuration = time.Millisecond * 500 ) @@ -101,12 +103,16 @@ func init() { } func getAuthRefNamespacedName(obj client.Object) (types.NamespacedName, error) { - m, err := NewSyncableSecretMetaData(obj) + m, err := NewSyncableSecretMetaDataI(obj) if err != nil { return types.NamespacedName{}, err } - authRef, err := ParseResourceRef(m.AuthRef, obj.GetNamespace()) + return getAuthRefNamespacedNameForMeta(m) +} + +func getAuthRefNamespacedNameForMeta(m SyncableSecretMetaDataI) (types.NamespacedName, error) { + authRef, err := ParseResourceRef(m.GetAuthRef(), m.GetNamespace()) if err != nil { return types.NamespacedName{}, err } @@ -201,15 +207,28 @@ func GetVaultAuthNamespaced(ctx context.Context, c ctrlclient.Client, obj ctrlcl return nil, err } - authObj, err := GetVaultAuthWithRetry(ctx, c, authRef, defaultRetryDuration, defaultMaxRetries) + return getVaultAuth(ctx, c, authRef, obj.GetNamespace(), globalOpts) +} + +func GetVaultAuthNamespacedForMeta(ctx context.Context, c ctrlclient.Client, m SyncableSecretMetaDataI, globalOpts *GlobalVaultAuthOptions) (*secretsv1beta1.VaultAuth, error) { + authRef, err := getAuthRefNamespacedNameForMeta(m) if err != nil { return nil, err } - if !isAllowedNamespace(authObj, obj.GetNamespace(), authObj.Spec.AllowedNamespaces...) { + return getVaultAuth(ctx, c, authRef, m.GetNamespace(), globalOpts) +} + +func getVaultAuth(ctx context.Context, c ctrlclient.Client, key types.NamespacedName, targetNS string, globalOpts *GlobalVaultAuthOptions) (*secretsv1beta1.VaultAuth, error) { + authObj, err := GetVaultAuthWithRetry(ctx, c, key, defaultRetryDuration, defaultMaxRetries) + if err != nil { + return nil, err + } + + if !isAllowedNamespace(authObj, targetNS, authObj.Spec.AllowedNamespaces...) { return nil, &NamespaceNotAllowedError{ - TargetNS: obj.GetNamespace(), - ObjRef: authRef, + TargetNS: targetNS, + ObjRef: key, RefKind: "VaultAuth", } } @@ -734,18 +753,12 @@ func FindVaultAuthForStorageEncryption(ctx context.Context, c client.Client) (*s // // Supported types for obj are: VaultDynamicSecret, VaultStaticSecret. VaultPKISecret func GetVaultNamespace(obj client.Object) (string, error) { - var ns string - switch o := obj.(type) { - case *secretsv1beta1.VaultPKISecret: - ns = o.Spec.Namespace - case *secretsv1beta1.VaultStaticSecret: - ns = o.Spec.Namespace - case *secretsv1beta1.VaultDynamicSecret: - ns = o.Spec.Namespace - default: - return "", fmt.Errorf("unsupported type %T", o) + meta, err := NewSyncableSecretMetaDataI(obj) + if err != nil { + return "", err } - return ns, nil + + return meta.GetVaultNamespace(), nil } func ValidateObjectKey(key ctrlclient.ObjectKey) error { @@ -758,6 +771,51 @@ func ValidateObjectKey(key ctrlclient.ObjectKey) error { return nil } +type SyncableSecretMetaDataI interface { + GetAPIVersion() string + GetKind() string + GetName() string + GetNamespace() string + GetVaultNamespace() string + GetDestination() *secretsv1beta1.Destination + GetTransformation() *secretsv1beta1.Transformation + GetAuthRef() string + GetProviderNamespace() string +} + +func (s *SyncableSecretMetaData) GetAPIVersion() string { + return s.APIVersion +} + +func (s *SyncableSecretMetaData) GetKind() string { + return s.Kind +} + +func (s *SyncableSecretMetaData) GetName() string { + return s.Name +} + +func (s *SyncableSecretMetaData) GetNamespace() string { + return s.Namespace +} + +func (s *SyncableSecretMetaData) GetDestination() *secretsv1beta1.Destination { + return s.Destination +} + +func (s *SyncableSecretMetaData) GetTransformation() *secretsv1beta1.Transformation { + if s.Destination == nil { + return nil + } + return &s.Destination.Transformation +} + +func (s *SyncableSecretMetaData) GetAuthRef() string { + return s.AuthRef +} + +var _ SyncableSecretMetaDataI = &SyncableSecretMetaData{} + // SyncableSecretMetaData provides common data structure that extracts the bits pertinent // when handling any of the sync-able secret custom resource types. // @@ -771,11 +829,54 @@ type SyncableSecretMetaData struct { Name string // Namespace Namespace string + // VaultNamespace + VaultNamespace string // Destination of the syncable-secret object. Maps to obj.Spec.Destination. Destination *secretsv1beta1.Destination AuthRef string } +func (s *SyncableSecretMetaData) GetProviderNamespace() string { + return s.GetNamespace() +} + +func (s *SyncableSecretMetaData) GetVaultNamespace() string { + return s.VaultNamespace +} + +var _ SyncableSecretMetaDataI = &csiSecretsMetaData{} + +type csiSecretsMetaData struct { + SyncableSecretMetaData + transformation *secretsv1beta1.Transformation + authRef *secretsv1beta1.VaultAuthRef +} + +func (s *csiSecretsMetaData) GetAuthRef() string { + if s.authRef == nil { + return "" + } + + if s.authRef.Namespace != "" { + return fmt.Sprintf("%s/%s", s.authRef.Namespace, s.authRef.Name) + } + + return s.authRef.Name +} + +func (s *csiSecretsMetaData) GetProviderNamespace() string { + if s.authRef != nil { + if s.authRef.TrustNamespace && s.authRef.Namespace != "" { + return s.authRef.Namespace + } + } + return s.GetNamespace() +} + +func (s *csiSecretsMetaData) GetTransformation() *secretsv1beta1.Transformation { + return s.transformation +} + // NewSyncableSecretMetaData returns SyncableSecretMetaData if obj is a supported type. // An error will be returned of obj is not a supported type. // @@ -814,6 +915,55 @@ func NewSyncableSecretMetaData(obj ctrlclient.Object) (*SyncableSecretMetaData, return meta, nil } +// NewSyncableSecretMetaDataI returns SyncableSecretMetaData if obj is a supported type. +// An error will be returned of obj is not a supported type. +// +// Supported types for obj are: VaultDynamicSecret, VaultStaticSecret. VaultPKISecret +func NewSyncableSecretMetaDataI(obj ctrlclient.Object) (SyncableSecretMetaDataI, error) { + meta := &SyncableSecretMetaData{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + + switch t := obj.(type) { + case *secretsv1beta1.VaultDynamicSecret: + meta.Destination = t.Spec.Destination.DeepCopy() + meta.APIVersion = t.APIVersion + meta.Kind = t.Kind + meta.AuthRef = t.Spec.VaultAuthRef + meta.VaultNamespace = t.Spec.Namespace + case *secretsv1beta1.VaultStaticSecret: + meta.Destination = t.Spec.Destination.DeepCopy() + meta.APIVersion = t.APIVersion + meta.Kind = t.Kind + meta.AuthRef = t.Spec.VaultAuthRef + meta.VaultNamespace = t.Spec.Namespace + case *secretsv1beta1.VaultPKISecret: + meta.Destination = t.Spec.Destination.DeepCopy() + meta.APIVersion = t.APIVersion + meta.Kind = t.Kind + meta.AuthRef = t.Spec.VaultAuthRef + meta.VaultNamespace = t.Spec.Namespace + case *secretsv1beta1.HCPVaultSecretsApp: + meta.Destination = t.Spec.Destination.DeepCopy() + meta.APIVersion = t.APIVersion + meta.Kind = t.Kind + meta.AuthRef = t.Spec.HCPAuthRef + case *secretsv1beta1.CSISecrets: + meta.Kind = t.Kind + meta.APIVersion = t.APIVersion + meta.VaultNamespace = t.Spec.Namespace + return &csiSecretsMetaData{ + SyncableSecretMetaData: *meta, + authRef: t.Spec.VaultAuthRef, + }, nil + default: + return nil, fmt.Errorf("unsupported type %T", t) + } + + return meta, nil +} + // FindVaultAuthGlobalDefault returns the default VaultAuthGlobal object in the // given namespaces. If the default object is not found in the given namespaces, // an error is returned. diff --git a/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml b/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml new file mode 100644 index 000000000..69b0676f8 --- /dev/null +++ b/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml @@ -0,0 +1,561 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: csisecrets.secrets.hashicorp.com +spec: + group: secrets.hashicorp.com + names: + kind: CSISecrets + listKind: CSISecretsList + plural: csisecrets + singular: csisecrets + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: CSISecrets is the Schema for the csisecrets API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + CSISecretsSpec defines the desired state of CSISecrets. It contains the + configuration for the CSI driver to populate the secret data. + properties: + accessControl: + description: AccessControl provides configuration for controlling + access to the secret. + properties: + matchPolicy: + default: all + description: |- + MatchPolicy is the policy to use when matching the access control rules. If + set to "any", only one of the rules should match. If set to "all", all the + rules should match. + enum: + - any + - all + type: string + namespacePatterns: + description: NamespacePatterns is a list of namespace name regex + patterns that are allowed access. + items: + type: string + type: array + podLabels: + additionalProperties: + type: string + description: PodLabels is a map of pod label key-value pairs that + should be allowed access. + type: object + podNamePatterns: + description: PodNamePatterns is a list of pod name regex patterns + that should be allowed access. + items: + type: string + type: array + serviceAccountPattern: + description: |- + ServiceAccountPattern is the name of the service account that should be used to + access the secret. It can be specified as a regex pattern. + A valid service account is always required. + type: string + required: + - serviceAccountPattern + type: object + namespace: + description: Namespace is the Vault namespace where the secret is + located. + type: string + secrets: + description: Secrets that will be synced with the CSI driver. + properties: + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation resource. + type: string + namespace: + description: Namespace of the SecretTransformation resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + vaultAppRoleSecretIDs: + description: VaultAppRoleSecretIDs is a list of AppRole secret + IDs to be used to populate the secret. + items: + description: VaultAppRoleSecretID defines the AppRole secret + ID to be used to populate the secret. + properties: + cidrList: + description: CIDRList is the list of CIDR blocks that access + the secret ID. + items: + type: string + type: array + metadata: + additionalProperties: + type: string + description: |- + Metadata is the metadata to be associated with the secret ID. It is set on + the token generated by the secret ID. + type: object + mount: + description: Mount path to the AppRole auth engine. + type: string + numUses: + description: NumUses is the number of times the secret ID + can be used. + type: integer + role: + description: Role is the name of the AppRole. + type: string + syncRoleID: + description: |- + SyncRoleID is the flag to fetch the role ID from the AppRole auth engine. + Requires that the provisioning VaultAuth has the necessary permissions to fetch the role ID. + type: boolean + tokenBoundCIDRs: + description: |- + TokenBoundCIDRs is the list of CIDR blocks that can be used to authenticate + using tokens generated by this secret ID. + items: + type: string + type: array + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation + resource. + type: string + namespace: + description: Namespace of the SecretTransformation + resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + ttl: + description: TTL is the TTL for the secret ID, after which + it becomes invalid. + pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$ + type: string + wrapTTL: + description: WrapTTL is the TTL for the wrapped secret ID. + pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$ + type: string + required: + - mount + - role + type: object + type: array + vaultStaticSecrets: + description: VaultStaticSecrets is a list of static secrets to + be synced by the CSI driver. + items: + properties: + mount: + description: Mount for the secret in Vault + type: string + path: + description: |- + Path of the secret in Vault, corresponds to the `path` parameter for: + kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret + kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version + type: string + transformation: + description: |- + Transformation provides configuration for transforming the secret data before + it is stored in the CSI volume. + properties: + excludeRaw: + description: |- + ExcludeRaw data from the destination Secret. Exclusion policy can be set + globally by including 'exclude-raw` in the '--global-transformation-options' + command line flag. If set, the command line flag always takes precedence over + this configuration. + type: boolean + excludes: + description: |- + Excludes contains regex patterns used to filter top-level source secret data + fields for exclusion from the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied before any inclusion patterns. To exclude all source secret data + fields, you can configure the single pattern ".*". + items: + type: string + type: array + includes: + description: |- + Includes contains regex patterns used to filter top-level source secret data + fields for inclusion in the final K8s Secret data. These pattern filters are + never applied to templated fields as defined in Templates. They are always + applied last. + items: + type: string + type: array + templates: + additionalProperties: + description: Template provides templating configuration. + properties: + name: + description: Name of the Template + type: string + text: + description: |- + Text contains the Go text template format. The template + references attributes from the data structure of the source secret. + Refer to https://pkg.go.dev/text/template for more information. + type: string + required: + - text + type: object + description: |- + Templates maps a template name to its Template. Templates are always included + in the rendered K8s Secret, and take precedence over templates defined in a + SecretTransformation. + type: object + transformationRefs: + description: |- + TransformationRefs contain references to template configuration from + SecretTransformation. + items: + description: |- + TransformationRef contains the configuration for accessing templates from an + SecretTransformation resource. TransformationRefs can be shared across all + syncable secret custom resources. + properties: + ignoreExcludes: + description: |- + IgnoreExcludes controls whether to use the SecretTransformation's Excludes + data key filters. + type: boolean + ignoreIncludes: + description: |- + IgnoreIncludes controls whether to use the SecretTransformation's Includes + data key filters. + type: boolean + name: + description: Name of the SecretTransformation + resource. + type: string + namespace: + description: Namespace of the SecretTransformation + resource. + type: string + templateRefs: + description: |- + TemplateRefs map to a Template found in this TransformationRef. If empty, then + all templates from the SecretTransformation will be rendered to the K8s Secret. + items: + description: |- + TemplateRef points to templating text that is stored in a + SecretTransformation custom resource. + properties: + keyOverride: + description: |- + KeyOverride to the rendered template in the Destination secret. If Key is + empty, then the Key from reference spec will be used. Set this to override the + Key set from the reference spec. + type: string + name: + description: |- + Name of the Template in SecretTransformationSpec.Templates. + the rendered secret data. + type: string + required: + - name + type: object + type: array + required: + - name + type: object + type: array + type: object + type: + description: Type of the Vault static secret + enum: + - kv-v1 + - kv-v2 + type: string + version: + description: |- + Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter: + https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version + minimum: 0 + type: integer + required: + - mount + - path + - type + type: object + type: array + type: object + syncConfig: + description: SyncConfig provides configuration for syncing the secret + data with the CSI driver. + properties: + containerState: + description: |- + ContainerState is the state of the container that the CSI driver always sync + on. This configuration is useful to sync when the last state of the container + is in the terminated state and the restart count is greater than 0. + properties: + imagePattern: + description: ImagePattern of the container. Can be expressed + as a regular expression. + type: string + namePattern: + description: NamePattern of the container. Can be expressed + as a regular expression. + type: string + type: object + required: + - containerState + type: object + vaultAuthRef: + description: VaultAuthRef is the reference to the VaultAuth resource. + properties: + name: + description: Name of the VaultAuth resource. + type: string + namespace: + description: Namespace of the VaultAuth resource. + type: string + trustNamespace: + description: |- + TrustNamespace of the referring VaultAuth resource. This means that any Vault + credentials will be provided by resources in the same namespace as the + VaultAuth resource. Otherwise, the credentials will be provided by the secret + resource's namespace. + type: boolean + required: + - name + type: object + required: + - accessControl + - secrets + type: object + status: + description: CSISecretsStatus defines the observed state of CSISecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/secrets.hashicorp.com_secrettransformations.yaml b/config/crd/bases/secrets.hashicorp.com_secrettransformations.yaml index 021ed6b40..96ad84937 100644 --- a/config/crd/bases/secrets.hashicorp.com_secrettransformations.yaml +++ b/config/crd/bases/secrets.hashicorp.com_secrettransformations.yaml @@ -46,7 +46,7 @@ spec: excludes: description: |- Excludes contains regex patterns used to filter top-level source secret data - fields for exclusion from the final K8s Secret data. These pattern filters are + fields for exclusion from the final secret data. These pattern filters are never applied to templated fields as defined in Templates. They are always applied before any inclusion patterns. To exclude all source secret data fields, you can configure the single pattern ".*". @@ -56,7 +56,7 @@ spec: includes: description: |- Includes contains regex patterns used to filter top-level source secret data - fields for inclusion in the final K8s Secret data. These pattern filters are + fields for inclusion in the final secret data. These pattern filters are never applied to templated fields as defined in Templates. They are always applied last. items: @@ -64,7 +64,7 @@ spec: type: array sourceTemplates: description: |- - SourceTemplates are never included in the rendered K8s Secret, they can be + SourceTemplates are never included in the rendered secret, they can be used to provide common template definitions, etc. items: description: SourceTemplate provides source templating configuration. @@ -99,7 +99,7 @@ spec: type: object description: |- Templates maps a template name to its Template. Templates are always included - in the rendered K8s Secret with the specified key. + in the rendered secret with the specified key. type: object type: object status: diff --git a/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml b/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml index 4086afeee..b5146c510 100644 --- a/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml +++ b/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml @@ -313,7 +313,7 @@ spec: description: |- Namespace of the VaultAuthGlobal resource. If not provided, the namespace of the referring VaultAuth resource is used. - pattern: ^([a-z0-9.-]{1,253})$ + pattern: ^([a-z0-9-]{1,63})$ type: string type: object vaultConnectionRef: diff --git a/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml index f916e2222..3978f9eb5 100644 --- a/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml +++ b/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml @@ -207,7 +207,7 @@ spec: type: string path: description: |- - Path of the secret in Vault, corresponds to the `path` parameter for, + Path of the secret in Vault, corresponds to the `path` parameter for: kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version type: string diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 009cb25dd..6539169d1 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,7 @@ resources: - bases/secrets.hashicorp.com_hcpauths.yaml - bases/secrets.hashicorp.com_secrettransformations.yaml - bases/secrets.hashicorp.com_vaultauthglobals.yaml +- bases/secrets.hashicorp.com_csisecrets.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/crd/patches/cainjection_in_csisecrets.yaml b/config/crd/patches/cainjection_in_csisecrets.yaml new file mode 100644 index 000000000..511bbe72a --- /dev/null +++ b/config/crd/patches/cainjection_in_csisecrets.yaml @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: csisecrets.secrets.hashicorp.com diff --git a/config/crd/patches/webhook_in_csisecrets.yaml b/config/crd/patches/webhook_in_csisecrets.yaml new file mode 100644 index 000000000..ae6279f80 --- /dev/null +++ b/config/crd/patches/webhook_in_csisecrets.yaml @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: csisecrets.secrets.hashicorp.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/csisecrets_editor_role.yaml b/config/rbac/csisecrets_editor_role.yaml new file mode 100644 index 000000000..677aceb17 --- /dev/null +++ b/config/rbac/csisecrets_editor_role.yaml @@ -0,0 +1,34 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# permissions for end users to edit csisecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: csisecrets-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-secrets-operator + app.kubernetes.io/part-of: vault-secrets-operator + app.kubernetes.io/managed-by: kustomize + name: csisecrets-editor-role +rules: +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets/status + verbs: + - get diff --git a/config/rbac/csisecrets_viewer_role.yaml b/config/rbac/csisecrets_viewer_role.yaml new file mode 100644 index 000000000..30fd42079 --- /dev/null +++ b/config/rbac/csisecrets_viewer_role.yaml @@ -0,0 +1,30 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# permissions for end users to view csisecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: csisecrets-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: vault-secrets-operator + app.kubernetes.io/part-of: vault-secrets-operator + app.kubernetes.io/managed-by: kustomize + name: csisecrets-viewer-role +rules: +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets + verbs: + - get + - list + - watch +- apiGroups: + - secrets.hashicorp.com + resources: + - csisecrets/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0ae6b4415..0ca5fc300 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -68,6 +68,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets - hcpauths - hcpvaultsecretsapps - secrettransformations @@ -88,6 +89,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets/finalizers - hcpauths/finalizers - hcpvaultsecretsapps/finalizers - secrettransformations/finalizers @@ -102,6 +104,7 @@ rules: - apiGroups: - secrets.hashicorp.com resources: + - csisecrets/status - hcpauths/status - hcpvaultsecretsapps/status - secrettransformations/status diff --git a/config/samples/secrets_v1beta1_csisecrets.yaml b/config/samples/secrets_v1beta1_csisecrets.yaml new file mode 100644 index 000000000..6428a351f --- /dev/null +++ b/config/samples/secrets_v1beta1_csisecrets.yaml @@ -0,0 +1,29 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: CSISecrets +metadata: + labels: + app.kubernetes.io/name: csisecrets + app.kubernetes.io/instance: csisecrets-sample + app.kubernetes.io/part-of: vault-secrets-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: vault-secrets-operator + name: csisecrets-sample + namespace: default +spec: + sources: + - kind: VaultStaticSecret + name: static-secret + - kind: VaultDynamicSecret + name: dynamic-secret + - kind: VaultToken + name: token + - kind: HCPVaultSecretsApp + name: hcp-vault-secrets-app + transformation: + transformationRefs: + - name: default + namespace: default diff --git a/consts/consts.go b/consts/consts.go index 2852f2fef..6bf50a388 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -23,4 +23,5 @@ const ( AWSSessionToken = "session_token" AnnotationResync = "vso.hashicorp.com/resync" + HeaderUserAgent = "User-Agent" ) diff --git a/controllers/csisecrets_controller.go b/controllers/csisecrets_controller.go new file mode 100644 index 000000000..e2c365846 --- /dev/null +++ b/controllers/csisecrets_controller.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1" +) + +// CSISecretsReconciler reconciles a CSISecrets object +type CSISecretsReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=secrets.hashicorp.com,resources=csisecrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=secrets.hashicorp.com,resources=csisecrets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=secrets.hashicorp.com,resources=csisecrets/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the CSISecrets object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *CSISecretsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CSISecretsReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&secretsv1beta1.CSISecrets{}). + Complete(r) +} diff --git a/controllers/eventhandlers.go b/controllers/eventhandlers.go index 52b645050..7cf1456a3 100644 --- a/controllers/eventhandlers.go +++ b/controllers/eventhandlers.go @@ -27,18 +27,41 @@ var maxRequeueAfter = time.Second * 1 // instance. It includes a ValidatorFunc that prevents the referring objects from // being queued for reconciliation. func NewEnqueueRefRequestsHandlerST(refCache ResourceReferenceCache, syncReg *SyncRegistry) handler.EventHandler { - return NewEnqueueRefRequestsHandler( - SecretTransformation, refCache, syncReg, - ValidateSecretTransformation, - ) + return NewEnqueueRefRequestsHandlerWithOptions(&EnqueueRefRequestOptions{ + Validator: ValidateSecretTransformation, + SyncRegistry: syncReg, + RefCache: refCache, + Kind: SecretTransformation, + }) } +type EnqueueRefRequestOptions struct { + Validator ValidatorFunc + MaxRequeueAfter time.Duration + SyncRegistry *SyncRegistry + RefCache ResourceReferenceCache + Kind ResourceKind +} + +// NewEnqueueRefRequestsHandler returns a handler.EventHandler for Watchers of ResourceKind. +// Deprecated: Use NewEnqueueRefRequestsHandlerWithOptions instead. func NewEnqueueRefRequestsHandler(kind ResourceKind, refCache ResourceReferenceCache, syncReg *SyncRegistry, validator ValidatorFunc) handler.EventHandler { + return NewEnqueueRefRequestsHandlerWithOptions(&EnqueueRefRequestOptions{ + Kind: kind, + RefCache: refCache, + SyncRegistry: syncReg, + Validator: validator, + }) +} + +// NewEnqueueRefRequestsHandlerWithOptions returns a handler.EventHandler for +// Watchers of ResourceKind. +func NewEnqueueRefRequestsHandlerWithOptions(opts *EnqueueRefRequestOptions) handler.EventHandler { return &enqueueRefRequestsHandler{ - kind: kind, - refCache: refCache, - syncReg: syncReg, - validator: validator, + kind: opts.Kind, + refCache: opts.RefCache, + syncReg: opts.SyncRegistry, + validator: opts.Validator, } } diff --git a/controllers/hcpvaultsecretsapp_controller.go b/controllers/hcpvaultsecretsapp_controller.go index 5e61248aa..420e0d678 100644 --- a/controllers/hcpvaultsecretsapp_controller.go +++ b/controllers/hcpvaultsecretsapp_controller.go @@ -40,12 +40,10 @@ import ( "github.com/hashicorp/vault-secrets-operator/common" "github.com/hashicorp/vault-secrets-operator/consts" "github.com/hashicorp/vault-secrets-operator/helpers" - "github.com/hashicorp/vault-secrets-operator/internal/version" ) const ( headerHVSRequester = "X-HVS-Requester" - headerUserAgent = "User-Agent" hcpVaultSecretsAppFinalizer = "hcpvaultsecretsapp.secrets.hashicorp.com/finalizer" @@ -67,7 +65,6 @@ const ( ) var ( - userAgent = fmt.Sprintf("vso/%s", version.Version().String()) // hvsErrorRe is a regexp to parse the error message from the HVS API // The error message is expected to be in the format: // [METHOD PATH_PATTERN][STATUS_CODE] @@ -302,7 +299,7 @@ func (r *HCPVaultSecretsAppReconciler) updateStatus(ctx context.Context, o *secr // SetupWithManager sets up the controller with the Manager. func (r *HCPVaultSecretsAppReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { - r.referenceCache = newResourceReferenceCache() + r.referenceCache = NewResourceReferenceCache() if r.BackOffRegistry == nil { r.BackOffRegistry = NewBackOffRegistry() } @@ -402,8 +399,8 @@ type transport struct { // RoundTrip is a wrapper implementation of the http.RoundTrip interface to // inject a header for identifying the requester type func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add(headerUserAgent, userAgent) - req.Header.Add(headerHVSRequester, userAgent) + req.Header.Add(consts.HeaderUserAgent, common.DefaultVSOUserAgent) + req.Header.Add(headerHVSRequester, common.DefaultVSOUserAgent) return t.child.RoundTrip(req) } diff --git a/controllers/registry.go b/controllers/registry.go index 1f5fb819a..24f667f8c 100644 --- a/controllers/registry.go +++ b/controllers/registry.go @@ -13,6 +13,40 @@ import ( type ResourceKind int +/* +{ + "level": "info", + "ts": "2024-10-27T19:47:38Z", + "msg": "Set ref cache", + "nodeName": "vso-demo-worker3", + "controller": "pod", + "controllerGroup": "", + "controllerKind": "Pod", + "Pod": { + "name": "vso-csi-app-bdb9bc6bb-pqxk4", + "namespace": "demo-ns-vso-csi" + }, + "namespace": "demo-ns-vso-csi", + "name": "vso-csi-app-bdb9bc6bb-pqxk4", + "reconcileID": "a4bceb93-14d2-491e-a13b-836fd454119f", + "req": { + "name": "vso-csi-app-bdb9bc6bb-pqxk4", + "namespace": "demo-ns-vso-csi" + }, + "cacheLen": 1, + "cacheKeys": [ + 7 + ], + "cacheValues": [ + { + "Namespace": "demo-ns-vso-csi", + "Name": "vso-csi-app-bdb9bc6bb-pqxk4" + } + ] +} + +*/ + const ( SecretTransformation ResourceKind = iota VaultDynamicSecret @@ -21,6 +55,8 @@ const ( HCPVaultSecretsApp VaultAuth VaultAuthGlobal + CSISecrets + Pod ) func (k ResourceKind) String() string { @@ -39,6 +75,10 @@ func (k ResourceKind) String() string { return "VaultAuth" case VaultAuthGlobal: return "VaultAuthGlobal" + case CSISecrets: + return "CSISecrets" + case Pod: + return "Pod" default: return "unknown" } @@ -49,13 +89,16 @@ type ResourceReferenceCache interface { Get(ResourceKind, client.ObjectKey) []client.ObjectKey Remove(ResourceKind, client.ObjectKey) bool Prune(ResourceKind, client.ObjectKey) int + Len() int + Keys() []ResourceKind + Values(ResourceKind) []client.ObjectKey } var _ ResourceReferenceCache = (*resourceReferenceCache)(nil) -// newResourceReferenceCache returns the default ReferenceCache that be used to +// NewResourceReferenceCache returns the default ReferenceCache that be used to // store object references for quick access by secret controllers. -func newResourceReferenceCache() ResourceReferenceCache { +func NewResourceReferenceCache() ResourceReferenceCache { return &resourceReferenceCache{ m: refCacheMap{}, } @@ -80,6 +123,45 @@ type resourceReferenceCache struct { mu sync.RWMutex } +func (c *resourceReferenceCache) Keys() []ResourceKind { + c.mu.RLock() + defer c.mu.RUnlock() + if c.m == nil { + return nil + } + + var keys []ResourceKind + for k := range c.m { + keys = append(keys, k) + } + return keys +} + +func (c *resourceReferenceCache) Values(kind ResourceKind) []client.ObjectKey { + c.mu.RLock() + defer c.mu.RUnlock() + if c.m == nil { + return nil + } + + scope, ok := c.scoped(kind, false) + if !ok { + return nil + } + + var keys []client.ObjectKey + for k := range scope { + keys = append(keys, k) + } + return keys +} + +func (c *resourceReferenceCache) Len() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.m) +} + // Set references of kind for referrer. func (c *resourceReferenceCache) Set(kind ResourceKind, referrer client.ObjectKey, references ...client.ObjectKey) { c.mu.Lock() diff --git a/controllers/vaultauth_controller.go b/controllers/vaultauth_controller.go index 524c526f4..aac327d53 100644 --- a/controllers/vaultauth_controller.go +++ b/controllers/vaultauth_controller.go @@ -240,7 +240,7 @@ func (r *VaultAuthReconciler) handleFinalizer(ctx context.Context, o *secretsv1b // SetupWithManager sets up the controller with the Manager. func (r *VaultAuthReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.referenceCache = newResourceReferenceCache() + r.referenceCache = NewResourceReferenceCache() return ctrl.NewControllerManagedBy(mgr). For(&secretsv1beta1.VaultAuth{}). Watches( diff --git a/controllers/vaultdynamicsecret_controller.go b/controllers/vaultdynamicsecret_controller.go index 2472fb943..8b0876536 100644 --- a/controllers/vaultdynamicsecret_controller.go +++ b/controllers/vaultdynamicsecret_controller.go @@ -372,9 +372,9 @@ func (r *VaultDynamicSecretReconciler) doVault(ctx context.Context, c vault.Clie logger = logger.WithValues("path", path, "method", method) switch method { case http.MethodPut, http.MethodPost: - resp, err = c.Write(ctx, vault.NewWriteRequest(path, params)) + resp, err = c.Write(ctx, vault.NewWriteRequest(path, params, nil)) case http.MethodGet: - resp, err = c.Read(ctx, vault.NewReadRequest(path, nil)) + resp, err = c.Read(ctx, vault.NewReadRequest(path, nil, nil)) default: return nil, fmt.Errorf("unsupported HTTP method %q for sync", method) } @@ -577,7 +577,7 @@ func (r *VaultDynamicSecretReconciler) renewLease( resp, err := c.Write(ctx, vault.NewWriteRequest("/sys/leases/renew", map[string]any{ "lease_id": o.Status.SecretLease.ID, "increment": o.Status.SecretLease.LeaseDuration, - })) + }, nil)) if err != nil { return nil, err } @@ -596,7 +596,7 @@ func (r *VaultDynamicSecretReconciler) renewLease( // SetupWithManager sets up the controller with the Manager. func (r *VaultDynamicSecretReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { - r.referenceCache = newResourceReferenceCache() + r.referenceCache = NewResourceReferenceCache() if r.BackOffRegistry == nil { r.BackOffRegistry = NewBackOffRegistry() } @@ -690,7 +690,7 @@ func (r *VaultDynamicSecretReconciler) revokeLease(ctx context.Context, o *secre } if _, err = c.Write(ctx, vault.NewWriteRequest("/sys/leases/revoke", map[string]any{ "lease_id": leaseID, - })); err != nil { + }, nil)); err != nil { msg := "Failed to revoke lease" r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretLeaseRevoke, msg+": %s", err) logger.Error(err, "Failed to revoke lease ", "id", leaseID) diff --git a/controllers/vaultdynamicsecret_controller_test.go b/controllers/vaultdynamicsecret_controller_test.go index abe315110..c2bfc99ca 100644 --- a/controllers/vaultdynamicsecret_controller_test.go +++ b/controllers/vaultdynamicsecret_controller_test.go @@ -1247,6 +1247,11 @@ type vaultResponse struct { data map[string]any } +func (s *vaultResponse) WrapInfo() *api.SecretWrapInfo { + // TODO implement me + panic("implement me") +} + func (s *vaultResponse) Secret() *api.Secret { return nil } diff --git a/controllers/vaultpkisecret_controller.go b/controllers/vaultpkisecret_controller.go index aae3c44d5..3d5716467 100644 --- a/controllers/vaultpkisecret_controller.go +++ b/controllers/vaultpkisecret_controller.go @@ -180,7 +180,7 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque }, nil } - resp, err := c.Write(ctx, vault.NewWriteRequest(path, o.GetIssuerAPIData())) + resp, err := c.Write(ctx, vault.NewWriteRequest(path, o.GetIssuerAPIData(), nil)) if err != nil { if vault.IsForbiddenError(err) { c.Taint() @@ -347,7 +347,7 @@ func (r *VaultPKISecretReconciler) handleDeletion(ctx context.Context, o *secret } func (r *VaultPKISecretReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { - r.referenceCache = newResourceReferenceCache() + r.referenceCache = NewResourceReferenceCache() if r.BackOffRegistry == nil { r.BackOffRegistry = NewBackOffRegistry() } @@ -404,7 +404,7 @@ func (r *VaultPKISecretReconciler) revokeCertificate(ctx context.Context, l logr if _, err := c.Write(ctx, vault.NewWriteRequest(fmt.Sprintf("%s/revoke", s.Spec.Mount), map[string]any{ "serial_number": s.Status.SerialNumber, - })); err != nil { + }, nil)); err != nil { l.Error(err, "Failed to revoke certificate", "serial_number", s.Status.SerialNumber) return err } diff --git a/controllers/vaultstaticsecret_controller.go b/controllers/vaultstaticsecret_controller.go index e4e4f3d07..d5bd57148 100644 --- a/controllers/vaultstaticsecret_controller.go +++ b/controllers/vaultstaticsecret_controller.go @@ -141,7 +141,7 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re r.BackOffRegistry.Delete(req.NamespacedName) } - data, err := r.SecretDataBuilder.WithVaultData(resp.Data(), resp.Secret().Data, transOption) + data, err := r.SecretDataBuilder.WithVaultData(resp.Data(), resp.Secret().Data, nil, transOption) if err != nil { r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretDataBuilderError, "Failed to build K8s secret data: %s", err) @@ -500,7 +500,7 @@ func (r *VaultStaticSecretReconciler) streamStaticSecretEvents(ctx context.Conte } func (r *VaultStaticSecretReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { - r.referenceCache = newResourceReferenceCache() + r.referenceCache = NewResourceReferenceCache() if r.BackOffRegistry == nil { r.BackOffRegistry = NewBackOffRegistry() } @@ -541,9 +541,9 @@ func newKVRequest(s secretsv1beta1.VaultStaticSecretSpec) (vault.ReadRequest, er var kvReq vault.ReadRequest switch s.Type { case consts.KVSecretTypeV1: - kvReq = vault.NewKVReadRequestV1(s.Mount, s.Path) + kvReq = vault.NewKVReadRequestV1(s.Mount, s.Path, nil) case consts.KVSecretTypeV2: - kvReq = vault.NewKVReadRequestV2(s.Mount, s.Path, s.Version) + kvReq = vault.NewKVReadRequestV2(s.Mount, s.Path, s.Version, nil) default: return nil, fmt.Errorf("unsupported secret type %q", s.Type) } diff --git a/demo.mk b/demo.mk index 4ec27332e..0f29caf07 100644 --- a/demo.mk +++ b/demo.mk @@ -16,6 +16,9 @@ TF_INFRA_DEMO_DIR_VAULT ?= $(DEMO_ROOT)/infra/vault TF_VAULT_STATE_DIR ?= $(TF_INFRA_DEMO_DIR_VAULT)/state TF_APP_STATE_DIR ?= $(TF_INFRA_DEMO_ROOT)/app/state +# install VSO using Helm, otherwise use Kustomize. +WITH_HELM ?= true + include ./Makefile .PHONY: demo-setup-kind @@ -39,32 +42,61 @@ demo-infra-vault: ## Deploy Vault for the demo VAULT_ENTERPRISE=$(VAULT_ENTERPRISE) \ TF_VAULT_STATE_DIR=$(TF_VAULT_STATE_DIR) \ TF_INFRA_STATE_DIR=$(TF_VAULT_STATE_DIR) \ - K8S_CLUSTER_CONTEXT=$(K8S_CLUSTER_CONTEXT) + K8S_CLUSTER_CONTEXT=$(K8S_CLUSTER_CONTEXT) \ + VAULT_PATCH_ROOT=$(DEMO_ROOT)/infra/vault .PHONY: demo-infra-app -demo-infra-app: demo-setup-kind ## Deploy Postgres for the demo +demo-infra-app: demo-setup-kind maybe-apply-crds ## Deploy Postgres for the demo @mkdir -p $(TF_APP_STATE_DIR) rm -f $(TF_APP_STATE_DIR)/*.tf + rm -f $(TF_APP_STATE_DIR)/modules cp $(DEMO_ROOT)/infra/app/*.tf $(TF_APP_STATE_DIR)/. + ln -s ../modules $(TF_APP_STATE_DIR)/. $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) init -upgrade $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) apply -auto-approve \ -var vault_enterprise=$(VAULT_ENTERPRISE) \ -var vault_address=http://127.0.0.1:38302 \ -var vault_token=root \ -var k8s_config_context=$(K8S_CLUSTER_CONTEXT) \ + -var deploy_operator_via_helm=$(WITH_HELM) \ $(EXTRA_VARS) || exit 1 \ +.PHONY: maybe-apply-crds +maybe-apply-crds: +ifneq ($(strip $(WITH_HELM)),) + kubectl apply --recursive --filename $(CHART_CRDS_DIR) > /dev/null +endif + .PHONY: demo-infra-app-plan -demo-infra-app-plan: demo-setup-kind ## Deploy Postgres for the demo +demo-infra-app-plan: demo-setup-kind maybe-apply-crds ## Deploy Postgres for the demo @mkdir -p $(TF_APP_STATE_DIR) rm -f $(TF_APP_STATE_DIR)/*.tf cp $(DEMO_ROOT)/infra/app/*.tf $(TF_APP_STATE_DIR)/. + rm -f $(TF_APP_STATE_DIR)/modules + ln -s ../modules $(TF_APP_STATE_DIR)/. $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) init -upgrade $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) plan \ -var vault_enterprise=$(VAULT_ENTERPRISE) \ -var vault_address=http://127.0.0.1:38302 \ -var vault_token=root \ -var k8s_config_context=$(K8S_CLUSTER_CONTEXT) \ + -var deploy_operator_via_helm=$(WITH_HELM) \ + $(EXTRA_VARS) || exit 1 \ + +.PHONY: demo-infra-app-destroy +demo-infra-app-destroy: demo-setup-kind ## Destroy the application portion of the demo + @mkdir -p $(TF_APP_STATE_DIR) + rm -f $(TF_APP_STATE_DIR)/*.tf + rm -f $(TF_APP_STATE_DIR)/modules + cp $(DEMO_ROOT)/infra/app/*.tf $(TF_APP_STATE_DIR)/. + ln -s ../modules $(TF_APP_STATE_DIR)/. + $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) init -upgrade + $(TERRAFORM) -chdir=$(TF_APP_STATE_DIR) apply -destroy -auto-approve \ + -var vault_enterprise=$(VAULT_ENTERPRISE) \ + -var vault_address=http://127.0.0.1:38302 \ + -var vault_token=root \ + -var k8s_config_context=$(K8S_CLUSTER_CONTEXT) \ + -var deploy_operator_via_helm=$(WITH_HELM) \ $(EXTRA_VARS) || exit 1 \ .PHONY: demo-deploy @@ -74,4 +106,5 @@ demo-deploy: demo-setup-kind ## Deploy controller to the K8s cluster specified i KUSTOMIZATION=persistence-encrypted-test .PHONY: demo -demo: demo-deploy demo-infra-vault demo-infra-app ## Deploy the demo +#demo: demo-deploy demo-infra-vault demo-infra-app ## Deploy the demo +demo: demo-setup-kind demo-infra-vault demo-infra-app ## Deploy the demo diff --git a/demo/infra/app/app.tf b/demo/infra/app/app.tf index 9e74ab729..cc5ddcda5 100644 --- a/demo/infra/app/app.tf +++ b/demo/infra/app/app.tf @@ -3,7 +3,7 @@ data "kubernetes_namespace" "operator" { metadata { - name = var.operator_namespace + name = var.create_namespace ? kubernetes_namespace.vso[0].metadata[0].name : var.operator_namespace } } @@ -24,6 +24,8 @@ resource "kubernetes_manifest" "vault-connection-default" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_manifest" "vault-auth-default" { @@ -45,6 +47,8 @@ resource "kubernetes_manifest" "vault-auth-default" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_manifest" "vault-dynamic-secret" { @@ -91,6 +95,8 @@ resource "kubernetes_manifest" "vault-dynamic-secret" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_manifest" "templates" { @@ -166,6 +172,8 @@ EOF # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_deployment" "example" { @@ -210,8 +218,9 @@ resource "kubernetes_deployment" "example" { } } container { - image = "postgres:latest" - name = "demo" + image = "postgres:latest" + image_pull_policy = "IfNotPresent" + name = "demo" command = [ "sh", "-c", "while : ; do psql $PGURL -c 'select 1;' ; sleep 30; done" ] diff --git a/demo/infra/app/auth.tf b/demo/infra/app/auth.tf index 5a3ccbafd..230862028 100644 --- a/demo/infra/app/auth.tf +++ b/demo/infra/app/auth.tf @@ -17,18 +17,14 @@ resource "vault_kubernetes_auth_backend_config" "default" { # kubernetes auth roles resource "vault_kubernetes_auth_backend_role" "default" { - namespace = vault_auth_backend.default.namespace - backend = vault_kubernetes_auth_backend_config.default.backend - role_name = local.auth_role - bound_service_account_names = ["default"] - bound_service_account_namespaces = [ - local.k8s_namespace, - ] - token_period = 120 - token_policies = [ - vault_policy.db.name, - ] - audience = "vault" + namespace = vault_auth_backend.default.namespace + backend = vault_kubernetes_auth_backend_config.default.backend + role_name = local.auth_role + bound_service_account_names = ["default"] + bound_service_account_namespaces = local.bound_service_account_namespaces + token_period = 120 + token_policies = local.default_token_policies + audience = "vault" } # operator role used for transit encrypt/decrypt of VSO's Vault client cache resource "vault_kubernetes_auth_backend_role" "operator" { diff --git a/demo/infra/app/cross-vault-ns.tf b/demo/infra/app/cross-vault-ns.tf index 0b1d00a5b..de421fcd9 100644 --- a/demo/infra/app/cross-vault-ns.tf +++ b/demo/infra/app/cross-vault-ns.tf @@ -166,6 +166,8 @@ resource "kubernetes_manifest" "tenant-vault-auth-global" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } # VaultAuth for service account UID K8s auth role @@ -193,6 +195,8 @@ resource "kubernetes_manifest" "tenant-vault-auth-sa-uid" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } # VaultAuth for service account name K8s auth role @@ -220,6 +224,8 @@ resource "kubernetes_manifest" "tenant-vault-auth-sa-name" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_manifest" "tenant-vss-uid" { @@ -251,6 +257,8 @@ resource "kubernetes_manifest" "tenant-vss-uid" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } resource "kubernetes_manifest" "tenant-vss-name" { @@ -282,4 +290,6 @@ resource "kubernetes_manifest" "tenant-vss-name" { # force field manager conflicts to be overridden force_conflicts = true } + + depends_on = [module.vso-helm] } diff --git a/demo/infra/app/csi-app.tf b/demo/infra/app/csi-app.tf new file mode 100644 index 000000000..2266cb69f --- /dev/null +++ b/demo/infra/app/csi-app.tf @@ -0,0 +1,329 @@ +# Deploys the CSI app to the Kubernetes cluster. It will pull Vault secrets using the VSO CSI driver. +resource "kubernetes_namespace" "demo-ns-vso-csi" { + count = local.csi_enabled ? 1 : 0 + metadata { + name = "demo-ns-vso-csi" + } +} + +resource "kubernetes_service_account" "vso-csi-app" { + count = local.csi_enabled ? 1 : 0 + metadata { + name = "vso-csi-sa" + namespace = kubernetes_namespace.demo-ns-vso-csi[0].metadata[0].name + } +} + +resource "kubernetes_deployment" "vso-csi-app" { + count = local.csi_enabled ? 1 : 0 + metadata { + name = "vso-csi-app" + namespace = kubernetes_namespace.demo-ns-vso-csi[0].metadata[0].name + labels = { + "app.kubernetes.io/component" = "vso-csi-app" + } + } + spec { + replicas = 3 + selector { + match_labels = { + "app.kubernetes.io/component" = "vso-csi-app" + } + } + template { + metadata { + labels = { + "app.kubernetes.io/component" = "vso-csi-app" + } + } + spec { + share_process_namespace = true + affinity { + node_affinity { + required_during_scheduling_ignored_during_execution { + node_selector_term { + match_expressions { + key = "kubernetes.io/hostname" + operator = "In" + values = [ + "vso-demo-worker", + "vso-demo-worker2", + "vso-demo-worker3", + ] + } + } + } + } + } + service_account_name = kubernetes_service_account.vso-csi-app[0].metadata[0].name + volume { + name = "vso-csi" + csi { + read_only = true + driver = "csi.vso.hashicorp.com" + volume_attributes = { + csiSecretsNamespace = kubernetes_manifest.csi-secrets[0].manifest.metadata.namespace + csiSecretsName = kubernetes_manifest.csi-secrets[0].manifest.metadata.name + } + } + } + container { + name = "app-num-uses-10" + image = "hashicorp/vault-secrets-operator-csi-demo-app:latest" + image_pull_policy = "Never" + command = ["/demo.sh"] + volume_mount { + mount_path = "/var/run/csi-secrets" + name = "vso-csi" + } + env { + name = "SECRET_PATH" + value = "/var/run/csi-secrets" + } + env { + name = "APP_ROLE_SECRET_IDX" + value = "0" + } + env { + name = "VAULT_ADDR" + value = local.k8s_vault_connection_address + } + env { + name = "VAULT_NAMESPACE" + value = local.namespace + } + env { + name = "VAULT_APP_ROLE_BACKEND" + value = "auth/${vault_approle_auth_backend_role.csi-secrets[0].backend}" + } + env { + name = "VAULT_APP_ROLE_ROLE_NAME" + value = vault_approle_auth_backend_role.csi-secrets[0].role_name + } + env { + name = "VAULT_APP_ROLE_ROLE_ID" + value = vault_approle_auth_backend_role.csi-secrets[0].role_id + } + } + container { + name = "app-unlimited" + image = "hashicorp/vault-secrets-operator-csi-demo-app:latest" + image_pull_policy = "Never" + command = ["/demo.sh"] + volume_mount { + mount_path = "/var/run/csi-secrets" + name = "vso-csi" + } + env { + name = "SECRET_PATH" + value = "/var/run/csi-secrets" + } + env { + name = "APP_ROLE_SECRET_IDX" + value = "1" + } + env { + name = "VAULT_ADDR" + value = local.k8s_vault_connection_address + } + env { + name = "VAULT_NAMESPACE" + value = local.namespace + } + env { + name = "VAULT_APP_ROLE_BACKEND" + value = "auth/${vault_approle_auth_backend_role.csi-secrets[0].backend}" + } + env { + name = "VAULT_APP_ROLE_ROLE_NAME" + value = vault_approle_auth_backend_role.csi-secrets[0].role_name + } + env { + name = "VAULT_APP_ROLE_ROLE_ID" + value = vault_approle_auth_backend_role.csi-secrets[0].role_id + } + } + container { + name = "control" + image = "hashicorp/vault-secrets-operator-csi-demo-app:latest" + image_pull_policy = "Never" + command = ["/control.sh"] + volume_mount { + mount_path = "/var/run/csi-secrets" + name = "vso-csi" + } + env { + name = "SECRET_PATH" + value = "/var/run/csi-secrets" + } + env { + name = "APP_ROLE_SECRET_IDX" + value = "0" + } + env { + name = "VAULT_ADDR" + value = local.k8s_vault_connection_address + } + env { + name = "VAULT_NAMESPACE" + value = local.namespace + } + env { + name = "VAULT_APP_ROLE_BACKEND" + value = "auth/${vault_approle_auth_backend_role.csi-secrets[0].backend}" + } + env { + name = "VAULT_APP_ROLE_ROLE_NAME" + value = vault_approle_auth_backend_role.csi-secrets[0].role_name + } + env { + name = "VAULT_APP_ROLE_ROLE_ID" + value = vault_approle_auth_backend_role.csi-secrets[0].role_id + } + } + } + } + } +} + +resource "kubernetes_manifest" "csi-secrets" { + count = local.csi_enabled ? 1 : 0 + manifest = { + metadata = { + name = "csi-secret" + namespace = kubernetes_namespace.demo-ns-vso-csi[0].metadata[0].name + } + + apiVersion = "secrets.hashicorp.com/v1beta1" + kind = "CSISecrets" + spec = { + namespace = local.namespace + vaultAuthRef = { + name = kubernetes_manifest.vault-auth-default.manifest.metadata.name + namespace = kubernetes_manifest.vault-auth-default.manifest.metadata.namespace + } + secrets = { + vaultAppRoleSecretIDs = [ + { + mount = vault_approle_auth_backend_role.csi-secrets[0].backend + role = vault_approle_auth_backend_role.csi-secrets[0].role_name + wrapTTL = "10m" + ttl = "1h" + # This container is expected to crash after 10 uses, this is here to + # demonstrate the CSI driver's after container termination remediation capabilities. + # Set the value 0 to ensure that this container will only exit after reaching the ttl expiry. + numUses = 10 + metadata = { + "app" = "vso-csi-app" + "expect_failure_on_max_num_uses" = "true" + "wrapped" = "true" + "unlimited_uses" = "false" + } + }, + { + mount = vault_approle_auth_backend_role.csi-secrets[0].backend + role = vault_approle_auth_backend_role.csi-secrets[0].role_name + wrapTTL = "10m" + ttl = "1h" + metadata = { + "app" = "vso-csi-app" + "expect_failure_on_ttl_expiry" = "true" + "wrapped" = "true" + "unlimited_uses" = "true" + } + }, + { + mount = vault_approle_auth_backend_role.csi-secrets[0].backend + role = vault_approle_auth_backend_role.csi-secrets[0].role_name + ttl = "30m" + metadata = { + "app" = "vso-csi-app" + "not_used" = "true" + "wrapped" = "false" + } + cidrList = [ + "192.168.98.0/24" + ] + tokenBoundCIDRs = [ + "10.0.0.0/8", + "192.168.98.0/24", + ] + }, + { + mount = vault_approle_auth_backend_role.csi-secrets[0].backend + role = vault_approle_auth_backend_role.csi-secrets[0].role_name + ttl = "10m" + metadata = { + "app" = "vso-csi-app" + "not_used" = "true" + "wrapped" = "false" + } + cidrList = [ + "192.168.98.0/24" + ] + tokenBoundCIDRs = [ + "10.0.0.0/8", + "192.168.98.0/24", + ] + } + ] + } + accessControl = { + serviceAccountPattern = "^${kubernetes_service_account.vso-csi-app[0].metadata[0].name}$" + matchPolicy = "all" + namespacePatterns = [ + "^${kubernetes_namespace.demo-ns-vso-csi[0].metadata[0].name}$" + ] + podNamePatterns = [ + "^vso-csi-app-.+" + ] + podLabels = { + "app.kubernetes.io/component" = "vso-csi-app" + } + } + syncConfig = { + containerState = { + namePattern = "^app" + } + } + } + } + + field_manager { + # force field manager conflicts to be overridden + force_conflicts = true + } + + depends_on = [module.vso-helm] +} + +resource "vault_auth_backend" "csi-secrets" { + count = local.csi_enabled ? 1 : 0 + namespace = local.namespace + type = "approle" +} + +resource "vault_approle_auth_backend_role" "csi-secrets" { + count = local.csi_enabled ? 1 : 0 + namespace = local.namespace + backend = vault_auth_backend.csi-secrets[0].path + role_name = "csi-secrets" + token_policies = ["default", "dev", "prod"] +} + +resource "vault_policy" "csi-secrets" { + count = local.csi_enabled ? 1 : 0 + namespace = local.namespace + name = "${local.auth_policy}-csi-app" + policy = <&2 + exit 0 +fi + +function waitVaultPod() { + echo "waiting for the vault pod to become Ready" + local tries=0 + until [ $tries -gt 5 ] + do + kubectl wait --namespace=${K8S_VAULT_NAMESPACE} \ + --for=condition=Ready \ + --timeout=1m pod -l \ + app.kubernetes.io/name=vault &> /dev/null && return 0 + ((++tries)) + sleep .5 + done + echo "failed waiting for the vault become Ready" >&2 +} + +waitVaultPod || exit 1 + +root="${0%/*}" +pushd ${root}/patches > /dev/null +for f in *.yaml +do + type= + case "${f}" in + statefulset-*) + type=statefulset + ;; + *) + echo "unsupported patch file ${f}, skipping" >&2 + continue + ;; + esac + kubectl patch --namespace=${K8S_VAULT_NAMESPACE} ${type} vault --patch-file ${f} +done +popd > /dev/null + +kubectl delete --wait --timeout=30s --namespace=${K8S_VAULT_NAMESPACE} pod vault-0 + +waitVaultPod || exit 1 + +exit 0 diff --git a/demo/infra/vault/patches/statefulset-hostPortPatch.yaml b/demo/infra/vault/patches/statefulset-hostPortPatch.yaml new file mode 100644 index 000000000..0d5898710 --- /dev/null +++ b/demo/infra/vault/patches/statefulset-hostPortPatch.yaml @@ -0,0 +1,18 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# This patch adds a hostPort to Vault so that kind can forward traffic all +# the way from the host machine to the Vault container. The helm chart doesn't +# support this directly, and doesn't have a compelling reason to either. +# See Makefile and the setup-integration-test target for usage. +spec: + template: + spec: + nodeSelector: + kubernetes.io/hostname: vso-demo-worker + containers: + - name: vault + ports: + - name: http + hostPort: 8200 + containerPort: 8200 diff --git a/demo/kind/config.yaml b/demo/kind/config.yaml index 47681c5da..cb575ec1a 100644 --- a/demo/kind/config.yaml +++ b/demo/kind/config.yaml @@ -5,8 +5,21 @@ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane +- role: worker extraPortMappings: - containerPort: 8200 hostPort: 38302 listenAddress: "127.0.0.1" protocol: TCP +- role: worker + extraPortMappings: + - containerPort: 8200 + hostPort: 38303 + listenAddress: "127.0.0.1" + protocol: TCP +- role: worker + extraPortMappings: + - containerPort: 8200 + hostPort: 38304 + listenAddress: "127.0.0.1" + protocol: TCP diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md index 43c734fe6..d93a79c05 100644 --- a/docs/api/api-reference.md +++ b/docs/api/api-reference.md @@ -9,6 +9,8 @@ Package v1beta1 contains API Schema definitions for the secrets v1beta1 API group ### Resource Types +- [CSISecrets](#csisecrets) +- [CSISecretsList](#csisecretslist) - [HCPAuth](#hcpauth) - [HCPAuthList](#hcpauthlist) - [HCPVaultSecretsApp](#hcpvaultsecretsapp) @@ -30,6 +32,121 @@ Package v1beta1 contains API Schema definitions for the secrets v1beta1 API grou +#### AccessControl + + + +AccessControl provides configuration for controlling access to the secret. +It allows specifying the namespaces, service account, pod names, and pod +labels that should be allowed to access the secret. + + + +_Appears in:_ +- [CSISecretsSpec](#csisecretsspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceAccountPattern` _string_ | ServiceAccountPattern is the name of the service account that should be used to
access the secret. It can be specified as a regex pattern.
A valid service account is always required. | | | +| `namespacePatterns` _string array_ | NamespacePatterns is a list of namespace name regex patterns that are allowed access. | | | +| `podNamePatterns` _string array_ | PodNamePatterns is a list of pod name regex patterns that should be allowed access. | | | +| `podLabels` _object (keys:string, values:string)_ | PodLabels is a map of pod label key-value pairs that should be allowed access. | | | +| `matchPolicy` _string_ | MatchPolicy is the policy to use when matching the access control rules. If
set to "any", only one of the rules should match. If set to "all", all the
rules should match. | all | Enum: [any all]
| + + +#### CSISecrets + + + +CSISecrets is the Schema for the csisecrets API + + + +_Appears in:_ +- [CSISecretsList](#csisecretslist) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `secrets.hashicorp.com/v1beta1` | | | +| `kind` _string_ | `CSISecrets` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[CSISecretsSpec](#csisecretsspec)_ | | | | + + +#### CSISecretsList + + + +CSISecretsList contains a list of CSISecrets + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `secrets.hashicorp.com/v1beta1` | | | +| `kind` _string_ | `CSISecretsList` | | | +| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `items` _[CSISecrets](#csisecrets) array_ | | | | + + +#### CSISecretsSpec + + + +CSISecretsSpec defines the desired state of CSISecrets. It contains the +configuration for the CSI driver to populate the secret data. + + + +_Appears in:_ +- [CSISecrets](#csisecrets) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `namespace` _string_ | Namespace is the Vault namespace where the secret is located. | | | +| `accessControl` _[AccessControl](#accesscontrol)_ | AccessControl provides configuration for controlling access to the secret. | | | +| `secrets` _[SecretCollection](#secretcollection)_ | Secrets that will be synced with the CSI driver. | | | +| `syncConfig` _[CSISyncConfig](#csisyncconfig)_ | SyncConfig provides configuration for syncing the secret data with the CSI driver. | | | +| `vaultAuthRef` _[VaultAuthRef](#vaultauthref)_ | VaultAuthRef is the reference to the VaultAuth resource. | | | + + + + +#### CSISyncConfig + + + + + + + +_Appears in:_ +- [CSISecretsSpec](#csisecretsspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `containerState` _[ContainerState](#containerstate)_ | ContainerState is the state of the container that the CSI driver always sync
on. This configuration is useful to sync when the last state of the container
is in the terminated state and the restart count is greater than 0. | | | + + +#### ContainerState + + + + + + + +_Appears in:_ +- [CSISyncConfig](#csisyncconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `namePattern` _string_ | NamePattern of the container. Can be expressed as a regular expression. | | | +| `imagePattern` _string_ | ImagePattern of the container. Can be expressed as a regular expression. | | | + + #### Destination @@ -291,6 +408,24 @@ _Appears in:_ | `name` _string_ | Name of the resource | | | +#### SecretCollection + + + + + + + +_Appears in:_ +- [CSISecretsSpec](#csisecretsspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `transformation` _[Transformation](#transformation)_ | Transformation provides configuration for transforming the secret data before
it is stored in the CSI volume. | | | +| `vaultAppRoleSecretIDs` _[VaultAppRoleSecretID](#vaultapprolesecretid) array_ | VaultAppRoleSecretIDs is a list of AppRole secret IDs to be used to populate the secret. | | | +| `vaultStaticSecrets` _[VaultStaticSecretCollectable](#vaultstaticsecretcollectable) array_ | VaultStaticSecrets is a list of static secrets to be synced by the CSI driver. | | | + + #### SecretTransformation @@ -341,10 +476,10 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `templates` _object (keys:string, values:[Template](#template))_ | Templates maps a template name to its Template. Templates are always included
in the rendered K8s Secret with the specified key. | | | -| `sourceTemplates` _[SourceTemplate](#sourcetemplate) array_ | SourceTemplates are never included in the rendered K8s Secret, they can be
used to provide common template definitions, etc. | | | -| `includes` _string array_ | Includes contains regex patterns used to filter top-level source secret data
fields for inclusion in the final K8s Secret data. These pattern filters are
never applied to templated fields as defined in Templates. They are always
applied last. | | | -| `excludes` _string array_ | Excludes contains regex patterns used to filter top-level source secret data
fields for exclusion from the final K8s Secret data. These pattern filters are
never applied to templated fields as defined in Templates. They are always
applied before any inclusion patterns. To exclude all source secret data
fields, you can configure the single pattern ".*". | | | +| `templates` _object (keys:string, values:[Template](#template))_ | Templates maps a template name to its Template. Templates are always included
in the rendered secret with the specified key. | | | +| `sourceTemplates` _[SourceTemplate](#sourcetemplate) array_ | SourceTemplates are never included in the rendered secret, they can be
used to provide common template definitions, etc. | | | +| `includes` _string array_ | Includes contains regex patterns used to filter top-level source secret data
fields for inclusion in the final secret data. These pattern filters are
never applied to templated fields as defined in Templates. They are always
applied last. | | | +| `excludes` _string array_ | Excludes contains regex patterns used to filter top-level source secret data
fields for exclusion from the final secret data. These pattern filters are
never applied to templated fields as defined in Templates. They are always
applied before any inclusion patterns. To exclude all source secret data
fields, you can configure the single pattern ".*". | | | @@ -446,6 +581,9 @@ _Appears in:_ _Appears in:_ - [Destination](#destination) +- [SecretCollection](#secretcollection) +- [VaultAppRoleSecretID](#vaultapprolesecretid) +- [VaultStaticSecretCollectable](#vaultstaticsecretcollectable) | Field | Description | Default | Validation | | --- | --- | --- | --- | @@ -478,6 +616,31 @@ _Appears in:_ | `ignoreExcludes` _boolean_ | IgnoreExcludes controls whether to use the SecretTransformation's Excludes
data key filters. | | | +#### VaultAppRoleSecretID + + + +VaultAppRoleSecretID defines the AppRole secret ID to be used to populate the secret. + + + +_Appears in:_ +- [SecretCollection](#secretcollection) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `mount` _string_ | Mount path to the AppRole auth engine. | | | +| `role` _string_ | Role is the name of the AppRole. | | | +| `metadata` _object (keys:string, values:string)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `cidrList` _string array_ | CIDRList is the list of CIDR blocks that access the secret ID. | | | +| `tokenBoundCIDRs` _string array_ | TokenBoundCIDRs is the list of CIDR blocks that can be used to authenticate
using tokens generated by this secret ID. | | | +| `ttl` _string_ | TTL is the TTL for the secret ID, after which it becomes invalid. | | Pattern: `^([0-9]+(\.[0-9]+)?(s|m|h))$`
| +| `numUses` _integer_ | NumUses is the number of times the secret ID can be used. | | | +| `wrapTTL` _string_ | WrapTTL is the TTL for the wrapped secret ID. | | Pattern: `^([0-9]+(\.[0-9]+)?(s|m|h))$`
| +| `syncRoleID` _boolean_ | SyncRoleID is the flag to fetch the role ID from the AppRole auth engine.
Requires that the provisioning VaultAuth has the necessary permissions to fetch the role ID. | | | +| `transformation` _[Transformation](#transformation)_ | Transformation provides configuration for transforming the secret data before
it is stored in the CSI volume. | | | + + #### VaultAuth @@ -780,7 +943,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the VaultAuthGlobal resource. | | Pattern: `^([a-z0-9.-]{1,253})$`
| -| `namespace` _string_ | Namespace of the VaultAuthGlobal resource. If not provided, the namespace of
the referring VaultAuth resource is used. | | Pattern: `^([a-z0-9.-]{1,253})$`
| +| `namespace` _string_ | Namespace of the VaultAuthGlobal resource. If not provided, the namespace of
the referring VaultAuth resource is used. | | Pattern: `^([a-z0-9-]{1,63})$`
| | `mergeStrategy` _[MergeStrategy](#mergestrategy)_ | MergeStrategy configures the merge strategy for HTTP headers and parameters
that are included in all Vault authentication requests. | | | | `allowDefault` _boolean_ | AllowDefault when set to true will use the default VaultAuthGlobal resource
as the default if Name is not set. The 'allow-default-globals' option must be
set on the operator's '-global-vault-auth-options' flag

The default VaultAuthGlobal search is conditional.
When a ref Namespace is set, the search for the default
VaultAuthGlobal resource is constrained to that namespace.
Otherwise, the search order is:
1. The default VaultAuthGlobal resource in the referring VaultAuth resource's
namespace.
2. The default VaultAuthGlobal resource in the Operator's namespace. | | | @@ -832,6 +995,24 @@ VaultAuthList contains a list of VaultAuth | `items` _[VaultAuth](#vaultauth) array_ | | | | +#### VaultAuthRef + + + + + + + +_Appears in:_ +- [CSISecretsSpec](#csisecretsspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name of the VaultAuth resource. | | | +| `namespace` _string_ | Namespace of the VaultAuth resource. | | | +| `trustNamespace` _boolean_ | TrustNamespace of the referring VaultAuth resource. This means that any Vault
credentials will be provided by resources in the same namespace as the
VaultAuth resource. Otherwise, the credentials will be provided by the secret
resource's namespace. | | | + + #### VaultAuthSpec @@ -1139,6 +1320,46 @@ _Appears in:_ | `spec` _[VaultStaticSecretSpec](#vaultstaticsecretspec)_ | | | | +#### VaultStaticSecretCollectable + + + + + + + +_Appears in:_ +- [SecretCollection](#secretcollection) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `mount` _string_ | Mount for the secret in Vault | | | +| `path` _string_ | Path of the secret in Vault, corresponds to the `path` parameter for:
kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret
kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version | | | +| `version` _integer_ | Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter:
https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version | | Minimum: 0
| +| `type` _string_ | Type of the Vault static secret | | Enum: [kv-v1 kv-v2]
| +| `transformation` _[Transformation](#transformation)_ | Transformation provides configuration for transforming the secret data before
it is stored in the CSI volume. | | | + + +#### VaultStaticSecretCommon + + + + + + + +_Appears in:_ +- [VaultStaticSecretCollectable](#vaultstaticsecretcollectable) +- [VaultStaticSecretSpec](#vaultstaticsecretspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `mount` _string_ | Mount for the secret in Vault | | | +| `path` _string_ | Path of the secret in Vault, corresponds to the `path` parameter for:
kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret
kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version | | | +| `version` _integer_ | Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter:
https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version | | Minimum: 0
| +| `type` _string_ | Type of the Vault static secret | | Enum: [kv-v1 kv-v2]
| + + #### VaultStaticSecretList @@ -1172,15 +1393,15 @@ _Appears in:_ | --- | --- | --- | --- | | `vaultAuthRef` _string_ | VaultAuthRef to the VaultAuth resource, can be prefixed with a namespace,
eg: `namespaceA/vaultAuthRefB`. If no namespace prefix is provided it will default to the
namespace of the VaultAuth CR. If no value is specified for VaultAuthRef the Operator will
default to the `default` VaultAuth, configured in the operator's namespace. | | | | `namespace` _string_ | Namespace of the secrets engine mount in Vault. If not set, the namespace that's
part of VaultAuth resource will be inferred. | | | -| `mount` _string_ | Mount for the secret in Vault | | | -| `path` _string_ | Path of the secret in Vault, corresponds to the `path` parameter for,
kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret
kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version | | | -| `version` _integer_ | Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter:
https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version | | Minimum: 0
| -| `type` _string_ | Type of the Vault static secret | | Enum: [kv-v1 kv-v2]
| | `refreshAfter` _string_ | RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h | | Pattern: `^([0-9]+(\.[0-9]+)?(s|m|h))$`
Type: string
| | `hmacSecretData` _boolean_ | HMACSecretData determines whether the Operator computes the
HMAC of the Secret's data. The MAC value will be stored in
the resource's Status.SecretMac field, and will be used for drift detection
and during incoming Vault secret comparison.
Enabling this feature is recommended to ensure that Secret's data stays consistent with Vault. | true | | | `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.
All configured targets will be ignored if HMACSecretData is set to false.
See RolloutRestartTarget for more details. | | | | `destination` _[Destination](#destination)_ | Destination provides configuration necessary for syncing the Vault secret to Kubernetes. | | | | `syncConfig` _[SyncConfig](#syncconfig)_ | SyncConfig configures sync behavior from Vault to VSO | | | +| `mount` _string_ | Mount for the secret in Vault | | | +| `path` _string_ | Path of the secret in Vault, corresponds to the `path` parameter for:
kv-v1: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1#read-secret
kv-v2: https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-version | | | +| `version` _integer_ | Version of the secret to fetch. Only valid for type kv-v2. Corresponds to version query parameter:
https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#version | | Minimum: 0
| +| `type` _string_ | Type of the Vault static secret | | Enum: [kv-v1 kv-v2]
| diff --git a/docs/diags/csi-trusted-orchestrator.puml b/docs/diags/csi-trusted-orchestrator.puml new file mode 100644 index 000000000..4a13d0ac1 --- /dev/null +++ b/docs/diags/csi-trusted-orchestrator.puml @@ -0,0 +1,210 @@ +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver Node Volume Publishing +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> NodePublishVolumeRequest: Receives node volume publish request + NodePublishVolumeRequest -> NodeServer: Fetches the configured CSISecret for the request and validates the request + NodeServer -> NodeServer: Authorizes the request for volume publishing + NodeServer -> Vault: Authenticates to Vault + Vault -> NodeServer: Returns a token + NodeServer -> Vault: Requests secret data for the CSISecret + Vault -> NodeServer: Returns the secret data + NodeServer -> NodeServer: Transforms the secret data into a volume payload + NodeServer -> TargetDirectory: Writes payload to the ephemeral volume holding the secret + TargetDirectory -> NodeServer: Successful write + NodeServer -> NodeServer: Stores the SyncStatus to the target directory for later reconciliation + NodeServer -> CSI: Returns a successful NodeVolumePublishResponse +else error + NodeServer -> CSI: Returns an error NodeVolumePublishResponse on unauthorized request or failed volume publishing +end +@enduml + +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver Request authorization (policy empty) +alt unauthorized + CSI -> NodeServer: Request node volume publishing + CSISecretAccessControl -> NodeServer: No policy configured + NodeServer -> CSI: Returns an error NodeVolumePublishResponse on unauthorized request if any of the checks fail +end +@enduml + +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver Request authorization (policy all) +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> CSISecret: Fetches the configured CSISecret for the request and reads the AccessControl +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> CSISecret: Fetches the configured CSISecret for the request and reads the AccessControl +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Namespace patterns +else error + NodeServer -> CSI: return unauthorized +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Pod name patterns +else error + NodeServer -> CSI: return unauthorized +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Pod labels +else error + NodeServer -> CSI: return unauthorized +else check + NodeServer -> CSISecretAccessControl: Checks K8s ServiceAccount name patterns +else error + NodeServer -> CSI: return unauthorized +else + NodeServer -> CSI: Returns an successful NodeVolumePublishResponse +end +@enduml + + +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver Request authorization (policy any) +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> CSISecret: Fetches the configured CSISecret for the request and reads the AccessControl +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> CSISecret: Fetches the configured CSISecret for the request and reads the AccessControl +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Namespace patterns +else error + CSISecretAccessControl -> NodeServer: continue to next check +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Pod name patterns +else error + CSISecretAccessControl -> NodeServer: continue to next check +else check + NodeServer -> CSISecretAccessControl: Checks matching K8s Pod labels +else error + CSISecretAccessControl -> NodeServer: continue to next check +else check + NodeServer -> CSISecretAccessControl: Checks K8s ServiceAccount name patterns +else error + CSISecretAccessControl -> NodeServer: error + NodeServer -> CSI: return unauthorized +else + NodeServer -> CSI: Returns an successful NodeVolumePublishResponse +end +@enduml + +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver Request authorization (policy all) +alt publish + CSI -> NodeServer : Request node volume publishing + NodeServer -> CSISecret: Fetches the configured CSISecret for the request and reads the AccessControl +else check + CSISecretAccessControl -> NodeServer: Checks matching K8s Namespace patterns OR unauthorized +else error + NodeServer -> CSI: return unauthorized +else check + CSISecretAccessControl -> NodeServer: Checks matching K8s Pod name patterns or unauthorized +else error + NodeServer -> CSI: return unauthorized +else check + CSISecretAccessControl -> NodeServer: Checks matching K8s Pod labels OR unauthorized +else check + CSISecretAccessControl -> NodeServer: Checks K8s ServiceAccount name patterns OR unauthorized +else error + NodeServer -> CSI: return unauthorized +else + NodeServer -> CSI: Returns an successful NodeVolumePublishResponse +end +@enduml + +@startuml +!theme C4_sandstone from https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/themes +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Sequence.puml + +autonumber +title VSO CSI Driver PodReconciler +ControllerRuntime -> PodReconciler: Reconcile Pods for CSI volume publishing +alt reconcile + PodReconciler -> K8sAPI: Get K8s Pod + PodReconciler -> Pod: Check if Volume requires republishing based on CSISecret configuration + PodReconciler -> NodeServer: Request node volume publishing +else error + PodReconciler -> ControllerRuntime: Requeue Pod on reconciliation error (repeat) +else republish + PodReconciler -> NodeServer: Request node volume publishing +else error + PodReconciler -> ControllerRuntime: Requeue Pod on reconciliation error (repeat) +else republish + NodeServer -> NodeServer: Publishes the volume +else error + PodReconciler -> ControllerRuntime: Requeue Pod on reconciliation error (repeat) +else republish + PodReconciler -> ControllerRuntime: Reconciliation complete +end +@enduml + + +@startuml +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Dynamic.puml +!include sprites/secret-128-scaled.puml +!include sprites/pod-128-scaled.puml +!include sprites/vol-128-scaled.puml +!include sprites/HashiCorp_Logomark_White_RGB-scaled.puml + +UpdateElementStyle("container", $shadowing="true") +UpdateElementStyle("person", $shadowing="true") +AddRelTag("deployer_sp", $lineColor="red", $lineStyle=BoldLine(), $lineThickness=3) +AddRelTag("deployer_app", $lineColor="#118bda", $lineStyle=BoldLine(), $lineThickness=3) +AddRelTag("vso", $lineColor="#d33716", $lineStyle=BoldLine(), $lineThickness=3) +AddElementTag("vso", $bgColor="#444444") +AddElementTag("hvs", $bgColor="#444444") + +LAYOUT_TOP_DOWN() +HIDE_STEREOTYPE() +UpdateBoundaryStyle($elementName="container", $type="k8s") + +Person(user, Deployer/Agent, $sprite="person") + +System_Boundary(c1, "Vault") { + Container(app, "Vault Secret", "", "Token", $sprite="HashiCorp_Logomark_White_RGB") +} + +System_Boundary(c2, "Kubernetes Cluster", "") { + Container_Boundary(vso_ns, "nodes") { + Container_Boundary(vso_ns, "vso-namespace") { + Container(csi, "Vault CSI Driver", "Pod", "Provisions ephemeral Volume mounts to Pods", $sprite="HashiCorp_Logomark_White_RGB", $tags="csi") + } + Container_Boundary(vso_ns, "vso-namespace") { + Container(vso, "Vault Secrets Operator", "Pod", "Secrets Lifecycle Agent, provides Vault auth support", $sprite="HashiCorp_Logomark_White_RGB", $tags="ns+vso") + } + Container_Boundary(app_ns, "App Namespace", $tags="namespace") { + Container_Boundary(c6, "App Deployment", $tags="namespace") { + Container(pod1, "app1", "Pod", "", $sprite="pod") + } + Container(app_secret, "ephemeral volume holding the secret", "Vault token", "", $sprite="vol") + } + } +} + +Rel_R(user, app, "Configure auth engine", "HTTPS v1/auth/kubernetes", $tags="deployer_app", $index=Index()) +Rel_R(user, app_ns, "Apply CSISecret, and VaultToken YAML manifest", "HTTPS k8s/api", $tags="deployer_app", $index=LastIndex()-1) +Rel_U(csi, app, "Authenticate to Vault and request token", "HTTPS auth/token/create", $tags="vso", $index=Index()) +Rel_D(csi, app_secret, "Provision Vault secret data", "HTTPS k8s/api", $tags="deployer_sp", $index=LastIndex()-1) +Rel_L(pod1, app_secret, "Read secret Data from mounted volume", $tags="deployer_sp", $index=LastIndex()-2) +Rel_L(csi, vso, "Provides CRDs", $tags="csi", $index=Index()-1) +@enduml diff --git a/docs/diags/sprites/vol-128-scaled.puml b/docs/diags/sprites/vol-128-scaled.puml new file mode 100644 index 000000000..c93e51937 --- /dev/null +++ b/docs/diags/sprites/vol-128-scaled.puml @@ -0,0 +1,33 @@ +sprite $vol [64x62/8] { +__________________________uu0000000uuu__________________________ +______________________uu00024KaaaSC42000uu______________________ +__________________uu00024KaaaaaaaaaaaaK42000uu__________________ +______________uu00034KaaaaaaaaaaaaaaaaaaaaK43000uu______________ +__________uu0013CSaaaaaaaaaaaaaaaaaaaaaaaaaaaaSC3100uu__________ +______uu0013CSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaSC3100uu______ +_____001CSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaSC00u_____ +_____00SaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaK00_____ +____00Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa100____ +____00SaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaJ00____ +___001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10u___ +___00KaaaaaaaaaaaaZXWWOG8880000000000888GOWWXZaaaaaaaaaaaaJ00___ +__u01aaaaaaaaaaaO000000000000000000000000000008Oaaaaaaaaaaa10u__ +__00JaaaaaaaaaaaH320000000000000000000000000123HaaaaaaaaaaaB00__ +_u01aaaaaaaaaaaa008GOOH9A23333333333332A9GOOG800aaaaaaaaaaaa00u_ +_00Jaaaaaaaaaaaa00000000000000000000000000000000aaaaaaaaaaaaA00_ +u00aaaaaaaaaaaaa00000000000000000000000000000000aaaaaaaaaaaaS00_ +00Aaaaaaaaaaaaaa00000000000000000000000000000000aaaaaaaaaaaaaA00 +00Saaaaaaaaaaaaa30000000000000000000000000000003aaaaaaaaaaaaaS00 +00aaaaaaaaaaaaaaaaSC443211100000000001112344CSaaaaaaaaaaaaaaaZ00 +008YaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaY800 +_700OaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZO007_ +___008XaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX007___ +____700GZaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZG007____ +______700XaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaO007______ +_______7008YaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaY800________ +_________700OZaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZO007_________ +___________000XaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX007___________ +____________700GZaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZG007____________ +______________700OZaaaaaaaaaaaaaaaaaaaaaaaaaaZO007______________ +_______________700000000000000000000000000000007________________ +} diff --git a/docs/threat-model/README.md b/docs/threat-model/README.md index 25aaaba47..abdaa2bea 100644 --- a/docs/threat-model/README.md +++ b/docs/threat-model/README.md @@ -14,6 +14,7 @@ The Operator occupies a privileged position in a Kubernetes cluster, with unencr * Encrypt the Kubernetes etcd database at rest using a KMS provider. Kubernetes Secrets stored in etcd are [not encrypted at rest by default](https://kubernetes.io/docs/concepts/security/secrets-good-practices/). * Use TLS negotiated by a well-secured certificate authority for all networked communication, especially for Vault and the Kubernetes API. * Update the Operator, Vault, and other systems regularly to guard against known vulnerabilities. +* If using the Vault Secrets Operator CSI Driver, see the dedicated section below for details. ## Terminology @@ -341,6 +342,7 @@ While none of this should be considered secret information, it can be sensitive + ### Threats specific to Kubernetes and Kubernetes Secrets These threats are included because using Kubernetes Secrets is a fundamental requirement to using the Operator, but their applicability is not affected by whether or not the Secrets are maintained by the Operator. All usages of Kubernetes Secrets should consider the following in their threat model. @@ -445,6 +447,56 @@ When etcd is encrypted by a KMS provider all objects are encrypted on disk, but +### Threats specific to the Vault Secrets Operator CSI driver + +The Vault Secrets Operator's Helm chart can also be used to deploy the Vault Secrets Operator CSI driver. This is an alternate approach to delivering Vault secrets to application Pods, in which a CSI driver Pod is deployed on each node in the Kubernetes cluster, and can mount ephemeral volumes containing Vault secrets directly to Pod containers. + +Since the Vault Secrets Operator CSI driver does not make use of persistent storage e.g. Kubernetes Secrets, some of the concerns in the above section can be avoided, but other security aspects should be considered. + + + + + + + + + + + + + + + + +
+ID + Threat + Categories + Description + Mitigation +
12 + An attacker with the ability to deploy a Pod onto the node may be able to access secret data if the accessControl permissions on the CSISecrets custom resource's spec are too broad. + Information disclosure + Regular expression pattern matching for Pod name, container name, namespace name, and service account name are used to restrict which Pods are allowed to mount the Vault secrets declared in a CSISecrets resource. + If an attacker is able to deploy Pods onto the node that match the provided pattern, those Pods could gain access to the sensitive data written to the volume. + +
    + +
  • +Write regular expressions carefully and granularly to avoid giving permissions too broadly. +
  • +
  • +Lock down permissions around the Kubernetes cluster in such a way that unauthorized users cannot deploy to it. +
  • +
  • +Prefer a matchPolicy of "all" over "any", and include a granular service account pattern, rather than pod name being the only requirement to match on. +
  • +
  • +Limit the number of Vault secrets that you give access to in an individual CSISecrets resource. +
  • +
+
+ ## References * [Vault Security Model](https://developer.hashicorp.com/vault/docs/internals/security) diff --git a/go.mod b/go.mod index 6c0ade585..20ad204c8 100644 --- a/go.mod +++ b/go.mod @@ -3,39 +3,39 @@ module github.com/hashicorp/vault-secrets-operator go 1.24.3 require ( - cloud.google.com/go/compute/metadata v0.8.0 + cloud.google.com/go/compute/metadata v0.6.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/argoproj/argo-rollouts v1.6.6 github.com/cenkalti/backoff/v4 v4.3.0 - github.com/go-logr/logr v1.4.3 + github.com/go-logr/logr v1.4.2 github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/strfmt v0.23.0 github.com/google/uuid v1.6.0 - github.com/gruntwork-io/terratest v0.50.0 + github.com/gruntwork-io/terratest v0.49.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/hcp-sdk-go v0.118.0 - github.com/hashicorp/vault/api v1.13.0 - github.com/hashicorp/vault/sdk v0.13.0 + github.com/hashicorp/vault/api v1.20.1-0.20250822193320-eff87a134a94 + github.com/hashicorp/vault/sdk v0.18.1-0.20250822193320-eff87a134a94 github.com/kelseyhightower/envconfig v1.4.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.38.0 - github.com/prometheus/client_golang v1.23.0 + github.com/onsi/gomega v1.37.0 + github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.41.0 - google.golang.org/api v0.247.0 + golang.org/x/crypto v0.40.0 + google.golang.org/api v0.232.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.4 - k8s.io/apiextensions-apiserver v0.33.4 - k8s.io/apimachinery v0.33.4 - k8s.io/client-go v0.33.4 + k8s.io/api v0.33.0 + k8s.io/apiextensions-apiserver v0.33.0 + k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 - sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/yaml v1.6.0 + sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -78,7 +78,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect github.com/aws/smithy-go v1.22.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/hashicorp/go-getter/v2 v2.2.3 // indirect @@ -88,35 +87,33 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/tools v0.34.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect ) require ( - cloud.google.com/go/auth v0.16.4 // indirect + cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.44.122 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/analysis v0.23.0 // indirect @@ -133,18 +130,18 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gruntwork-io/go-commons v0.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.6 // indirect - github.com/hashicorp/hcl v1.0.1-vault-5 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/hashicorp/terraform-json v0.23.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -154,7 +151,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect @@ -176,8 +173,8 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/otp v1.4.0 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -190,23 +187,23 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/zclconf/go-cty v1.15.0 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/grpc v1.74.2 // indirect - google.golang.org/protobuf v1.36.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index bd4c11f69..adef74db9 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= -cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -12,8 +12,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -106,12 +106,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -132,8 +128,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -147,11 +143,11 @@ github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXE github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -183,8 +179,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -209,22 +205,22 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= -github.com/gruntwork-io/terratest v0.50.0 h1:AbBJ7IRCpLZ9H4HBrjeoWESITv8nLjN6/f1riMNcAsw= -github.com/gruntwork-io/terratest v0.50.0/go.mod h1:see0lbKvAqz6rvzvN2wyfuFQQG4PWcAb2yHulF6B2q4= +github.com/gruntwork-io/terratest v0.49.0 h1:GurfpHEOEr8vntB77QcxDh+P7aiQRUgPFdgb6q9PuWI= +github.com/gruntwork-io/terratest v0.49.0/go.mod h1:/+dfGio9NqUpvvukuPo29B8zy6U5FYJn9PdmvwztK4A= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -237,8 +233,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= @@ -249,24 +245,24 @@ github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRct github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= -github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= -github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/hcp-sdk-go v0.118.0 h1:ttW1uAV2luG7saSyvn//tTPgy93GqSWn7bRGScEuEwA= github.com/hashicorp/hcp-sdk-go v0.118.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= -github.com/hashicorp/vault/api v1.13.0 h1:RTCGpE2Rgkn9jyPcFlc7YmNocomda44k5ck8FKMH41Y= -github.com/hashicorp/vault/api v1.13.0/go.mod h1:0cb/uZUv1w2cVu9DIvuW1SMlXXC6qtATJt+LXJRx+kg= -github.com/hashicorp/vault/sdk v0.13.0 h1:UmcLF+7r70gy1igU44Suflgio30P2GOL4MkHPhJuiP8= -github.com/hashicorp/vault/sdk v0.13.0/go.mod h1:LxhNTWRG99mXg9xijBCnCnIus+brLC5uFsQUQ4zgOnU= +github.com/hashicorp/vault/api v1.20.1-0.20250822193320-eff87a134a94 h1:jFcpmAsw5oxr0EW3M62fspXn1A7dAY7aipzho2dOyLI= +github.com/hashicorp/vault/api v1.20.1-0.20250822193320-eff87a134a94/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/sdk v0.18.1-0.20250822193320-eff87a134a94 h1://DrPOOh0PWMUfsiarEHuVgynM+xdKHRXDghODF4viQ= +github.com/hashicorp/vault/sdk v0.18.1-0.20250822193320-eff87a134a94/go.mod h1:kZ7GBPZHR8JEh6Pzv76UphKe0lQbaHLtLMxYLOyrQnk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -311,13 +307,12 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= @@ -357,12 +352,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= -github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -374,17 +369,17 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -440,39 +435,33 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -481,8 +470,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -510,18 +499,17 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -529,32 +517,31 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= -google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/api v0.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I= +google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= +google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjNe+P2XiTmPsLawi/pCbSPfxt6lTfw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -576,14 +563,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= -k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= -k8s.io/apiextensions-apiserver v0.33.4 h1:rtq5SeXiDbXmSwxsF0MLe2Mtv3SwprA6wp+5qh/CrOU= -k8s.io/apiextensions-apiserver v0.33.4/go.mod h1:mWXcZQkQV1GQyxeIjYApuqsn/081hhXPZwZ2URuJeSs= -k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= -k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= -k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= @@ -592,8 +579,8 @@ k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6J k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= @@ -601,6 +588,5 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/helm-reference-gen/doc_node.go b/hack/helm-reference-gen/doc_node.go index eb8fca595..04d77fddd 100644 --- a/hack/helm-reference-gen/doc_node.go +++ b/hack/helm-reference-gen/doc_node.go @@ -168,6 +168,8 @@ func (n DocNode) FormattedKind() string { // will show it as that is handled above via the typeAnnotation regex // match. return "" + case "seq": + return "array" default: return fmt.Sprintf("%s '%v'", UnknownKindError, n.KindTag) } diff --git a/helpers/secrets.go b/helpers/secrets.go index b578bb0ba..d6de22fca 100644 --- a/helpers/secrets.go +++ b/helpers/secrets.go @@ -490,7 +490,7 @@ func DeleteSecret(ctx context.Context, client ctrlclient.Client, objKey client.O type SecretDataBuilder struct{} // WithVaultData returns the K8s Secret data from a Vault Secret data. -func (s *SecretDataBuilder) WithVaultData(d, secretData map[string]any, opt *SecretTransformationOption) (map[string][]byte, error) { +func (s *SecretDataBuilder) WithVaultData(d, secretData, wrapData map[string]any, opt *SecretTransformationOption) (map[string][]byte, error) { if opt == nil { opt = &SecretTransformationOption{} } @@ -507,7 +507,7 @@ func (s *SecretDataBuilder) WithVaultData(d, secretData map[string]any, opt *Sec metadata = make(map[string]any) } - input := NewSecretInput(d, metadata, opt.Annotations, opt.Labels) + input := NewSecretInput(d, wrapData, metadata, opt.Annotations, opt.Labels) data, err = renderTemplates(opt, input) if err != nil { return nil, err @@ -608,7 +608,7 @@ func (s *SecretDataBuilder) WithHVSAppSecrets(resp *hvsclient.OpenAppSecretsOK, } if hasTemplates { - data, err = renderTemplates(opt, NewSecretInput(secrets, metadata, opt.Annotations, opt.Labels)) + data, err = renderTemplates(opt, NewSecretInput(secrets, nil, metadata, opt.Annotations, opt.Labels)) if err != nil { return nil, err } diff --git a/helpers/secrets_test.go b/helpers/secrets_test.go index 2462f39fe..3b1b91fbc 100644 --- a/helpers/secrets_test.go +++ b/helpers/secrets_test.go @@ -570,12 +570,13 @@ func TestSecretDataBuilder_WithVaultData(t *testing.T) { t.Parallel() tests := []struct { - name string - data map[string]interface{} - opt *SecretTransformationOption - raw map[string]interface{} - want map[string][]byte - wantErr assert.ErrorAssertionFunc + name string + data map[string]interface{} + wrapData map[string]interface{} + opt *SecretTransformationOption + raw map[string]interface{} + want map[string][]byte + wantErr assert.ErrorAssertionFunc }{ { name: "equal-raw-data", @@ -898,6 +899,57 @@ META_QUX=biff }, wantErr: assert.NoError, }, + { + name: "tmpl-mixed-with-wrapData", + opt: &SecretTransformationOption{ + KeyedTemplates: []*KeyedTemplate{ + { + Key: "buz", + Template: secretsv1beta1.Template{ + Name: "tmpl1", + Text: `{{- range $key, $value := .Secrets }} +{{- printf "%s=%v\n" $key $value -}} +{{- end }}`, + }, + }, + { + Key: "wrapData", + Template: secretsv1beta1.Template{ + Name: "tmpl2", + Text: `{{- range $key, $value := .WrapData }} +{{- printf "%s=%v\n" $key $value -}} +{{- end }}`, + }, + }, + }, + }, + data: map[string]interface{}{ + "baz": "qux", + "foo": "biff", + "buz": 1, + }, + wrapData: map[string]any{ + "token": "1234567890abcdef", + "accessor": "some-accessor", + }, + raw: map[string]interface{}{ + "baz": "qux", + "foo": "biff", + "buz": 1, + }, + want: map[string][]byte{ + "buz": []byte("baz=qux\nbuz=1\nfoo=biff\n"), + "foo": []byte("biff"), + "baz": []byte("qux"), + "wrapData": []byte("accessor=some-accessor\ntoken=1234567890abcdef\n"), + SecretDataKeyRaw: marshalRaw(t, map[string]any{ + "baz": "qux", + "foo": "biff", + "buz": 1, + }), + }, + wantErr: assert.NoError, + }, { name: "tmpl-filter-includes-mixed", opt: &SecretTransformationOption{ @@ -1262,7 +1314,7 @@ META_QUX=biff tt := tt t.Parallel() s := &SecretDataBuilder{} - got, err := s.WithVaultData(tt.data, tt.raw, tt.opt) + got, err := s.WithVaultData(tt.data, tt.raw, tt.wrapData, tt.opt) if !tt.wantErr(t, err, fmt.Sprintf("WithVaultData(%v, %v)", tt.data, tt.raw)) { return } diff --git a/helpers/template.go b/helpers/template.go index a40047161..e7afabcba 100644 --- a/helpers/template.go +++ b/helpers/template.go @@ -123,30 +123,36 @@ type GlobalTransformationOptions struct { } func NewSecretTransformationOption(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Object, globalOpt *GlobalTransformationOptions) (*SecretTransformationOption, error) { - meta, err := common.NewSyncableSecretMetaData(obj) + meta, err := common.NewSyncableSecretMetaDataI(obj) if err != nil { return nil, err } - keyedTemplates, ff, err := gatherTemplates(ctx, client, meta) - if err != nil { - return nil, err - } + return NewSecretTransformationOptionWithTransformation(ctx, client, obj, meta.GetTransformation(), globalOpt) +} - opt := &SecretTransformationOption{ - Excludes: ff.excludes(), - Includes: ff.includes(), - KeyedTemplates: keyedTemplates, - Annotations: obj.GetAnnotations(), - Labels: obj.GetLabels(), - } +func NewSecretTransformationOptionWithTransformation(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Object, transformation *secretsv1beta1.Transformation, + globalOpt *GlobalTransformationOptions, +) (*SecretTransformationOption, error) { + opt := &SecretTransformationOption{} + if transformation != nil { + keyedTemplates, ff, err := gatherTemplates(ctx, client, obj, transformation) + if err != nil { + return nil, err + } - if globalOpt != nil { - opt.ExcludeRaw = globalOpt.ExcludeRaw + opt.Annotations = obj.GetAnnotations() + opt.Labels = obj.GetLabels() + opt.Excludes = ff.excludes() + opt.Includes = ff.includes() + opt.KeyedTemplates = keyedTemplates + opt.ExcludeRaw = transformation.ExcludeRaw } - if meta.Destination.Transformation.ExcludeRaw { - opt.ExcludeRaw = meta.Destination.Transformation.ExcludeRaw + if globalOpt != nil { + if globalOpt.ExcludeRaw { + opt.ExcludeRaw = globalOpt.ExcludeRaw + } } return opt, nil @@ -154,10 +160,14 @@ func NewSecretTransformationOption(ctx context.Context, client ctrlclient.Client // gatherTemplates attempts to collect all v1beta1.Template(s) for the // syncable secret object. -func gatherTemplates(ctx context.Context, client ctrlclient.Client, meta *common.SyncableSecretMetaData) ([]*KeyedTemplate, *fieldFilters, error) { +func gatherTemplates(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Object, transformation *secretsv1beta1.Transformation) ([]*KeyedTemplate, *fieldFilters, error) { var errs error var keyedTemplates []*KeyedTemplate + if transformation == nil { + return nil, nil, fmt.Errorf("transformation is nil") + } + // used to deduplicate templates by name seenTemplates := make(map[string]secretsv1beta1.Template) addTemplate := func(tmpl secretsv1beta1.Template, key string) { @@ -180,15 +190,19 @@ func gatherTemplates(ctx context.Context, client ctrlclient.Client, meta *common } ff := newFieldFilters() - ff.addExcludes(meta.Destination.Transformation.Excludes...) - ff.addIncludes(meta.Destination.Transformation.Includes...) + ff.addExcludes(transformation.Excludes...) + ff.addIncludes(transformation.Includes...) + + objKey := ctrlclient.ObjectKeyFromObject(obj) + if err := common.ValidateObjectKey(objKey); err != nil { + return nil, nil, err + } - transformation := meta.Destination.Transformation // get the in-line template templates for key, tmpl := range transformation.Templates { name := tmpl.Name if name == "" { - tmpl.Name = fmt.Sprintf("%s/%s/%s", meta.Namespace, meta.Name, key) + tmpl.Name = fmt.Sprintf("%s/%s", objKey, key) } addTemplate(tmpl, key) } @@ -196,7 +210,7 @@ func gatherTemplates(ctx context.Context, client ctrlclient.Client, meta *common seenRefs := make(map[ctrlclient.ObjectKey]bool) // get the remote ref template templates for _, ref := range transformation.TransformationRefs { - ns := meta.Namespace + ns := obj.GetNamespace() if ref.Namespace != "" { ns = ref.Namespace } @@ -435,6 +449,8 @@ func filterData[V any](opt *SecretTransformationOption, data map[string]V) (map[ type SecretInput struct { // Secrets contains the secret data that is considered confidential. Secrets map[string]any `json:"secrets"` + // WrapData contains any wrapping key material and its corresponding metadata. + WrapData map[string]any `json:"wrapData,omitempty"` // Metadata contains the secret metadata that is not considered confidential. Metadata map[string]any `json:"metadata"` // Annotations associated with syncable secret K8s resource @@ -446,9 +462,7 @@ type SecretInput struct { // NewSecretInput sets up a SecretInput instance from the provided secret data // secret metadata, and annotations and labels which are typically of the type // map[string]string. -func NewSecretInput[A, L any](secrets, metadata map[string]any, - annotations map[string]A, labels map[string]L, -) *SecretInput { +func NewSecretInput[A, L any](secrets, wrapData, metadata map[string]any, annotations map[string]A, labels map[string]L) *SecretInput { var a map[string]any if annotations != nil { a = make(map[string]any) @@ -470,6 +484,7 @@ func NewSecretInput[A, L any](secrets, metadata map[string]any, return &SecretInput{ Secrets: secrets, Metadata: metadata, + WrapData: wrapData, Annotations: a, Labels: l, } diff --git a/helpers/template_test.go b/helpers/template_test.go index ce4c42b31..fe57c9f6c 100644 --- a/helpers/template_test.go +++ b/helpers/template_test.go @@ -34,6 +34,11 @@ func Test_renderTemplates(t *testing.T) { "super": "duper", }, } + wrapData := map[string]any{ + "token": "c29tZS10b2tlbg==", // base64 encoded value of `some-token` + "ttl": "1h", + "accessor": "some-accessor", + } tests := []struct { name string input *SecretInput @@ -43,7 +48,7 @@ func Test_renderTemplates(t *testing.T) { }{ { name: "multi-with-helper", - input: NewSecretInput[any, any](secrets, nil, nil, nil), + input: NewSecretInput[any, any](secrets, nil, nil, nil, nil), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -84,7 +89,7 @@ func Test_renderTemplates(t *testing.T) { }, { name: "multi-with-multi-helpers", - input: NewSecretInput[string, string](secrets, nil, nil, nil), + input: NewSecretInput[string, string](secrets, nil, nil, nil, nil), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -131,17 +136,14 @@ func Test_renderTemplates(t *testing.T) { }, { name: "multi-with-real-world-helper", - input: NewSecretInput( - map[string]any{ - "username": "alice", - "password": "secret", - }, nil, - map[string]string{ - "myapp.config/postgres-host": "postgres-postgresql.postgres.svc.cluster.local:5432", - }, - map[string]string{ - "myapp/name": "db", - }), + input: NewSecretInput(map[string]any{ + "username": "alice", + "password": "secret", + }, nil, nil, map[string]string{ + "myapp.config/postgres-host": "postgres-postgresql.postgres.svc.cluster.local:5432", + }, map[string]string{ + "myapp/name": "db", + }), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -224,7 +226,7 @@ db.username=alice }, { name: "single-with-metadata-only", - input: NewSecretInput[string, string](secrets, metadata, nil, nil), + input: NewSecretInput[string, string](secrets, nil, metadata, nil, nil), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -244,7 +246,7 @@ db.username=alice }, { name: "single-with-secret-and-metadata", - input: NewSecretInput[string, string](secrets, metadata, nil, nil), + input: NewSecretInput[string, string](secrets, nil, metadata, nil, nil), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -263,14 +265,34 @@ db.username=alice }, wantErr: assert.NoError, }, + { + name: "single-with-secret-wrapData-and-metadata", + input: NewSecretInput[string, string](secrets, wrapData, metadata, nil, nil), + opt: &SecretTransformationOption{ + KeyedTemplates: []*KeyedTemplate{ + { + Key: "tmpl", + Template: secretsv1beta1.Template{ + Name: "tmpl", + Text: `{{- $custom := get .Metadata "custom" -}} + {{- printf "%s_%s_%s_%s" (get $custom "super") (get .Secrets "bar" | b64dec) (get .WrapData "accessor") (get .WrapData "token" | b64dec) -}} + `, + }, + }, + }, + }, + want: map[string][]byte{ + "tmpl": []byte(`duper_buz_some-accessor_some-token`), + }, + wantErr: assert.NoError, + }, { name: "single-with-secret-metadata-annotations-and-labels", - input: NewSecretInput[string, string](secrets, metadata, - map[string]string{ - "anno1": "foo", - }, map[string]string{ - "label1": "baz", - }), + input: NewSecretInput[string, string](secrets, nil, metadata, map[string]string{ + "anno1": "foo", + }, map[string]string{ + "label1": "baz", + }), opt: &SecretTransformationOption{ KeyedTemplates: []*KeyedTemplate{ { @@ -291,7 +313,7 @@ db.username=alice }, { name: "no-specs-error", - input: NewSecretInput[string, string](nil, nil, nil, nil), + input: NewSecretInput[string, string](nil, nil, nil, nil, nil), opt: &SecretTransformationOption{}, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return assert.EqualError(t, err, @@ -1272,6 +1294,8 @@ func TestNewSecretTransformationOption(t *testing.T) { } func TestNewSecretInput(t *testing.T) { + t.Parallel() + secrets := map[string]any{ "foo": "baz", "biff": 1, @@ -1281,9 +1305,14 @@ func TestNewSecretInput(t *testing.T) { "buz": "qux", }, } + + wrapData := map[string]any{ + "token": "1234567890abcdef", + } tests := []struct { name string secrets map[string]any + wrapData map[string]any metadata map[string]any annotations map[string]string labels map[string]string @@ -1306,19 +1335,30 @@ func TestNewSecretInput(t *testing.T) { }, }, { - name: "both", + name: "wrapData-only", + wrapData: wrapData, + want: &SecretInput{ + Secrets: nil, + Metadata: nil, + WrapData: wrapData, + }, + }, + { + name: "all", secrets: secrets, metadata: metadata, + wrapData: wrapData, want: &SecretInput{ Secrets: secrets, Metadata: metadata, + WrapData: wrapData, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, NewSecretInput(tt.secrets, tt.metadata, tt.annotations, tt.labels), - "NewSecretInput(%v, %v)", tt.secrets, tt.metadata) + assert.Equalf(t, tt.want, NewSecretInput(tt.secrets, tt.wrapData, tt.metadata, tt.annotations, tt.labels), + "NewSecretInput(%v, %v, %v)", tt.secrets, tt.wrapData, tt.metadata) }) } } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 964f98ea6..683fadd57 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -50,7 +50,7 @@ var ResourceStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{ "namespace", }) -func init() { +func MustRegisterResourceStatus() { metrics.Registry.MustRegister( ResourceStatus, ) diff --git a/main.go b/main.go index 58eb61948..df1d28a64 100644 --- a/main.go +++ b/main.go @@ -46,8 +46,8 @@ import ( secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1" "github.com/hashicorp/vault-secrets-operator/controllers" "github.com/hashicorp/vault-secrets-operator/internal/metrics" - "github.com/hashicorp/vault-secrets-operator/internal/options" "github.com/hashicorp/vault-secrets-operator/internal/version" + "github.com/hashicorp/vault-secrets-operator/options" // +kubebuilder:scaffold:imports ) @@ -204,7 +204,7 @@ func main() { "Maximum elapsed time without a successful sync from the secret's source. "+ "It's important to note that setting this option to anything other than "+ "its default value of 0 will result in the secret sync no longer being retried after "+ - "reaching the max elapsed time without a successful sync. This could "+ + "reaching the max elapsed time without a successful sync. This could result in stale secrets."+ "All errors are tried using an exponential backoff strategy. "+ "Also set from environment variable VSO_BACKOFF_MAX_ELAPSED_TIME.") flag.Float64Var(&backoffRandomizationFactor, "backoff-randomization-factor", @@ -268,6 +268,9 @@ func main() { if vsoEnvOptions.BackoffMaxInterval != 0 { backoffMaxInterval = vsoEnvOptions.BackoffMaxInterval } + if vsoEnvOptions.BackoffMaxElapsedTime != 0 { + backoffMaxElapsedTime = vsoEnvOptions.BackoffMaxElapsedTime + } if vsoEnvOptions.BackoffRandomizationFactor != 0 { backoffRandomizationFactor = vsoEnvOptions.BackoffRandomizationFactor } @@ -399,6 +402,7 @@ func main() { metrics.NewBuildInfoGauge(versionInfo), ) vclient.MustRegisterClientMetrics(cfc.MetricsRegistry) + metrics.MustRegisterResourceStatus() metric := prometheus.NewGauge( prometheus.GaugeOpts{ diff --git a/internal/options/env.go b/options/env.go similarity index 100% rename from internal/options/env.go rename to options/env.go diff --git a/internal/options/env_test.go b/options/env_test.go similarity index 100% rename from internal/options/env_test.go rename to options/env_test.go diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index 8d312fd8a..cca74367e 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -436,7 +436,7 @@ func waitForPKIData(t *testing.T, maxRetries int, delay time.Duration, vpsObj *s } for _, field := range []string{"certificate", "private_key"} { if len(destSecret.Data[field]) == 0 { - return "", errors.New(field + " is empty") + return "", fmt.Errorf("%s is empty", field) } } tlsFieldsCheck, err := checkTLSFields(destSecret) diff --git a/test/integration/modules/vso-helm/main.tf b/test/integration/modules/vso-helm/main.tf index 4724bdd0a..dcd0eb533 100644 --- a/test/integration/modules/vso-helm/main.tf +++ b/test/integration/modules/vso-helm/main.tf @@ -14,7 +14,7 @@ resource "random_string" "suffix" { resource "helm_release" "vault-secrets-operator" { name = local.name namespace = var.operator_namespace - create_namespace = true + create_namespace = var.create_namespace wait = true chart = var.operator_helm_chart_path @@ -157,4 +157,12 @@ resource "helm_release" "vault-secrets-operator" { value = var.memory_requests } } + set { + name = "csi.enabled" + value = var.csi_enabled + } + set { + name = "csi.driver.logging.level" + value = var.csi_logging_level + } } diff --git a/test/integration/modules/vso-helm/variables.tf b/test/integration/modules/vso-helm/variables.tf index d2883315e..65bfd223d 100644 --- a/test/integration/modules/vso-helm/variables.tf +++ b/test/integration/modules/vso-helm/variables.tf @@ -111,3 +111,18 @@ variable "manager_extra_args" { "-zap-log-level=5" ] } + +variable "csi_enabled" { + type = bool + default = false +} + +variable "csi_logging_level" { + type = string + default = "info" +} + +variable "create_namespace" { + type = bool + default = true +} diff --git a/test/integration/revocation_integration_test.go b/test/integration/revocation_integration_test.go index 441fcf388..71d721bf8 100644 --- a/test/integration/revocation_integration_test.go +++ b/test/integration/revocation_integration_test.go @@ -177,9 +177,11 @@ func TestRevocation(t *testing.T) { Spec: secretsv1beta1.VaultStaticSecretSpec{ VaultAuthRef: a.Name, Namespace: testVaultNamespace, - Mount: testKvv2MountPath, - Type: consts.KVSecretTypeV2, - Path: dest, + VaultStaticSecretCommon: secretsv1beta1.VaultStaticSecretCommon{ + Mount: testKvv2MountPath, + Type: consts.KVSecretTypeV2, + Path: dest, + }, Destination: secretsv1beta1.Destination{ Name: dest, Create: true, diff --git a/test/integration/vaultauthmethods_integration_test.go b/test/integration/vaultauthmethods_integration_test.go index a4a21c700..ecff2409c 100644 --- a/test/integration/vaultauthmethods_integration_test.go +++ b/test/integration/vaultauthmethods_integration_test.go @@ -450,9 +450,11 @@ func TestVaultAuthMethods(t *testing.T) { Spec: secretsv1beta1.VaultStaticSecretSpec{ VaultAuthRef: a.vaultAuth.Name, Namespace: testVaultNamespace, - Mount: testKvv2MountPath, - Type: consts.KVSecretTypeV2, - Path: dest, + VaultStaticSecretCommon: secretsv1beta1.VaultStaticSecretCommon{ + Mount: testKvv2MountPath, + Type: consts.KVSecretTypeV2, + Path: dest, + }, Destination: secretsv1beta1.Destination{ Name: dest, Create: true, diff --git a/test/integration/vaultstaticsecret_integration_test.go b/test/integration/vaultstaticsecret_integration_test.go index f050ac3a9..264cbf709 100644 --- a/test/integration/vaultstaticsecret_integration_test.go +++ b/test/integration/vaultstaticsecret_integration_test.go @@ -240,9 +240,11 @@ func TestVaultStaticSecret(t *testing.T) { // This Secret references an Auth Method in a different namespace. // VaultAuthRef: fmt.Sprintf("%s/%s", auths[0].ObjectMeta.Namespace, auths[0].ObjectMeta.Name), Namespace: outputs.AppVaultNamespace, - Mount: outputs.KVMount, - Type: consts.KVSecretTypeV1, - Path: "secret", + VaultStaticSecretCommon: secretsv1beta1.VaultStaticSecretCommon{ + Mount: outputs.KVMount, + Type: consts.KVSecretTypeV1, + Path: "secret", + }, Destination: secretsv1beta1.Destination{ Name: "secretkv", Create: false, @@ -267,9 +269,11 @@ func TestVaultStaticSecret(t *testing.T) { // This Secret references the default Auth Method. VaultAuthRef: ctrlclient.ObjectKeyFromObject(defaultVaultAuth).String(), Namespace: outputs.AppK8sNamespace, - Mount: outputs.KVV2Mount, - Type: consts.KVSecretTypeV2, - Path: "secret", + VaultStaticSecretCommon: secretsv1beta1.VaultStaticSecretCommon{ + Mount: outputs.KVV2Mount, + Type: consts.KVSecretTypeV2, + Path: "secret", + }, Destination: secretsv1beta1.Destination{ Name: "secretkvv2", Create: false, @@ -536,9 +540,11 @@ func TestVaultStaticSecret(t *testing.T) { Spec: secretsv1beta1.VaultStaticSecretSpec{ VaultAuthRef: fmt.Sprintf("%s/%s", auths[0].ObjectMeta.Namespace, auths[0].ObjectMeta.Name), Namespace: outputs.AppVaultNamespace, - Mount: mount, - Type: kvType, - Path: dest, + VaultStaticSecretCommon: secretsv1beta1.VaultStaticSecretCommon{ + Mount: mount, + Type: kvType, + Path: dest, + }, Destination: secretsv1beta1.Destination{ Name: dest, Create: true, diff --git a/test/unit/clusterrole-aggregates-csi-driver.bats b/test/unit/clusterrole-aggregates-csi-driver.bats new file mode 100644 index 000000000..141c97c85 --- /dev/null +++ b/test/unit/clusterrole-aggregates-csi-driver.bats @@ -0,0 +1,311 @@ +#!/usr/bin/env bats + +# +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +# + +load _helpers + +#-------------------------------------------------------------------- +# clusterrole-aggregated-viewer-csi-driver tests + +@test "CSIDriver/ClusterRoleAggregated: not created when csi.enabled is false - default" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/clusterrole-aggregated-viewer-csi-driver.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver/ClusterRoleAggregated: name is correct when csi.enabled is true" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/clusterrole-aggregated-viewer-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.metadata.name' | tee /dev/stderr) + + [ "${object}" = "release-name-vault-secrets-operator-aggregate-role-viewer-csi-driver" ] +} + +@test "CSIDriver/ClusterRoleAggregated: metadata labels are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/clusterrole-aggregated-viewer-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.metadata.labels' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "8" ] + actual=$(echo "$object" | yq '."app.kubernetes.io/component"' | tee /dev/stderr) + [ "${actual}" = "rbac" ] + actual=$(echo "$object" | yq '."vso.hashicorp.com/role-instance"' | tee /dev/stderr) + [ "${actual}" = "aggregate-role-viewer" ] + actual=$(echo "$object" | yq '."rbac.authorization.k8s.io/aggregate-to-view"' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "CSIDriver/ClusterRoleAggregated: aggregation rules correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/clusterrole-aggregated-viewer-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.aggregationRule.clusterRoleSelectors' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "1" ] + actual=$(echo "$object" | yq '.[0].matchLabels | length' | tee /dev/stderr) + [ "${actual}" = "1" ] + actual=$(echo "$object" | + yq '.[0].matchLabels."vso.hashicorp.com/aggregate-to-viewer"' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# csisecrets_editor_role tests + +@test "CSIDriver/ClusterRoleEditor: metadata name is correct" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_editor_role.yaml \ + . | tee /dev/stderr | + yq '.metadata.name' | tee /dev/stderr) + + [ "${object}" = "release-name-vault-secrets-operator-csisecrets-editor-role" ] +} + +@test "CSIDriver/ClusterRoleEditor: metadata labels are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_editor_role.yaml \ + . | tee /dev/stderr | + yq '.metadata.labels' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "8" ] + actual=$(echo "$object" | yq '."app.kubernetes.io/component"' | tee /dev/stderr) + [ "${actual}" = "rbac" ] + actual=$(echo "$object" | yq '."vso.hashicorp.com/role-instance"' | tee /dev/stderr) + [ "${actual}" = "csisecrets-editor-role" ] + actual=$(echo "$object" | yq '."vso.hashicorp.com/aggregate-to-editor"' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "CSIDriver/ClusterRoleEditor: rules are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_editor_role.yaml \ + . | tee /dev/stderr | + yq '.rules' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "2" ] + + actual=$(echo "$object" | yq '.[0].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "secrets.hashicorp.com" ] + actual=$(echo "$object" | yq '.[0].resources[0]' | tee /dev/stderr) + [ "${actual}" = "csisecrets" ] + actual=$(echo "$object" | yq '.[0].verbs | join(",")' | tee /dev/stderr) + [ "${actual}" = "create,delete,get,list,patch,update,watch" ] + + actual=$(echo "$object" | yq '.[1].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "secrets.hashicorp.com" ] + actual=$(echo "$object" | yq '.[1].resources[0]' | tee /dev/stderr) + [ "${actual}" = "csisecrets/status" ] + actual=$(echo "$object" | yq '.[1].verbs[0]' | tee /dev/stderr) + [ "${actual}" = "get" ] +} + +#-------------------------------------------------------------------- +# csisecrets_viewer_role tests + +@test "CSIDriver/ClusterRoleViewer: metadata name is correct" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_viewer_role.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.metadata.name' | tee /dev/stderr) + + [ "${object}" = "release-name-vault-secrets-operator-csisecrets-viewer-role" ] +} + +@test "CSIDriver/ClusterRoleViewer: metadata labels are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_viewer_role.yaml \ + . | tee /dev/stderr | + yq '.metadata.labels' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "8" ] + actual=$(echo "$object" | yq '."app.kubernetes.io/component"' | tee /dev/stderr) + [ "${actual}" = "rbac" ] + actual=$(echo "$object" | yq '."vso.hashicorp.com/role-instance"' | tee /dev/stderr) + [ "${actual}" = "csisecrets-viewer-role" ] + actual=$(echo "$object" | yq '."vso.hashicorp.com/aggregate-to-viewer"' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "CSIDriver/ClusterRoleViewer: rules are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csisecrets_viewer_role.yaml \ + . | tee /dev/stderr | + yq '.rules' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "2" ] + + actual=$(echo "$object" | yq '.[0].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "secrets.hashicorp.com" ] + actual=$(echo "$object" | yq '.[0].resources[0]' | tee /dev/stderr) + [ "${actual}" = "csisecrets" ] + actual=$(echo "$object" | yq '.[0].verbs | join(",")' | tee /dev/stderr) + [ "${actual}" = "get,list,watch" ] + + actual=$(echo "$object" | yq '.[1].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "secrets.hashicorp.com" ] + actual=$(echo "$object" | yq '.[1].resources[0]' | tee /dev/stderr) + [ "${actual}" = "csisecrets/status" ] + actual=$(echo "$object" | yq '.[1].verbs[0]' | tee /dev/stderr) + [ "${actual}" = "get" ] +} + +#-------------------------------------------------------------------- +# cluster-role-csi-driver tests + +@test "CSIDriver/ClusterRole: not created when csi.enabled is false - default" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/cluster-role-csi-driver.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver/ClusterRole: metadata name is correct" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.metadata.name' | tee /dev/stderr) + + [ "${object}" = "release-name-vault-secrets-operator-csi-driver-role" ] +} + +@test "CSIDriver/ClusterRole: metadata labels are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.metadata.labels' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "6" ] + actual=$(echo "$object" | yq '."app.kubernetes.io/component"' | tee /dev/stderr) + [ "${actual}" = "rbac" ] +} + +@test "CSIDriver/ClusterRole: rules are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq '.rules' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "4" ] + + actual=$(echo "$object" | yq '.[0].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "" ] + actual=$(echo "$object" | yq '.[0].resources | join(",")' | tee /dev/stderr) + [ "${actual}" = "pods,serviceaccounts,configmaps" ] + actual=$(echo "$object" | yq '.[0].verbs | join(",")' | tee /dev/stderr) + [ "${actual}" = "get,list,watch" ] + + actual=$(echo "$object" | yq '.[1].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "" ] + actual=$(echo "$object" | yq '.[1].resources[0]' | tee /dev/stderr) + [ "${actual}" = "pods/status" ] + actual=$(echo "$object" | yq '.[1].verbs[0]' | tee /dev/stderr) + [ "${actual}" = "get" ] + + actual=$(echo "$object" | yq '.[2].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "" ] + actual=$(echo "$object" | yq '.[2].resources[0]' | tee /dev/stderr) + [ "${actual}" = "events" ] + actual=$(echo "$object" | yq '.[2].verbs | join(",")' | tee /dev/stderr) + [ "${actual}" = "create,patch" ] + + actual=$(echo "$object" | yq '.[3].apiGroups[0]' | tee /dev/stderr) + [ "${actual}" = "" ] + actual=$(echo "$object" | yq '.[3].resources[0]' | tee /dev/stderr) + [ "${actual}" = "serviceaccounts/token" ] + actual=$(echo "$object" | yq '.[3].verbs | join(",")' | tee /dev/stderr) + [ "${actual}" = "create,get,list,watch" ] +} + +#-------------------------------------------------------------------- +# cluster-role-binding tests + +@test "CSIDriver/ClusterRoleBinding: metadata name is correct" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-binding.yaml \ + . | tee /dev/stderr | + yq '.metadata.name' | tee /dev/stderr) + + [ "${object}" = "release-name-vault-secrets-operator-manager-rolebinding" ] +} + +@test "CSIDriver/ClusterRoleBinding: labels are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-binding.yaml \ + . | tee /dev/stderr | + yq '.metadata.labels' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "6" ] + actual=$(echo "$object" | yq '."app.kubernetes.io/component"' | tee /dev/stderr) + [ "${actual}" = "controller-manager" ] +} + +@test "CSIDriver/ClusterRoleBinding: roleRef is correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-binding.yaml \ + . | tee /dev/stderr | + yq '.roleRef' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '.kind' | tee /dev/stderr) + [ "${actual}" = "ClusterRole" ] + actual=$(echo "$object" | yq '.name' | tee /dev/stderr) + [ "${actual}" = "release-name-vault-secrets-operator-manager-role" ] +} + +@test "CSIDriver/ClusterRoleBinding: subjects are correctly set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/cluster-role-binding.yaml \ + . | tee /dev/stderr | + yq '.subjects' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "1" ] + + actual=$(echo "$object" | yq '.[0].kind' | tee /dev/stderr) + [ "${actual}" = "ServiceAccount" ] + actual=$(echo "$object" | yq '.[0].name' | tee /dev/stderr) + [ "${actual}" = "release-name-vault-secrets-operator-controller-manager" ] + actual=$(echo "$object" | yq '.[0].namespace' | tee /dev/stderr) + [ "${actual}" = "default" ] +} diff --git a/test/unit/csi-driver.bats b/test/unit/csi-driver.bats new file mode 100644 index 000000000..2738cced4 --- /dev/null +++ b/test/unit/csi-driver.bats @@ -0,0 +1,621 @@ +#!/usr/bin/env bats + +# +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +# + +load _helpers + +#-------------------------------------------------------------------- +# ServiceAccount Tests + +@test "CSIDriver/ServiceAccount: not created when csi.enabled is false" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + . | tee /dev/stderr | + yq 'select(.kind == "ServiceAccount") | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver/ServiceAccount: created when csi.enabled is true" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "ServiceAccount") | .kind' | tee /dev/stderr) + [ "${actual}" = "ServiceAccount" ] +} + +@test "CSIDriver/ServiceAccount: labels are correctly set" { + cd "$(chart_dir)" + local labels=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "ServiceAccount") | .metadata.labels' | tee /dev/stderr) + + local component=$(echo "$labels" | yq '."app.kubernetes.io/component"') + [ "$component" = "csi-driver" ] +} + +@test "CSIDriver/ServiceAccount: no imagePullSecrets by default" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "ServiceAccount") | .imagePullSecrets' | tee /dev/stderr) + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver/ServiceAccount: custom imagePullSecrets can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.imagePullSecrets[0].name=foo' \ + --set 'csi.imagePullSecrets[1].name=bar' \ + . | tee /dev/stderr | + yq 'select(.kind == "ServiceAccount") | .imagePullSecrets' | tee /dev/stderr) + + local actual=$(echo "$object" | yq -r 'length' | tee /dev/stderr) + [ "${actual}" = "2" ] + actual=$(echo "$object" | yq -r '.[0].name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + actual=$(echo "$object" | yq -r '.[1].name' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +#-------------------------------------------------------------------- +# CSIDriver Tests + +@test "CSIDriver: not created when csi.enabled is false" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + . | tee /dev/stderr | + yq 'select(.kind == "CSIDriver") | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver: created when csi.enabled is true" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "CSIDriver") | .kind' | tee /dev/stderr) + [ "${actual}" = "CSIDriver" ] +} + +@test "CSIDriver: podInfoOnMount and volume lifecycle modes are correctly configured" { + cd "$(chart_dir)" + local config=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "CSIDriver") | .spec' | tee /dev/stderr) + local podInfoOnMount=$(echo "$config" | yq '.podInfoOnMount') + [ "$podInfoOnMount" = "true" ] + local volumeLifecycleModes=$(echo "$config" | yq '.volumeLifecycleModes[0]') + [ "$volumeLifecycleModes" = "Ephemeral" ] +} + +@test "CSIDriver: tokenRequests are correctly configured" { + cd "$(chart_dir)" + local config=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "CSIDriver") | .spec.tokenRequests' | tee /dev/stderr) + local audience=$(echo "$config" | yq '.[0].audience') + [ "$audience" = "csi.vso.hashicorp.com" ] +} + +#-------------------------------------------------------------------- +# DaemonSet Tests + +@test "CSIDriver/DaemonSet: not created when csi.enabled is false" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "CSIDriver/DaemonSet: created when csi.enabled is true" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .kind' | tee /dev/stderr) + [ "${actual}" = "DaemonSet" ] +} + +@test "CSIDriver/DeamonSet: hostAliases not set by default" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.hostAliases' | tee /dev/stderr) + + [ "${object}" = null ] +} + +@test "CSIDriver/DeamonSet: custom hostAliases can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.hostAliases[0].ip=192.168.1.100' \ + --set 'csi.hostAliases[0].hostnames={vault.example.com}' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.hostAliases' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.[0] | length' | tee /dev/stderr) + [ "${actual}" = '2' ] + actual=$(echo "$object" | yq '.[0].ip' | tee /dev/stderr) + [ "${actual}" = '192.168.1.100' ] + actual=$(echo "$object" | yq '.[0].hostnames | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.[0].hostnames[0]' | tee /dev/stderr) + [ "${actual}" = 'vault.example.com' ] +} + +@test "CSIDriver/DaemonSet: nodeSelector set to default kubernetes.io/os: linux" { + cd "$(chart_dir)" + local object=$(helm template -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.nodeSelector' | tee /dev/stderr) + local os=$(echo "$object" | yq '."kubernetes.io/os"') + [ "$os" = "linux" ] +} + +@test "CSIDriver/DaemonSet: nodeSelector includes custom value and default" { + cd "$(chart_dir)" + local object=$(helm template -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.nodeSelector.custom-key=custom-value' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.nodeSelector' | tee /dev/stderr) + + local custom_value=$(echo "$object" | yq '."custom-key"') + [ "$custom_value" = "custom-value" ] + local os=$(echo "$object" | yq '."kubernetes.io/os"') + [ "$os" = "linux" ] +} + +@test "CSIDriver/DaemonSet: tolerations set to default operator: Exists" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.tolerations' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '.[0]."operator"' | tee /dev/stderr) + [ "${actual}" = "Exists" ] +} + +@test "CSIDriver/DaemonSet: custom tolerations can be set and includes default" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.tolerations[0].key=key1' \ + --set 'csi.tolerations[0].operator=Equal' \ + --set 'csi.tolerations[0].value=value1' \ + --set 'csi.tolerations[0].effect=NoSchedule' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.tolerations' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "2" ] + actual=$(echo "$object" | yq '.[0].key' | tee /dev/stderr) + [ "${actual}" = "key1" ] + actual=$(echo "$object" | yq '.[0].operator' | tee /dev/stderr) + [ "${actual}" = "Equal" ] + actual=$(echo "$object" | yq '.[0].value' | tee /dev/stderr) + [ "${actual}" = "value1" ] + actual=$(echo "$object" | yq '.[0].effect' | tee /dev/stderr) + [ "${actual}" = "NoSchedule" ] + + local default_operator=$(echo "$object" | yq '.[1].operator' | tee /dev/stderr) + [ "${default_operator}" = "Exists" ] +} + +@test "CSIDriver/DaemonSet: operator Exists toleration is not duplicated if specified by user" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.tolerations[0].operator=Exists' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.tolerations' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "1" ] + actual=$(echo "$object" | yq '.[0].operator' | tee /dev/stderr) + [ "${actual}" = "Exists" ] +} + +@test "CSIDriver/DaemonSet: default affinity is an empty object" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.affinity' | tee /dev/stderr) + [ "$actual" = null ] +} + +@test "CSIDriver/DaemonSet: affinity can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set "csi.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key=topology.kubernetes.io/zone" \ + --set "csi.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator=In" \ + --set "csi.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values={antarctica-east1,antarctica-west1}" \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.affinity' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0] | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions | length' | tee /dev/stderr) + [ "${actual}" = '1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | length' | tee /dev/stderr) + [ "${actual}" = '3' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key' | tee /dev/stderr) + [ "${actual}" = 'topology.kubernetes.io/zone' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator' | tee /dev/stderr) + [ "${actual}" = 'In' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | length' | tee /dev/stderr) + [ "${actual}" = '2' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values[0]' | tee /dev/stderr) + [ "${actual}" = 'antarctica-east1' ] + actual=$(echo "$object" | yq '.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values[1]' | tee /dev/stderr) + [ "${actual}" = 'antarctica-west1' ] +} + +@test "CSIDriver/DaemonSet: annotations set to default" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.metadata.annotations' | tee /dev/stderr) + local annotations=$(echo "$actual" | yq '."kubectl.kubernetes.io/default-container"') + [ "$annotations" = "secrets-store" ] +} + +@test "CSIDriver/DaemonSet: annotations include custom value and default" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.annotations.annotationKey=annotationValue' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.metadata.annotations' | tee /dev/stderr) + local annotationValue=$(echo "$actual" | yq '.annotationKey') + [ "$annotationValue" = "annotationValue" ] + local defaultAnnotation=$(echo "$actual" | yq '."kubectl.kubernetes.io/default-container"') + [ "$defaultAnnotation" = "secrets-store" ] +} + +@test "CSIDriver/DaemonSet: driver image defaults" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.containers[1].image' | tee /dev/stderr) + [ "${actual}" = "hashicorp/vault-secrets-operator-csi:0.0.0-dev" ] +} + +@test "CSIDriver/DaemonSet: custom driver image can be set" { + cd "$(chart_dir)" + local actual=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.driver.image.repository=custom-repo' \ + --set 'csi.driver.image.tag=1.2.3' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.containers[1].image' | tee /dev/stderr) + [ "${actual}" = "custom-repo:1.2.3" ] +} + +@test "CSIDriver/DaemonSet: extraEnv variables aren't set by default" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.containers[1].env' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "2" ] +} + +@test "CSIDriver/DaemonSet: extraEnv variables can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.driver.extraEnv[0].name=HTTP_PROXY' \ + --set 'csi.driver.extraEnv[0].value=http://proxy.example.com' \ + --set 'csi.driver.extraEnv[1].name=VSO_OUTPUT_FORMAT' \ + --set 'csi.driver.extraEnv[1].value=json' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") | .spec.template.spec.containers[1].env' | tee /dev/stderr) + + local proxy=$(echo "$object" | yq '.[2].name') + [ "$proxy" = "HTTP_PROXY" ] + local proxy_value=$(echo "$object" | yq '.[2].value') + [ "$proxy_value" = "http://proxy.example.com" ] + + local output_format=$(echo "$object" | yq '.[3].name') + [ "$output_format" = "VSO_OUTPUT_FORMAT" ] + local format_value=$(echo "$object" | yq '.[3].value') + [ "$format_value" = "json" ] +} + +@test "CSIDriver/DaemonSet: extraArgs not set by default" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "10" ] +} + +@test "CSIDriver/DaemonSet: with extraArgs" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.driver.extraArgs={--foo=baz,--bar=qux}' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "12" ] + actual=$(echo "$object" | yq '.[10]' | tee /dev/stderr) + [ "${actual}" = "--foo=baz" ] + actual=$(echo "$object" | yq '.[11]' | tee /dev/stderr) + [ "${actual}" = "--bar=qux" ] +} + +@test "CSIDriver/DaemonSet: driver logging defaults" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app.kubernetes.io/component" == "csi-driver" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "10" ] + local actual_log_level=$(echo "$object" | yq '.[2]' | tee /dev/stderr) + [ "${actual_log_level}" = "--zap-log-level=info" ] + local actual_time_encoding=$(echo "$object" | yq '.[3]' | tee /dev/stderr) + [ "${actual_time_encoding}" = "--zap-time-encoding=rfc3339" ] + local actual_stacktrace_level=$(echo "$object" | yq '.[4]' | tee /dev/stderr) + [ "${actual_stacktrace_level}" = "--zap-stacktrace-level=panic" ] +} + +@test "CSIDriver/DaemonSet: driver logging custom values" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.driver.logging.level=debug' \ + --set 'csi.driver.logging.timeEncoding=millis' \ + --set 'csi.driver.logging.stacktraceLevel=error' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app.kubernetes.io/component" == "csi-driver" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "10" ] + local actual_log_level=$(echo "$object" | yq '.[2]' | tee /dev/stderr) + [ "${actual_log_level}" = "--zap-log-level=debug" ] + local actual_time_encoding=$(echo "$object" | yq '.[3]' | tee /dev/stderr) + [ "${actual_time_encoding}" = "--zap-time-encoding=millis" ] + local actual_stacktrace_level=$(echo "$object" | yq '.[4]' | tee /dev/stderr) + [ "${actual_stacktrace_level}" = "--zap-stacktrace-level=error" ] +} + +@test "CSIDriver/DaemonSet: livenessProbe args are set correctly" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "liveness-probe")' | tee /dev/stderr) + + local liveness_probe_image=$(echo "$object" | yq '.image') + [ "$liveness_probe_image" = "registry.k8s.io/sig-storage/livenessprobe:v2.10.0" ] + + local liveness_probe_args=$(echo "$object" | yq '.args') + local actual_length=$(echo "$liveness_probe_args" | yq '. | length') + [ "$actual_length" = "4" ] + + local actual=$(echo "$liveness_probe_args" | yq '.[0]') + [ "$actual" = "--csi-address=/csi/csi.sock" ] + actual=$(echo "$liveness_probe_args" | yq '.[1]') + [ "$actual" = "--probe-timeout=3s" ] + actual=$(echo "$liveness_probe_args" | yq '.[2]') + [ "$actual" = "--http-endpoint=0.0.0.0:9808" ] + actual=$(echo "$liveness_probe_args" | yq '.[3]') + [ "$actual" = "-v=2" ] +} + +@test "CSIDriver/DaemonSet: custom livenessProbe args can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.livenessProbe.extraArgs[0]=--foo=baz' \ + --set 'csi.livenessProbe.extraArgs[1]=--bar-arg=qux' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "liveness-probe")' | tee /dev/stderr) + + local liveness_probe_image=$(echo "$object" | yq '.image') + [ "$liveness_probe_image" = "registry.k8s.io/sig-storage/livenessprobe:v2.10.0" ] + + local liveness_probe_args=$(echo "$object" | yq '.args') + local actual_length=$(echo "$liveness_probe_args" | yq '. | length') + [ "$actual_length" = "6" ] + + local actual=$(echo "$liveness_probe_args" | yq '.[4]') + [ "$actual" = "--foo=baz" ] + actual=$(echo "$liveness_probe_args" | yq '.[5]') + [ "$actual" = "--bar-arg=qux" ] +} + +@test "CSIDriver/DaemonSet: nodeDriverRegistrar args set correctly" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "node-driver-registrar") | .args' | tee /dev/stderr) + + local actual_length=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual_length}" = "3" ] + actual=$(echo "$object" | yq '.[0]') + [ "$actual" = "--v=5" ] + actual=$(echo "$object" | yq '.[1]') + [ "$actual" = "--csi-address=/csi/csi.sock" ] + actual=$(echo "$object" | yq '.[2]') + [ "$actual" = "--kubelet-registration-path=/var/lib/kubelet/plugins/vso-csi/csi.sock" ] +} + +@test "CSIDriver/DaemonSet: custom extraArgs for nodeDriverRegistrar can be set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.nodeDriverRegistrar.extraArgs[0]=--foo=baz' \ + --set 'csi.nodeDriverRegistrar.extraArgs[1]=--bar=qux' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "node-driver-registrar") | .args' | tee /dev/stderr) + + local actual_length=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual_length}" = "5" ] + local actual=$(echo "$object" | yq '.[3]' | tee /dev/stderr) + [ "$actual" = "--foo=baz" ] + actual=$(echo "$object" | yq '.[4]' | tee /dev/stderr) + [ "$actual" = "--bar=qux" ] +} + +@test "CSIDriver/DeamonSet: without updateStrategy" { + cd "$(chart_dir)" + local object=$( + helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet") and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec' | tee /dev/stderr + ) + + local actual=$(echo "$object" | yq '.strategy' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "CSIDriver/DeamonSet: with rollingUpdate strategy" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.updateStrategy.type=rollingUpdate' \ + --set 'csi.updateStrategy.rollingUpdate.maxSurge=1' \ + --set 'csi.updateStrategy.rollingUpdate.maxUnavailable=1' \ + . | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec' | tee /dev/stderr) + + local actual=$(echo "$object" | yq '.updateStrategy | length' | tee /dev/stderr) + [ "${actual}" = "2" ] + actual=$(echo "$object" | yq '.updateStrategy.type' | tee /dev/stderr) + [ "${actual}" = "rollingUpdate" ] + actual=$(echo "$object" | yq '.updateStrategy.rollingUpdate | length' | tee /dev/stderr) + [ "${actual}" = "2" ] + actual=$(echo "$object" | yq '.updateStrategy.rollingUpdate.maxSurge' | tee /dev/stderr) + [ "${actual}" = "1" ] + actual=$(echo "$object" | yq '.updateStrategy.rollingUpdate.maxUnavailable' | tee /dev/stderr) + [ "${actual}" = "1" ] +} + +@test "CSIDriver/DaemonSet: with backoffOnSecretSourceErrorCSI defaults" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual + actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "10" ] + actual=$(echo "$object" | yq '.[5]' | tee /dev/stderr) + [ "${actual}" = "--backoff-initial-interval=5s" ] + actual=$(echo "$object" | yq '.[6]' | tee /dev/stderr) + [ "${actual}" = "--backoff-max-interval=60s" ] + actual=$(echo "$object" | yq '.[7]' | tee /dev/stderr) + [ "${actual}" = "--backoff-max-elapsed-time=0s" ] + actual=$(echo "$object" | yq '.[8]' | tee /dev/stderr) + [ "${actual}" = "--backoff-multiplier=1.50" ] + actual=$(echo "$object" | yq '.[9]' | tee /dev/stderr) + [ "${actual}" = "--backoff-randomization-factor=0.50" ] +} + +@test "CSIDriver/DaemonSet: with backoffOnSecretSourceErrorCSI set" { + cd "$(chart_dir)" + local object=$(helm template \ + -s templates/csi-driver.yaml \ + --set 'csi.enabled=true' \ + --set 'csi.driver.backoffOnSecretSourceError.initialInterval=30s' \ + --set 'csi.driver.backoffOnSecretSourceError.maxInterval=300s' \ + --set 'csi.driver.backoffOnSecretSourceError.maxElapsedTime=24h' \ + --set 'csi.driver.backoffOnSecretSourceError.multiplier=2.5' \ + --set 'csi.driver.backoffOnSecretSourceError.randomizationFactor=3.7361' \ + . | tee /dev/stderr | + yq 'select(.kind == "DaemonSet" and .metadata.labels."app" == "vault-secrets-operator-csi") | .spec.template.spec.containers[] | select(.name == "driver") | .args' | tee /dev/stderr) + + local actual + actual=$(echo "$object" | yq '. | length' | tee /dev/stderr) + [ "${actual}" = "10" ] + actual=$(echo "$object" | yq '.[5]' | tee /dev/stderr) + [ "${actual}" = "--backoff-initial-interval=30s" ] + actual=$(echo "$object" | yq '.[6]' | tee /dev/stderr) + [ "${actual}" = "--backoff-max-interval=300s" ] + actual=$(echo "$object" | yq '.[7]' | tee /dev/stderr) + [ "${actual}" = "--backoff-max-elapsed-time=24h" ] + actual=$(echo "$object" | yq '.[8]' | tee /dev/stderr) + [ "${actual}" = "--backoff-multiplier=2.50" ] + actual=$(echo "$object" | yq '.[9]' | tee /dev/stderr) + [ "${actual}" = "--backoff-randomization-factor=3.74" ] +} diff --git a/test/unit/helpers.bats b/test/unit/helpers.bats index 1ae43c83c..dac7092b9 100644 --- a/test/unit/helpers.bats +++ b/test/unit/helpers.bats @@ -54,12 +54,22 @@ load _helpers # clusterrolebindings. # # If this test fails, you're likely missing setting the namespace. - @test "helper/namespace: used everywhere" { - cd `chart_dir` - # Grep for files that don't have 'namespace: ' in them - local actual=$(grep -L 'namespace: ' templates/*.yaml | grep -E -v 'crd|rbac|editor_role|viewer_role|role.yaml|clusterrole' | tee /dev/stderr ) - [ "${actual}" = '' ] + cd "$(chart_dir)" + + # Render all templates and check for the presence of the 'namespace' field in relevant resources + local actual=$(helm template . | + yq 'select(.kind != "CustomResourceDefinition" and + .kind != "ClusterRole" and + .kind != "ClusterRoleBinding" and + .kind != "Role" and + .metadata.name != "editor_role" and + .metadata.name != "viewer_role") | + select(.metadata.namespace == null) | + {"name": .metadata.name, "kind": .kind, "doc": document_index}' | + tee /dev/stderr | grep -c '^') # count the number of documents missing 'namespace' + + [ "${actual}" = "0" ] } #-------------------------------------------------------------------- diff --git a/vault/cache.go b/vault/cache.go index bd2306f83..9c7e4885e 100644 --- a/vault/cache.go +++ b/vault/cache.go @@ -24,6 +24,7 @@ type ClientCache interface { Prune(filterFunc ClientCachePruneFilterFunc) []Client Contains(key ClientCacheKey) bool Purge() []ClientCacheKey + Keys() []ClientCacheKey } var _ ClientCache = (*clientCache)(nil) @@ -40,6 +41,10 @@ type clientCache struct { missCloneCounter prometheus.Counter } +func (c *clientCache) Keys() []ClientCacheKey { + return c.cache.Keys() +} + // Purge all Clients from the cache. Useful when shutting down a // CachingClientFactory. func (c *clientCache) Purge() []ClientCacheKey { diff --git a/vault/cache_key.go b/vault/cache_key.go index 831942f10..b46ec10c2 100644 --- a/vault/cache_key.go +++ b/vault/cache_key.go @@ -104,7 +104,40 @@ func ComputeClientCacheKeyFromObj(ctx context.Context, client ctrlclient.Client, return computeClientCacheKey(authObj, connObj, provider.GetUID()) } -// computeClientCacheKey for use in a ClientCache. It is derived by combining instances of +// ComputeClientCacheKeyFromMeta for use in a ClientCache. It is derived from the configuration of obj. +// If the obj is not of a supported type or is not properly configured, an error will be returned. +// This operation calls out to the Kubernetes API multiple times. +// +// See ComputeClientCacheKey for more details on how the client cache is derived. +func ComputeClientCacheKeyFromMeta(ctx context.Context, client ctrlclient.Client, m common.SyncableSecretMetaDataI, opts *ClientOptions) (ClientCacheKey, error) { + if opts.CredentialProviderFactory == nil { + return "", errors.New("CredentialProviderFactory is nil") + } + + authObj, err := common.GetVaultAuthNamespacedForMeta(ctx, client, m, opts.GlobalVaultAuthOptions) + if err != nil { + return "", err + } + + connName, err := common.GetConnectionNamespacedName(authObj) + if err != nil { + return "", err + } + + connObj, err := common.GetVaultConnection(ctx, client, connName) + if err != nil { + return "", err + } + + provider, err := opts.CredentialProviderFactory.New(ctx, client, authObj, m.GetProviderNamespace()) + if err != nil { + return "", err + } + + return computeClientCacheKey(authObj, connObj, provider.GetUID()) +} + +// ComputeClientCacheKey for use in a ClientCache. It is derived by combining instances of // VaultAuth, VaultConnection, and a CredentialProvider UID. // All of these elements are summed together into a SHA256 checksum, // and prefixed with the VaultAuth method. The chances of a collision are extremely remote, diff --git a/vault/client.go b/vault/client.go index 663e5ed2f..bad5efea7 100644 --- a/vault/client.go +++ b/vault/client.go @@ -26,11 +26,14 @@ import ( "github.com/hashicorp/vault-secrets-operator/internal/metrics" ) +type NewClientFunc func(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Object, opts *ClientOptions) (Client, error) + type ClientOptions struct { SkipRenewal bool WatcherDoneCh chan<- *ClientCallbackHandlerRequest GlobalVaultAuthOptions *common.GlobalVaultAuthOptions CredentialProviderFactory credentials.CredentialProviderFactory + UserAgent string } func defaultClientOptions() *ClientOptions { @@ -53,6 +56,7 @@ func NewClient(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Obj authObj = t providerNamespace = authObj.Namespace if providerNamespace != common.OperatorNamespace { + // TODO: update this to work in trusted orchestrator mode return nil, fmt.Errorf("invalid object %T, only allowed in the %s namespace", authObj, common.OperatorNamespace) } if authObj.Spec.StorageEncryption == nil { @@ -78,6 +82,12 @@ func NewClient(ctx context.Context, client ctrlclient.Client, obj ctrlclient.Obj if err != nil { return nil, err } + + return NewClientFromObjs(ctx, client, authObj, connObj, providerNamespace, opts) +} + +// NewClientFromObjs returns a Client from already resolved objects. +func NewClientFromObjs(ctx context.Context, client ctrlclient.Client, authObj *secretsv1beta1.VaultAuth, connObj *secretsv1beta1.VaultConnection, providerNamespace string, opts *ClientOptions) (Client, error) { c := &defaultClient{} if err := c.Init(ctx, client, authObj, connObj, providerNamespace, opts); err != nil { return nil, err @@ -268,7 +278,9 @@ func (c *defaultClient) Validate(ctx context.Context) error { } if expired, err := c.checkExpiry(0); expired || err != nil { - return errors.New("client token expired") + ttl, _ := c.getTokenTTL() + now := time.Now() + return fmt.Errorf("client token expired, ttl=%s, last=%d, now=%d", ttl, c.lastRenewal, now.Unix()) } if c.client == nil { @@ -276,7 +288,7 @@ func (c *defaultClient) Validate(ctx context.Context) error { } if c.tainted { - if _, err := c.Read(ctx, NewReadRequest("auth/token/lookup-self", nil)); err != nil { + if _, err := c.Read(ctx, NewReadRequest("auth/token/lookup-self", nil, nil)); err != nil { return fmt.Errorf("tainted client is invalid: %w", err) } } @@ -506,7 +518,7 @@ func (c *defaultClient) startLifetimeWatcher(ctx context.Context) error { wg := sync.WaitGroup{} wg.Add(1) go func(ctx context.Context, c *defaultClient, watcher *api.LifetimeWatcher) { - logger := log.FromContext(nil).WithName("lifetimeWatcher").WithValues( + logger := log.FromContext(ctx).WithName("lifetimeWatcher").WithValues( "id", watcherID, "entityID", c.authSecret.Auth.EntityID, "clientID", c.id, "cacheKey", cacheKey) logger.Info("Starting") @@ -521,7 +533,8 @@ func (c *defaultClient) startLifetimeWatcher(ctx context.Context) error { logger.V(consts.LogLevelDebug).Info("Started") for { select { - case <-ctx.Done(): + case result := <-ctx.Done(): + logger.V(consts.LogLevelTrace).Info("Context done", "result", result) return case err := <-watcher.DoneCh(): if err != nil { @@ -690,7 +703,7 @@ func (c *defaultClient) GetVaultConnectionObj() *secretsv1beta1.VaultConnection return c.connObj } -func (c *defaultClient) Read(ctx context.Context, request ReadRequest) (Response, error) { +func (c *defaultClient) Read(ctx context.Context, req ReadRequest) (Response, error) { var err error startTS := time.Now() defer func() { @@ -699,7 +712,7 @@ func (c *defaultClient) Read(ctx context.Context, request ReadRequest) (Response }() var respFunc func(*api.Secret) Response - switch t := request.(type) { + switch t := req.(type) { case *defaultReadRequest: respFunc = NewDefaultResponse case *kvReadRequestV1: @@ -710,15 +723,14 @@ func (c *defaultClient) Read(ctx context.Context, request ReadRequest) (Response return nil, fmt.Errorf("unsupported ReadRequest type %T", t) } - path := request.Path() var secret *api.Secret - secret, err = c.client.Logical().ReadWithDataWithContext(ctx, path, request.Values()) + secret, err = c.client.Logical().ReadWithRequest(ctx, req) if err != nil { return nil, err } if secret == nil { - return nil, fmt.Errorf("empty response from Vault, path=%q", path) + return nil, fmt.Errorf("empty response from Vault, path=%q", req.Path()) } return respFunc(secret), nil @@ -733,9 +745,12 @@ func (c *defaultClient) Write(ctx context.Context, req WriteRequest) (Response, }() var secret *api.Secret - secret, err = c.client.Logical().WriteWithContext(ctx, req.Path(), req.Params()) + secret, err = c.client.Logical().WriteWithRequest(ctx, req) + if err != nil { + return nil, err + } - return &defaultResponse{secret: secret}, err + return &defaultResponse{secret: secret}, nil } func (c *defaultClient) renew(ctx context.Context) error { @@ -759,7 +774,7 @@ func (c *defaultClient) renew(ctx context.Context) error { return errs } - resp, err := c.Write(ctx, NewWriteRequest("/auth/token/renew-self", nil)) + resp, err := c.Write(ctx, NewWriteRequest("/auth/token/renew-self", nil, nil)) if err != nil { c.authSecret = nil c.lastRenewal = 0 @@ -787,6 +802,17 @@ func (c *defaultClient) init(ctx context.Context, client ctrlclient.Client, if err != nil { return err } + + // set the User-Agent header + if cfg.Headers == nil { + cfg.Headers = make(http.Header) + } + if opts.UserAgent != "" { + cfg.Headers.Add(consts.HeaderUserAgent, opts.UserAgent) + } else { + cfg.Headers.Add(consts.HeaderUserAgent, common.DefaultVSOUserAgent) + } + vc, err := MakeVaultClient(ctx, cfg, client) if err != nil { return err @@ -881,7 +907,7 @@ func (m *MockRecordingVaultClient) Write(_ context.Context, s WriteRequest) (Res m.Requests = append(m.Requests, &MockRequest{ Method: http.MethodPut, Path: s.Path(), - Params: s.Params(), + Params: s.Data(), }) resps, ok := m.WriteResponses[s.Path()] @@ -907,13 +933,20 @@ func NewClientConfigFromConnObj(connObj *secretsv1beta1.VaultConnection, vaultNS return nil, errors.New("invalid nil VaultConnection") } + headers := make(http.Header) + if connObj.Spec.Headers != nil { + for k, v := range connObj.Spec.Headers { + headers.Add(k, v) + } + } + cfg := &ClientConfig{ Address: connObj.Spec.Address, SkipTLSVerify: connObj.Spec.SkipTLSVerify, TLSServerName: connObj.Spec.TLSServerName, K8sNamespace: connObj.Namespace, CACertSecretRef: connObj.Spec.CACertSecretRef, - Headers: connObj.Spec.Headers, + Headers: headers, VaultNamespace: vaultNS, } diff --git a/vault/client_factory.go b/vault/client_factory.go index ab5a0965d..22fca1620 100644 --- a/vault/client_factory.go +++ b/vault/client_factory.go @@ -108,10 +108,14 @@ type CachingClientFactory interface { Restore(context.Context, ctrlclient.Client, ctrlclient.Object) (Client, error) Prune(context.Context, ctrlclient.Client, ctrlclient.Object, CachingClientFactoryPruneRequest) (int, error) Start(context.Context) + Keys() []ClientCacheKey + Remove(ClientCacheKey) bool Stop() ShutDown(CachingClientFactoryShutDownRequest) } +type ClientGetValidator func(context.Context, Client) error + var _ CachingClientFactory = (*cachingClientFactory)(nil) type cachingClientFactory struct { @@ -148,6 +152,28 @@ type cachingClientFactory struct { GlobalVaultAuthOptions *common.GlobalVaultAuthOptions // credentialProviderFactory is a function that returns a CredentialProvider. credentialProviderFactory credentials.CredentialProviderFactory + // clientGetValidator function provide custom post client login validation on + // Get requests. + clientGetValidator ClientGetValidator + // newClientFunc is a function that returns a new Client. + newClientFunc NewClientFunc +} + +// Remove removes a Client from the cache. Returns true if the Client was removed. +func (m *cachingClientFactory) Remove(key ClientCacheKey) bool { + cacheKeyForLock := key.String() + m.clientMutex.LockKey(cacheKeyForLock) + defer func() { + if err := m.clientMutex.UnlockKey(cacheKeyForLock); err != nil { + m.logger.Error(err, "Failed to unlock client mutex") + } + }() + return m.cache.Remove(key) +} + +// Keys returns all the keys in the cache. +func (m *cachingClientFactory) Keys() []ClientCacheKey { + return m.cache.Keys() } // Start method for cachingClientFactory starts the lifetime watcher handler. @@ -284,7 +310,12 @@ func (m *cachingClientFactory) Restore(ctx context.Context, client ctrlclient.Cl ) }() - cacheKey, err = ComputeClientCacheKeyFromObj(ctx, client, obj, m.clientOptions()) + metaObj, err := common.NewSyncableSecretMetaDataI(obj) + if err != nil { + return nil, err + } + + cacheKey, err = ComputeClientCacheKeyFromMeta(ctx, client, metaObj, m.clientOptions()) if err != nil { m.recorder.Eventf(obj, v1.EventTypeWarning, consts.ReasonUnrecoverable, "Failed to get cacheKey from obj, err=%s", err) @@ -352,7 +383,12 @@ func (m *cachingClientFactory) Get(ctx context.Context, client ctrlclient.Client ) }() - cacheKey, err = ComputeClientCacheKeyFromObj(ctx, client, obj, m.clientOptions()) + metaObj, err := common.NewSyncableSecretMetaDataI(obj) + if err != nil { + return nil, err + } + + cacheKey, err = ComputeClientCacheKeyFromMeta(ctx, client, metaObj, m.clientOptions()) if err != nil { logger.Error(err, "Failed to get cacheKey from obj") m.recorder.Eventf(obj, v1.EventTypeWarning, consts.ReasonUnrecoverable, @@ -442,12 +478,19 @@ func (m *cachingClientFactory) Get(ctx context.Context, client ctrlclient.Client } // if we couldn't produce a valid Client, create a new one, log it in, and cache it - c, err = NewClientWithLogin(ctx, client, obj, m.clientOptions()) + c, err = m.newClientFunc(ctx, client, obj, m.clientOptions()) if err != nil { logger.Error(err, "Failed to get NewClientWithLogin") errs = errors.Join(err) return nil, errs + } + if m.clientGetValidator != nil { + if err := m.clientGetValidator(ctx, c); err != nil { + logger.Error(err, "Validator failed", + "cacheKey", cacheKey, "clientID", c.ID()) + return nil, err + } } logger.V(consts.LogLevelTrace).Info("New client created", @@ -707,7 +750,7 @@ func (m *cachingClientFactory) setEncryptionClient(ctx context.Context, client c return } - c, err = NewClientWithLogin(ctx, client, encryptionVaultAuth, &ClientOptions{ + c, err = m.newClientFunc(ctx, client, encryptionVaultAuth, &ClientOptions{ CredentialProviderFactory: m.credentialProviderFactory, }) if err != nil { @@ -883,6 +926,8 @@ func NewCachingClientFactory(ctx context.Context, client ctrlclient.Client, cach clientMutex: keymutex.NewHashed(config.ClientCacheNumLocks), GlobalVaultAuthOptions: config.GlobalVaultAuthOptions, credentialProviderFactory: config.CredentialProviderFactory, + clientGetValidator: config.ClientGetValidator, + newClientFunc: config.NewClientFunc, logger: zap.New().WithName("clientCacheFactory").WithValues( "persist", config.Persist, "enforceEncryption", config.StorageConfig.EnforceEncryption, @@ -953,6 +998,9 @@ type CachingClientFactoryConfig struct { // operations. A higher number of locks will reduce contention but increase // memory usage. ClientCacheNumLocks int + ClientGetValidator ClientGetValidator + // NewClientFunc is a function that returns a new Client. + NewClientFunc NewClientFunc } // DefaultCachingClientFactoryConfig provides the default configuration for a CachingClientFactory instance. @@ -966,6 +1014,7 @@ func DefaultCachingClientFactoryConfig() *CachingClientFactoryConfig { CredentialProviderFactory: credentials.NewCredentialProviderFactory(), SetupEncryptionClientTimeout: defaultSetupEncryptionClientTimeout, ClientCacheNumLocks: 100, + NewClientFunc: NewClientWithLogin, } } diff --git a/vault/client_factory_test.go b/vault/client_factory_test.go index efba2cbec..d7c454a65 100644 --- a/vault/client_factory_test.go +++ b/vault/client_factory_test.go @@ -557,6 +557,7 @@ func Test_cachingClientFactory_storageEncryptionClient(t *testing.T) { credentialProviderFactory: credentials.NewFakeCredentialProviderFactory(tt.factoryFunc), cache: clientCache, encClientSetupTimeout: tt.setupTimeout, + newClientFunc: NewClientWithLogin, } got0, err := m.storageEncryptionClient(ctx, tt.client) diff --git a/vault/client_test.go b/vault/client_test.go index d73cf6337..9c2254a71 100644 --- a/vault/client_test.go +++ b/vault/client_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/httptest" "net/url" "testing" "time" @@ -18,12 +19,15 @@ import ( "golang.org/x/crypto/blake2b" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1" + "github.com/hashicorp/vault-secrets-operator/common" "github.com/hashicorp/vault-secrets-operator/consts" + "github.com/hashicorp/vault-secrets-operator/credentials" "github.com/hashicorp/vault-secrets-operator/credentials/provider" "github.com/hashicorp/vault-secrets-operator/credentials/vault" vaultcredsconsts "github.com/hashicorp/vault-secrets-operator/credentials/vault/consts" @@ -444,7 +448,7 @@ func Test_defaultClient_Validate(t *testing.T) { watcher: &api.LifetimeWatcher{}, lastWatcherErr: nil, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.EqualError(t, err, "client token expired", i...) + return assert.ErrorContains(t, err, "client token expired", i...) }, }, { @@ -662,7 +666,7 @@ func Test_defaultClient_Read(t *testing.T) { }{ { name: "default-request", - request: NewReadRequest("foo/bar", nil), + request: NewReadRequest("foo/bar", nil, nil), handler: &testHandler{ handlerFunc: handlerFunc, }, @@ -679,7 +683,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "fail-default-nil-response", - request: NewReadRequest("foo/bar", nil), + request: NewReadRequest("foo/bar", nil, nil), handler: &testHandler{ handlerFunc: func(t *testHandler, w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) @@ -695,7 +699,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "kv-v1-request", - request: NewKVReadRequestV1("kv-v1", "secrets"), + request: NewKVReadRequestV1("kv-v1", "secrets", nil), handler: &testHandler{ handlerFunc: handlerFunc, }, @@ -712,7 +716,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "kv-v2-request", - request: NewKVReadRequestV2("kv-v2", "secrets", 0), + request: NewKVReadRequestV2("kv-v2", "secrets", 0, nil), handler: &testHandler{ handlerFunc: func(t *testHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( @@ -748,7 +752,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "kv-v2-request-with-version", - request: NewKVReadRequestV2("kv-v2", "secrets", 1), + request: NewKVReadRequestV2("kv-v2", "secrets", 1, nil), handler: &testHandler{ handlerFunc: func(t *testHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( @@ -789,7 +793,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "fail-kv-v1-nil-response", - request: NewKVReadRequestV1("kv-v1", "secrets"), + request: NewKVReadRequestV1("kv-v1", "secrets", nil), handler: &testHandler{ handlerFunc: func(t *testHandler, w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) @@ -805,7 +809,7 @@ func Test_defaultClient_Read(t *testing.T) { }, { name: "fail-kv-v2-nil-response", - request: NewKVReadRequestV2("kv-v2", "secrets", 0), + request: NewKVReadRequestV2("kv-v2", "secrets", 0, nil), handler: &testHandler{ handlerFunc: func(t *testHandler, w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) @@ -1188,7 +1192,7 @@ func TestNewClientConfigFromConnObj(t *testing.T) { connObj: connObjBase, want: &ClientConfig{ Address: "https://vault.example.com", - Headers: map[string]string{"foo": "bar"}, + Headers: http.Header{"Foo": []string{"bar"}}, TLSServerName: "baz.biff", CACertSecretRef: "ca.crt", SkipTLSVerify: true, @@ -1201,7 +1205,7 @@ func TestNewClientConfigFromConnObj(t *testing.T) { connObj: connObjEmptyTimeout, want: &ClientConfig{ Address: "https://vault.example.com", - Headers: map[string]string{"foo": "bar"}, + Headers: http.Header{"Foo": []string{"bar"}}, TLSServerName: "baz.biff", CACertSecretRef: "ca.crt", SkipTLSVerify: true, @@ -1263,3 +1267,114 @@ func Test_defaultClient_Renewable(t *testing.T) { }) } } + +func TestClient_UserAgentHeader(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userAgent string + expectedUserAgent string + }{ + { + name: "default-user-agent", + userAgent: "", + expectedUserAgent: common.DefaultVSOUserAgent, + }, + { + name: "custom-user-agent", + userAgent: "custom-client/1.0.0", + expectedUserAgent: "custom-client/1.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a mock client that captures headers + var capturedHeaders http.Header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeaders = r.Header.Clone() + // Return a successful auth response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + resp := map[string]interface{}{ + "auth": map[string]interface{}{ + "client_token": "test-token", + "accessor": "test-accessor", + "renewable": false, + "lease_duration": 3600, + }, + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + // Create test objects + vaultConnection := &secretsv1beta1.VaultConnection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-connection", + Namespace: "test-ns", + }, + Spec: secretsv1beta1.VaultConnectionSpec{ + Address: server.URL, + }, + } + + vaultAuth := &secretsv1beta1.VaultAuth{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-auth", + Namespace: "test-ns", + }, + Spec: secretsv1beta1.VaultAuthSpec{ + Method: "kubernetes", + Mount: "kubernetes", + Kubernetes: &secretsv1beta1.VaultAuthConfigKubernetes{ + Role: "test-role", + ServiceAccount: "test-sa", + }, + }, + } + + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + Namespace: "test-ns", + }, + } + + scheme := runtime.NewScheme() + require.NoError(t, secretsv1beta1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + k8sClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(vaultConnection, vaultAuth, serviceAccount). + Build() + + var opts *ClientOptions + if tt.userAgent != "" { + opts = &ClientOptions{ + UserAgent: tt.userAgent, + CredentialProviderFactory: credentials.NewCredentialProviderFactory(), + } + } + + // Create and initialize the client + client := &defaultClient{} + err := client.Init(context.Background(), k8sClient, vaultAuth, vaultConnection, "test-ns", opts) + require.NoError(t, err) + + // Perform a login to trigger HTTP request with headers + err = client.Login(context.Background(), k8sClient) + require.NoError(t, err) + + // Verify that the User-Agent header was set correctly + require.NotNil(t, capturedHeaders) + userAgentHeaders := capturedHeaders.Values(consts.HeaderUserAgent) + require.Len(t, userAgentHeaders, 1, "Expected exactly one User-Agent header") + assert.Equal(t, tt.expectedUserAgent, userAgentHeaders[0]) + }) + } +} diff --git a/vault/config.go b/vault/config.go index 721f83e11..1b3860f02 100644 --- a/vault/config.go +++ b/vault/config.go @@ -7,6 +7,7 @@ import ( "context" "crypto/x509" "fmt" + "net/http" "time" "github.com/hashicorp/vault/api" @@ -38,7 +39,7 @@ type ClientConfig struct { // VaultNamespace is the namespace in Vault to auth to VaultNamespace string // Headers are http headers to set on the Vault client - Headers map[string]string + Headers http.Header // Timeout applied to all Vault requests. If not set, the default timeout from // the Vault API client config is used. Timeout *time.Duration @@ -98,6 +99,7 @@ func MakeVaultClient(ctx context.Context, cfg *ClientConfig, client ctrlclient.C config.CloneToken = true config.CloneHeaders = true + config.CloneTLSConfig = true c, err := api.NewClient(config) if err != nil { @@ -107,8 +109,10 @@ func MakeVaultClient(ctx context.Context, cfg *ClientConfig, client ctrlclient.C if _, exists := cfg.Headers[vconsts.NamespaceHeaderName]; exists { return nil, fmt.Errorf("setting header %q on VaultConnection is not permitted", vconsts.NamespaceHeaderName) } - for k, v := range cfg.Headers { - c.AddHeader(k, v) + for k, values := range cfg.Headers { + for _, v := range values { + c.AddHeader(k, v) + } } if cfg.VaultNamespace != "" { c.SetNamespace(cfg.VaultNamespace) diff --git a/vault/config_test.go b/vault/config_test.go index d411cd214..7c0b4282e 100644 --- a/vault/config_test.go +++ b/vault/config_test.go @@ -89,9 +89,9 @@ func TestMakeVaultClient(t *testing.T) { }, "headers": { vaultConfig: &ClientConfig{ - Headers: map[string]string{ - "X-Proxy-Setting": "yes", - "Y-Proxy-Setting": "no", + Headers: http.Header{ + "X-Proxy-Setting": []string{"yes"}, + "Y-Proxy-Setting": []string{"no"}, }, VaultNamespace: "vault-test-namespace", }, @@ -100,10 +100,10 @@ func TestMakeVaultClient(t *testing.T) { }, "headers can't override namespace": { vaultConfig: &ClientConfig{ - Headers: map[string]string{ - "X-Proxy-Setting": "yes", - "Y-Proxy-Setting": "no", - vconsts.NamespaceHeaderName: "nope", + Headers: http.Header{ + "X-Proxy-Setting": []string{"yes"}, + "Y-Proxy-Setting": []string{"no"}, + vconsts.NamespaceHeaderName: []string{"nope"}, }, VaultNamespace: "vault-test-namespace", }, @@ -175,12 +175,14 @@ func TestMakeVaultClient(t *testing.T) { } } -func makeVaultHttpHeaders(t *testing.T, namespace string, headers map[string]string) http.Header { +func makeVaultHttpHeaders(t *testing.T, namespace string, headers http.Header) http.Header { t.Helper() h := make(http.Header) - for k, v := range headers { - h.Set(k, v) + for k, values := range headers { + for _, v := range values { + h.Add(k, v) + } } h.Set("X-Vault-Request", "true") if namespace != "" { diff --git a/vault/requests.go b/vault/requests.go index 1e1d2af89..975554c8e 100644 --- a/vault/requests.go +++ b/vault/requests.go @@ -4,18 +4,25 @@ package vault import ( + "maps" + "net/http" "net/url" "strconv" ) -type ReadRequest interface { +type BaseRequest interface { Path() string + Headers() http.Header +} + +type ReadRequest interface { + BaseRequest Values() url.Values } type WriteRequest interface { - Path() string - Params() map[string]any + BaseRequest + Data() map[string]any } var ( @@ -26,21 +33,32 @@ var ( ) type defaultWriteRequest struct { - path string - params map[string]any + path string + params map[string]any + headers http.Header + data []byte +} + +func (r *defaultWriteRequest) Headers() http.Header { + return maps.Clone(r.headers) } func (r *defaultWriteRequest) Path() string { return r.path } -func (r *defaultWriteRequest) Params() map[string]any { +func (r *defaultWriteRequest) Data() map[string]any { return r.params } type defaultReadRequest struct { - path string - values url.Values + path string + values url.Values + headers http.Header +} + +func (r *defaultReadRequest) Headers() http.Header { + return maps.Clone(r.headers) } func (r *defaultReadRequest) Path() string { @@ -54,8 +72,13 @@ func (r *defaultReadRequest) Values() url.Values { // kvReadRequestV1 can be used in ClientBase.Read to get KV version 1 secrets // from Vault. type kvReadRequestV1 struct { - mount string - path string + mount string + path string + headers http.Header +} + +func (r *kvReadRequestV1) Headers() http.Header { + return maps.Clone(r.headers) } func (r *kvReadRequestV1) Path() string { @@ -72,6 +95,11 @@ type kvReadRequestV2 struct { mount string path string version int + headers http.Header +} + +func (r *kvReadRequestV2) Headers() http.Header { + return maps.Clone(r.headers) } func (r *kvReadRequestV2) Path() string { @@ -89,31 +117,35 @@ func (r *kvReadRequestV2) Values() url.Values { return vals } -func NewKVReadRequestV1(mount, path string) ReadRequest { +func NewKVReadRequestV1(mount, path string, headers http.Header) ReadRequest { return &kvReadRequestV1{ - mount: mount, - path: path, + mount: mount, + path: path, + headers: headers, } } -func NewKVReadRequestV2(mount, path string, version int) ReadRequest { +func NewKVReadRequestV2(mount, path string, version int, headers http.Header) ReadRequest { return &kvReadRequestV2{ mount: mount, path: path, + headers: headers, version: version, } } -func NewReadRequest(path string, values url.Values) ReadRequest { +func NewReadRequest(path string, values url.Values, headers http.Header) ReadRequest { return &defaultReadRequest{ - path: path, - values: values, + path: path, + values: values, + headers: headers, } } -func NewWriteRequest(path string, params map[string]any) WriteRequest { +func NewWriteRequest(path string, params map[string]any, headers http.Header) WriteRequest { return &defaultWriteRequest{ - path: path, - params: params, + path: path, + params: params, + headers: headers, } } diff --git a/vault/requests_test.go b/vault/requests_test.go index 3cebb8459..460725db4 100644 --- a/vault/requests_test.go +++ b/vault/requests_test.go @@ -195,7 +195,7 @@ func Test_defaultWriteRequest_Params(t *testing.T) { r := &defaultWriteRequest{ params: tt.params, } - assert.Equalf(t, tt.want, r.Params(), "Params()") + assert.Equalf(t, tt.want, r.Data(), "Data()") }) } } diff --git a/vault/responses.go b/vault/responses.go index 6709cc0cd..c74192905 100644 --- a/vault/responses.go +++ b/vault/responses.go @@ -4,6 +4,7 @@ package vault import ( + "encoding/json" "errors" "fmt" "net/http" @@ -16,11 +17,13 @@ import ( var ( _ Response = (*defaultResponse)(nil) _ Response = (*kvV2Response)(nil) + _ Response = (*kvV1Response)(nil) ) type Response interface { Secret() *api.Secret Data() map[string]any + WrapInfo() *api.SecretWrapInfo SecretK8sData(*helpers.SecretTransformationOption) (map[string][]byte, error) } @@ -28,13 +31,31 @@ type defaultResponse struct { secret *api.Secret } +func (r *defaultResponse) WrapInfo() *api.SecretWrapInfo { + if r.secret != nil { + return r.secret.WrapInfo + } + return nil +} + func (r *defaultResponse) SecretK8sData(opt *helpers.SecretTransformationOption) (map[string][]byte, error) { var rawData map[string]interface{} if r.secret != nil { rawData = r.secret.Data } - return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, opt) + var wrapData map[string]any + if wrapInfo := r.WrapInfo(); wrapInfo != nil { + b, err := json.Marshal(wrapInfo) + if err != nil { + return nil, fmt.Errorf("failed to marshal wrap info: %w", err) + } + if err := json.Unmarshal(b, &wrapData); err != nil { + return nil, fmt.Errorf("failed to unmarshal wrap info: %w", err) + } + } + + return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, wrapData, opt) } func (r *defaultResponse) Secret() *api.Secret { @@ -53,6 +74,10 @@ type kvV1Response struct { secret *api.Secret } +func (r *kvV1Response) WrapInfo() *api.SecretWrapInfo { + return nil +} + func (r *kvV1Response) SecretK8sData(opt *helpers.SecretTransformationOption) (map[string][]byte, error) { var rawData map[string]interface{} if r.secret != nil { @@ -63,7 +88,7 @@ func (r *kvV1Response) SecretK8sData(opt *helpers.SecretTransformationOption) (m return nil, fmt.Errorf("raw portion of vault KV secret was nil") } - return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, opt) + return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, nil, opt) } func (r *kvV1Response) Secret() *api.Secret { @@ -82,6 +107,10 @@ type kvV2Response struct { secret *api.Secret } +func (r *kvV2Response) WrapInfo() *api.SecretWrapInfo { + return nil +} + func (r *kvV2Response) SecretK8sData(opt *helpers.SecretTransformationOption) (map[string][]byte, error) { var rawData map[string]interface{} if r.secret != nil { @@ -92,7 +121,7 @@ func (r *kvV2Response) SecretK8sData(opt *helpers.SecretTransformationOption) (m return nil, fmt.Errorf("raw portion of vault KV secret was nil") } - return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, opt) + return helpers.NewSecretsDataBuilder().WithVaultData(r.Data(), rawData, nil, opt) } func (r *kvV2Response) Secret() *api.Secret { diff --git a/vault/responses_test.go b/vault/responses_test.go index 15660c12f..39e01a458 100644 --- a/vault/responses_test.go +++ b/vault/responses_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" @@ -29,6 +30,13 @@ type testResponseData struct { want map[string]interface{} } +type testResponseWrapInfo struct { + name string + respFunc func(tt testResponseWrapInfo) Response + secret *api.Secret + want *api.SecretWrapInfo +} + type testResponseSecretK8sData struct { name string respFunc func(tt testResponseSecretK8sData) Response @@ -232,6 +240,55 @@ func Test_defaultResponse_SecretK8sData(t *testing.T) { } } +func Test_defaultResponse_WrapInfo(t *testing.T) { + t.Parallel() + + respFunc := func(tt testResponseWrapInfo) Response { + return &defaultResponse{ + secret: tt.secret, + } + } + + ts := time.Now().UTC() + tests := []testResponseWrapInfo{ + { + name: "basic", + secret: &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "bar": "baz", + }, + }, + WrapInfo: &api.SecretWrapInfo{ + Token: "1234546", + Accessor: "some-accessor", + TTL: 0, + CreationTime: ts, + CreationPath: "foo/bar", + WrappedAccessor: "some-wrapped-accessor", + }, + }, + respFunc: respFunc, + want: &api.SecretWrapInfo{ + Token: "1234546", + Accessor: "some-accessor", + TTL: 0, + CreationTime: ts, + CreationPath: "foo/bar", + WrappedAccessor: "some-wrapped-accessor", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt := tt + t.Parallel() + + assertResponseWrapInfo(t, tt) + }) + } +} + func Test_kvV1Response_Data(t *testing.T) { t.Parallel() @@ -455,6 +512,48 @@ func Test_kvV1Response_SecretK8sData(t *testing.T) { } } +func Test_kv1Response_WrapInfo(t *testing.T) { + t.Parallel() + + respFunc := func(tt testResponseWrapInfo) Response { + return &kvV1Response{ + secret: tt.secret, + } + } + + ts := time.Now().UTC() + tests := []testResponseWrapInfo{ + { + name: "basic", + secret: &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "bar": "baz", + }, + }, + WrapInfo: &api.SecretWrapInfo{ + Token: "1234546", + Accessor: "some-accessor", + TTL: 0, + CreationTime: ts, + CreationPath: "foo/bar", + WrappedAccessor: "some-wrapped-accessor", + }, + }, + respFunc: respFunc, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt := tt + t.Parallel() + + assertResponseWrapInfo(t, tt) + }) + } +} + func Test_kvV2Response_Data(t *testing.T) { t.Parallel() @@ -678,6 +777,48 @@ func Test_kvV2Response_SecretK8sData(t *testing.T) { } } +func Test_kv2Response_WrapInfo(t *testing.T) { + t.Parallel() + + respFunc := func(tt testResponseWrapInfo) Response { + return &kvV2Response{ + secret: tt.secret, + } + } + + ts := time.Now().UTC() + tests := []testResponseWrapInfo{ + { + name: "basic", + secret: &api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "bar": "baz", + }, + }, + WrapInfo: &api.SecretWrapInfo{ + Token: "1234546", + Accessor: "some-accessor", + TTL: 0, + CreationTime: ts, + CreationPath: "foo/bar", + WrappedAccessor: "some-wrapped-accessor", + }, + }, + respFunc: respFunc, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt := tt + t.Parallel() + + assertResponseWrapInfo(t, tt) + }) + } +} + func TestIsLeaseNotFoundError(t *testing.T) { t.Parallel() @@ -773,6 +914,12 @@ func assertResponseData(t *testing.T, tt testResponseData) { assert.Equalf(t, tt.want, resp.Data(), "Data()") } +func assertResponseWrapInfo(t *testing.T, tt testResponseWrapInfo) { + t.Helper() + resp := tt.respFunc(tt) + assert.Equalf(t, tt.want, resp.WrapInfo(), "WrapInfo()") +} + func assertResponseSecret(t *testing.T, tt testResponseSecret) { t.Helper() resp := tt.respFunc(tt) diff --git a/vault/transit.go b/vault/transit.go index 66a8110fb..285bff190 100644 --- a/vault/transit.go +++ b/vault/transit.go @@ -29,7 +29,7 @@ func EncryptWithTransit(ctx context.Context, vaultClient Client, mount, key stri resp, err := vaultClient.Write(ctx, NewWriteRequest(path, map[string]any{ "name": key, "plaintext": base64.StdEncoding.EncodeToString(data), - }), + }, nil), ) if err != nil { return nil, err @@ -55,7 +55,7 @@ func DecryptWithTransit(ctx context.Context, vaultClient Client, mount, key stri "ciphertext": v.Ciphertext, } - resp, err := vaultClient.Write(ctx, NewWriteRequest(path, params)) + resp, err := vaultClient.Write(ctx, NewWriteRequest(path, params, nil)) if err != nil { return nil, err }