diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod index ca1539ed14e..ab5b40e92f6 100644 --- a/pkg/apis/secret/go.mod +++ b/pkg/apis/secret/go.mod @@ -11,6 +11,7 @@ require ( k8s.io/apimachinery v0.32.3 k8s.io/apiserver v0.32.3 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 ) require ( @@ -94,7 +95,6 @@ require ( k8s.io/client-go v0.32.3 // indirect k8s.io/component-base v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/pkg/apis/secret/v0alpha1/keeper.go b/pkg/apis/secret/v0alpha1/keeper.go index d45e481db3a..f9535defa91 100644 --- a/pkg/apis/secret/v0alpha1/keeper.go +++ b/pkg/apis/secret/v0alpha1/keeper.go @@ -16,26 +16,52 @@ type Keeper struct { // This is the actual keeper schema. // +patchStrategy=replace // +patchMergeKey=name - Spec KeeperSpec `json:"spec,omitempty" patchStrategy:"replace" patchMergeKey:"name"` + Spec KeeperSpec `json:"spec" patchStrategy:"replace" patchMergeKey:"name"` } -func (k *Keeper) IsSqlKeeper() bool { - return k.Spec.SQL != nil && k.Spec.SQL.Encryption != nil +// KeeperType represents the type of a Keeper. +type KeeperType string + +const ( + AWSKeeperType KeeperType = "aws" + AzureKeeperType KeeperType = "azure" + GCPKeeperType KeeperType = "gcp" + HashiCorpKeeperType KeeperType = "hashicorp" +) + +func (kt KeeperType) String() string { + return string(kt) } +// KeeperConfig is an interface that all keeper config types must implement. type KeeperConfig interface { - Type() string + Type() KeeperType } type KeeperSpec struct { - // Human friendly name for the keeper. - Title string `json:"title"` + // Short description for the Keeper. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + Description string `json:"description"` - // You can only chose one of the following. - SQL *SQLKeeperConfig `json:"sql,omitempty"` - AWS *AWSKeeperConfig `json:"aws,omitempty"` - Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"` - GCP *GCPKeeperConfig `json:"gcp,omitempty"` + // AWS Keeper Configuration. + // +structType=atomic + // +optional + AWS *AWSKeeperConfig `json:"aws,omitempty"` + + // Azure Keeper Configuration. + // +structType=atomic + // +optional + Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"` + + // GCP Keeper Configuration. + // +structType=atomic + // +optional + GCP *GCPKeeperConfig `json:"gcp,omitempty"` + + // HashiCorp Vault Keeper Configuration. + // +structType=atomic + // +optional HashiCorp *HashiCorpKeeperConfig `json:"hashivault,omitempty"` } @@ -52,25 +78,6 @@ type KeeperList struct { Items []Keeper `json:"items,omitempty"` } -// The default SQL keeper. -type SQLKeeperConfig struct { - Encryption *Encryption `json:"encryption,omitempty"` -} - -func (s *SQLKeeperConfig) Type() string { - return "sql" -} - -// Encryption of default SQL keeper. -type Encryption struct { - Envelope *Envelope `json:"envelope,omitempty"` // TODO: what would this be - - AWS *AWSCredentials `json:"aws,omitempty"` - Azure *AzureCredentials `json:"azure,omitempty"` - GCP *GCPCredentials `json:"gcp,omitempty"` - HashiCorp *HashiCorpCredentials `json:"hashicorp,omitempty"` -} - // Credentials of remote keepers. type AWSCredentials struct { AccessKeyID CredentialValue `json:"accessKeyId"` @@ -99,15 +106,19 @@ type HashiCorpCredentials struct { type Envelope struct{} // Holds the way credentials are obtained. +// +union type CredentialValue struct { // The name of the secure value that holds the actual value. + // +optional SecureValueName string `json:"secureValueName,omitempty"` // The value is taken from the environment variable. + // +optional ValueFromEnv string `json:"valueFromEnv,omitempty"` // The value is taken from the Grafana config file. // TODO: how do we explain that this is a path to the config file? + // +optional ValueFromConfig string `json:"valueFromConfig,omitempty"` } @@ -128,18 +139,18 @@ type HashiCorpKeeperConfig struct { HashiCorpCredentials `json:",inline"` } -func (s *AWSKeeperConfig) Type() string { - return "aws" +func (s *AWSKeeperConfig) Type() KeeperType { + return AWSKeeperType } -func (s *AzureKeeperConfig) Type() string { - return "azure" +func (s *AzureKeeperConfig) Type() KeeperType { + return AzureKeeperType } -func (s *GCPKeeperConfig) Type() string { - return "gcp" +func (s *GCPKeeperConfig) Type() KeeperType { + return GCPKeeperType } -func (s *HashiCorpKeeperConfig) Type() string { - return "hashicorp" +func (s *HashiCorpKeeperConfig) Type() KeeperType { + return HashiCorpKeeperType } diff --git a/pkg/apis/secret/v0alpha1/register.go b/pkg/apis/secret/v0alpha1/register.go index e2d7c065487..f31ab882212 100644 --- a/pkg/apis/secret/v0alpha1/register.go +++ b/pkg/apis/secret/v0alpha1/register.go @@ -30,7 +30,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo( // This defines the fields we view in `kubectl get`. Not related with the storage layer. Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "The display name of the secure value"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, {Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"}, @@ -41,7 +41,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo( if ok { return []interface{}{ r.Name, - r.Spec.Title, + r.Spec.Description, r.Spec.Keeper, r.Spec.Ref, r.Status.Phase, @@ -65,7 +65,7 @@ var KeeperResourceInfo = utils.NewResourceInfo( // This defines the fields we view in `kubectl get`. Not related with the storage layer. Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "The display name of the keeper"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"}, }, // Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`. Reader: func(obj any) ([]interface{}, error) { @@ -73,7 +73,7 @@ var KeeperResourceInfo = utils.NewResourceInfo( if ok { return []interface{}{ r.Name, - r.Spec.Title, + r.Spec.Description, }, nil } diff --git a/pkg/apis/secret/v0alpha1/secure_value.go b/pkg/apis/secret/v0alpha1/secure_value.go index 9e476f298a7..854faeaee4c 100644 --- a/pkg/apis/secret/v0alpha1/secure_value.go +++ b/pkg/apis/secret/v0alpha1/secure_value.go @@ -14,11 +14,11 @@ type SecureValue struct { metav1.ObjectMeta `json:"metadata,omitempty"` // This is the actual secure value schema. - Spec SecureValueSpec `json:"spec,omitempty"` + Spec SecureValueSpec `json:"spec"` // Read-only observed status of the `SecureValue`. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status - Status SecureValueStatus `json:"status"` + Status SecureValueStatus `json:"status,omitempty"` } // +enum @@ -46,32 +46,40 @@ type SecureValueStatus struct { // Only applicable if the `phase=Failed`. // +optional Message string `json:"message,omitempty"` + + // +optional + ExternalID string `json:"externalId,omitempty"` } type SecureValueSpec struct { - // Human friendly name for the secure value. - Title string `json:"title"` + // Short description that explains the purpose of this SecureValue. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + Description string `json:"description"` // The raw value is only valid for write. Read/List will always be empty. // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // +k8s:validation:minLength=1 Value ExposedSecureValue `json:"value,omitempty"` - // When using a remote Key manager, the ref is used to reference a value inside the remote storage. + // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. // This should not contain sensitive information. - Ref string `json:"ref,omitempty"` + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=1024 + // +optional + Ref *string `json:"ref,omitempty"` // Name of the keeper, being the actual storage of the secure value. - Keeper string `json:"keeper,omitempty"` + // If not specified, the default keeper for the namespace will be used. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + // +optional + Keeper *string `json:"keeper,omitempty"` // The Decrypters that are allowed to decrypt this secret. // An empty list means no service can decrypt it. - // Support and behavior is still TBD, but could likely look like: - // * testdata.grafana.app/{name1} - // * testdata.grafana.app/{name2} - // * runner.k6.grafana.app/* -- allow any k6 test runner - // Rather than a string pattern, we may want a more explicit object: - // [{ group:"testdata.grafana.app", name="name1"}, - // { group:"runner.k6.grafana.app"}] + // +k8s:validation:maxItems=64 + // +k8s:validation:uniqueItems=true // +listType=atomic // +optional Decrypters []string `json:"decrypters"` diff --git a/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go index 96491535452..49975b17610 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go @@ -96,47 +96,6 @@ func (in *CredentialValue) DeepCopy() *CredentialValue { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Encryption) DeepCopyInto(out *Encryption) { - *out = *in - if in.Envelope != nil { - in, out := &in.Envelope, &out.Envelope - *out = new(Envelope) - **out = **in - } - if in.AWS != nil { - in, out := &in.AWS, &out.AWS - *out = new(AWSCredentials) - **out = **in - } - if in.Azure != nil { - in, out := &in.Azure, &out.Azure - *out = new(AzureCredentials) - **out = **in - } - if in.GCP != nil { - in, out := &in.GCP, &out.GCP - *out = new(GCPCredentials) - **out = **in - } - if in.HashiCorp != nil { - in, out := &in.HashiCorp, &out.HashiCorp - *out = new(HashiCorpCredentials) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Encryption. -func (in *Encryption) DeepCopy() *Encryption { - if in == nil { - return nil - } - out := new(Encryption) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Envelope) DeepCopyInto(out *Envelope) { *out = *in @@ -283,11 +242,6 @@ func (in *KeeperList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeeperSpec) DeepCopyInto(out *KeeperSpec) { *out = *in - if in.SQL != nil { - in, out := &in.SQL, &out.SQL - *out = new(SQLKeeperConfig) - (*in).DeepCopyInto(*out) - } if in.AWS != nil { in, out := &in.AWS, &out.AWS *out = new(AWSKeeperConfig) @@ -321,27 +275,6 @@ func (in *KeeperSpec) DeepCopy() *KeeperSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SQLKeeperConfig) DeepCopyInto(out *SQLKeeperConfig) { - *out = *in - if in.Encryption != nil { - in, out := &in.Encryption, &out.Encryption - *out = new(Encryption) - (*in).DeepCopyInto(*out) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLKeeperConfig. -func (in *SQLKeeperConfig) DeepCopy() *SQLKeeperConfig { - if in == nil { - return nil - } - out := new(SQLKeeperConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecureValue) DeepCopyInto(out *SecureValue) { *out = *in @@ -406,6 +339,16 @@ func (in *SecureValueList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecureValueSpec) DeepCopyInto(out *SecureValueSpec) { *out = *in + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(string) + **out = **in + } + if in.Keeper != nil { + in, out := &in.Keeper, &out.Keeper + *out = new(string) + **out = **in + } if in.Decrypters != nil { in, out := &in.Decrypters, &out.Decrypters *out = make([]string, len(*in)) diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go index dab5ab27215..b1dbf444965 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go @@ -10,6 +10,7 @@ package v0alpha1 import ( common "k8s.io/kube-openapi/pkg/common" spec "k8s.io/kube-openapi/pkg/validation/spec" + ptr "k8s.io/utils/ptr" ) func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { @@ -19,7 +20,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials": schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig": schema_pkg_apis_secret_v0alpha1_AzureKeeperConfig(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue": schema_pkg_apis_secret_v0alpha1_CredentialValue(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption": schema_pkg_apis_secret_v0alpha1_Encryption(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope": schema_pkg_apis_secret_v0alpha1_Envelope(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials": schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig": schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(ref), @@ -28,7 +28,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper": schema_pkg_apis_secret_v0alpha1_Keeper(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperList": schema_pkg_apis_secret_v0alpha1_KeeperList(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec": schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig": schema_pkg_apis_secret_v0alpha1_SQLKeeperConfig(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValue": schema_pkg_apis_secret_v0alpha1_SecureValue(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueList": schema_pkg_apis_secret_v0alpha1_SecureValueList(ref), "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec": schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref), @@ -218,47 +217,20 @@ func schema_pkg_apis_secret_v0alpha1_CredentialValue(ref common.ReferenceCallbac }, }, }, - }, - } -} - -func schema_pkg_apis_secret_v0alpha1_Encryption(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Encryption of default SQL keeper.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "envelope": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope"), - }, - }, - "aws": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSCredentials"), - }, - }, - "azure": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials"), - }, - }, - "gcp": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials"), - }, - }, - "hashicorp": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials"), + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-unions": []interface{}{ + map[string]interface{}{ + "fields-to-discriminateBy": map[string]interface{}{ + "secureValueName": "SecureValueName", + "valueFromConfig": "ValueFromConfig", + "valueFromEnv": "ValueFromEnv", + }, }, }, }, }, }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials"}, } } @@ -424,6 +396,7 @@ func schema_pkg_apis_secret_v0alpha1_Keeper(ref common.ReferenceCallback) common }, }, }, + Required: []string{"spec"}, }, }, Dependencies: []string{ @@ -486,66 +459,66 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "title": { + "description": { SchemaProps: spec.SchemaProps{ - Description: "Human friendly name for the keeper.", + Description: "Short description for the Keeper.", Default: "", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](253), Type: []string{"string"}, Format: "", }, }, - "sql": { - SchemaProps: spec.SchemaProps{ - Description: "You can only chose one of the following.", - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig"), - }, - }, "aws": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig"), + Description: "AWS Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig"), }, }, "azurekeyvault": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig"), + Description: "Azure Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig"), }, }, "gcp": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig"), + Description: "GCP Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig"), }, }, "hashivault": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"), + Description: "HashiCorp Vault Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"), }, }, }, - Required: []string{"title"}, + Required: []string{"description"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_SQLKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "The default SQL keeper.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "encryption": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption"}, + "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"}, } } @@ -591,7 +564,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValue(ref common.ReferenceCallback) c }, }, }, - Required: []string{"status"}, + Required: []string{"spec"}, }, }, Dependencies: []string{ @@ -654,10 +627,12 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "title": { + "description": { SchemaProps: spec.SchemaProps{ - Description: "Human friendly name for the secure value.", + Description: "Short description that explains the purpose of this SecureValue.", Default: "", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](253), Type: []string{"string"}, Format: "", }, @@ -665,20 +640,25 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac "value": { SchemaProps: spec.SchemaProps{ Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.", + MinLength: ptr.To[int64](1), Type: []string{"string"}, Format: "", }, }, "ref": { SchemaProps: spec.SchemaProps{ - Description: "When using a remote Key manager, the ref is used to reference a value inside the remote storage. This should not contain sensitive information.", + Description: "When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. This should not contain sensitive information.", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](1024), Type: []string{"string"}, Format: "", }, }, "keeper": { SchemaProps: spec.SchemaProps{ - Description: "Name of the keeper, being the actual storage of the secure value.", + Description: "Name of the keeper, being the actual storage of the secure value. If not specified, the default keeper for the namespace will be used.", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](253), Type: []string{"string"}, Format: "", }, @@ -690,7 +670,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac }, }, SchemaProps: spec.SchemaProps{ - Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it. Support and behavior is still TBD, but could likely look like: * testdata.grafana.app/{name1} * testdata.grafana.app/{name2} * runner.k6.grafana.app/* -- allow any k6 test runner Rather than a string pattern, we may want a more explicit object: [{ group:\"testdata.grafana.app\", name=\"name1\"},\n { group:\"runner.k6.grafana.app\"}]", + Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it.", + MaxItems: ptr.To[int64](64), + UniqueItems: true, Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ @@ -704,7 +686,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac }, }, }, - Required: []string{"title"}, + Required: []string{"description"}, }, }, } @@ -732,6 +714,12 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb Format: "", }, }, + "externalId": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"phase"}, }, diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list index 48af67bb3b5..679e6938ae0 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -2,7 +2,7 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AWSCredentials,KMSKeyID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,ClientID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,TenantID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,Encryption,HashiCorp API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,GCPCredentials,ProjectID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID diff --git a/pkg/registry/apis/secret/reststorage/keeper_rest.go b/pkg/registry/apis/secret/reststorage/keeper_rest.go index 77926723e78..4037d57f33e 100644 --- a/pkg/registry/apis/secret/reststorage/keeper_rest.go +++ b/pkg/registry/apis/secret/reststorage/keeper_rest.go @@ -209,8 +209,8 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation errs := make(field.ErrorList, 0) - if keeper.Spec.Title == "" { - errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) + if keeper.Spec.Description == "" { + errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required")) } // Only one keeper type can be configured. Return early and don't validate the specific keeper fields. @@ -220,28 +220,6 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation return errs } - // TODO: Improve SQL keeper validation. - // SQL keeper is not allowed to use `secureValueName` in credentials fields to avoid depending on another keeper. - if keeper.IsSqlKeeper() { - if keeper.Spec.SQL.Encryption.AWS != nil { - if keeper.Spec.SQL.Encryption.AWS.AccessKeyID.SecureValueName != "" { - errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "accessKeyId"), "secureValueName cannot be used with SQL keeper")) - } - - if keeper.Spec.SQL.Encryption.AWS.SecretAccessKey.SecureValueName != "" { - errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "secretAccessKey"), "secureValueName cannot be used with SQL keeper")) - } - } - - if keeper.Spec.SQL.Encryption.Azure != nil && keeper.Spec.SQL.Encryption.Azure.ClientSecret.SecureValueName != "" { - errs = append(errs, field.Forbidden(field.NewPath("spec", "azure", "clientSecret"), "secureValueName cannot be used with SQL keeper")) - } - - if keeper.Spec.SQL.Encryption.HashiCorp != nil && keeper.Spec.SQL.Encryption.HashiCorp.Token.SecureValueName != "" { - errs = append(errs, field.Forbidden(field.NewPath("spec", "hashicorp", "token"), "secureValueName cannot be used with SQL keeper")) - } - } - if keeper.Spec.AWS != nil { if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyId"), keeper.Spec.AWS.AccessKeyID); err != nil { errs = append(errs, err) @@ -295,7 +273,6 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error { availableKeepers := map[string]bool{ - "sql": keeper.Spec.SQL != nil, "aws": keeper.Spec.AWS != nil, "azure": keeper.Spec.Azure != nil, "gcp": keeper.Spec.GCP != nil, diff --git a/pkg/registry/apis/secret/reststorage/keeper_rest_test.go b/pkg/registry/apis/secret/reststorage/keeper_rest_test.go index abc1ac52b63..0191b79458d 100644 --- a/pkg/registry/apis/secret/reststorage/keeper_rest_test.go +++ b/pkg/registry/apis/secret/reststorage/keeper_rest_test.go @@ -10,28 +10,33 @@ import ( func TestValidateKeeper(t *testing.T) { t.Run("when creating a new keeper", func(t *testing.T) { - t.Run("the `title` must be present", func(t *testing.T) { + t.Run("the `description` must be present", func(t *testing.T) { keeper := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - SQL: &secretv0alpha1.SQLKeeperConfig{}, + AWS: &secretv0alpha1.AWSKeeperConfig{ + AWSCredentials: secretv0alpha1.AWSCredentials{ + AccessKeyID: secretv0alpha1.CredentialValue{ValueFromEnv: "some-value"}, + SecretAccessKey: secretv0alpha1.CredentialValue{ValueFromEnv: "some-value"}, + KMSKeyID: "kms-key-id", + }, + }, }, } errs := ValidateKeeper(keeper, admission.Create) require.Len(t, errs, 1) - require.Equal(t, "spec.title", errs[0].Field) + require.Equal(t, "spec.description", errs[0].Field) }) }) t.Run("only one `keeper` must be present", func(t *testing.T) { keeper := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", - SQL: &secretv0alpha1.SQLKeeperConfig{}, - AWS: &secretv0alpha1.AWSKeeperConfig{}, - Azure: &secretv0alpha1.AzureKeeperConfig{}, - GCP: &secretv0alpha1.GCPKeeperConfig{}, - HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{}, + Description: "short description", + AWS: &secretv0alpha1.AWSKeeperConfig{}, + Azure: &secretv0alpha1.AzureKeeperConfig{}, + GCP: &secretv0alpha1.GCPKeeperConfig{}, + HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{}, }, } @@ -43,7 +48,7 @@ func TestValidateKeeper(t *testing.T) { t.Run("at least one `keeper` must be present", func(t *testing.T) { keeper := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", + Description: "description", }, } @@ -55,7 +60,7 @@ func TestValidateKeeper(t *testing.T) { t.Run("aws keeper validation", func(t *testing.T) { validKeeperAWS := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", + Description: "description", AWS: &secretv0alpha1.AWSKeeperConfig{ AWSCredentials: secretv0alpha1.AWSCredentials{ AccessKeyID: secretv0alpha1.CredentialValue{ @@ -122,7 +127,7 @@ func TestValidateKeeper(t *testing.T) { t.Run("azure keeper validation", func(t *testing.T) { validKeeperAzure := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", + Description: "description", Azure: &secretv0alpha1.AzureKeeperConfig{ AzureCredentials: secretv0alpha1.AzureCredentials{ KeyVaultName: "kv-name", @@ -191,7 +196,7 @@ func TestValidateKeeper(t *testing.T) { t.Run("gcp keeper validation", func(t *testing.T) { validKeeperGCP := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", + Description: "description", GCP: &secretv0alpha1.GCPKeeperConfig{ GCPCredentials: secretv0alpha1.GCPCredentials{ ProjectID: "project-id", @@ -223,7 +228,7 @@ func TestValidateKeeper(t *testing.T) { t.Run("hashicorp keeper validation", func(t *testing.T) { validKeeperHashiCorp := &secretv0alpha1.Keeper{ Spec: secretv0alpha1.KeeperSpec{ - Title: "title", + Description: "description", HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{ HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{ Address: "http://address", @@ -268,65 +273,4 @@ func TestValidateKeeper(t *testing.T) { }) }) }) - - t.Run("sql keeper validation", func(t *testing.T) { - t.Run("does not allow usage of `secureValueName` in credentials", func(t *testing.T) { - providers := []struct { - name string - enc secretv0alpha1.Encryption - expectedErrors int - }{ - { - name: "aws", - enc: secretv0alpha1.Encryption{ - AWS: &secretv0alpha1.AWSCredentials{ - AccessKeyID: secretv0alpha1.CredentialValue{ - SecureValueName: "not-empty", - }, - SecretAccessKey: secretv0alpha1.CredentialValue{ - SecureValueName: "not-empty", - }, - }, - }, - expectedErrors: 2, - }, - { - name: "azure", - enc: secretv0alpha1.Encryption{ - Azure: &secretv0alpha1.AzureCredentials{ - ClientSecret: secretv0alpha1.CredentialValue{ - SecureValueName: "not-empty", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "hashicorp", - enc: secretv0alpha1.Encryption{ - HashiCorp: &secretv0alpha1.HashiCorpCredentials{ - Token: secretv0alpha1.CredentialValue{ - SecureValueName: "not-empty", - }, - }, - }, - expectedErrors: 1, - }, - } - - for _, tc := range providers { - t.Run("when using credentials for "+tc.name, func(t *testing.T) { - keeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ - Title: "title", - SQL: &secretv0alpha1.SQLKeeperConfig{Encryption: &tc.enc}, - }, - } - - errs := ValidateKeeper(keeper, admission.Create) - require.Len(t, errs, tc.expectedErrors) - }) - } - }) - }) } diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest.go b/pkg/registry/apis/secret/reststorage/secure_value_rest.go index f0b043108e9..888398926a9 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest.go @@ -218,19 +218,15 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss func validateSecureValueCreate(sv *secretv0alpha1.SecureValue) field.ErrorList { errs := make(field.ErrorList, 0) - if sv.Spec.Title == "" { - errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) + if sv.Spec.Description == "" { + errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required")) } - if sv.Spec.Keeper == "" { - errs = append(errs, field.Required(field.NewPath("spec", "keeper"), "a `keeper` is required")) - } - - if sv.Spec.Value == "" && sv.Spec.Ref == "" { + if sv.Spec.Value == "" && (sv.Spec.Ref == nil || (sv.Spec.Ref != nil && *sv.Spec.Ref == "")) { errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required")) } - if sv.Spec.Value != "" && sv.Spec.Ref != "" { + if sv.Spec.Value != "" && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { errs = append(errs, field.Forbidden(field.NewPath("spec"), "only one of `value` or `ref` can be set")) } @@ -249,12 +245,12 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro } // Only validate if one of the fields is being changed/set. - if sv.Spec.Value != "" || sv.Spec.Ref != "" { - if oldSv.Spec.Ref != "" && sv.Spec.Value != "" { + if sv.Spec.Value != "" || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { + if (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref != "") && sv.Spec.Value != "" { errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set")) } - if oldSv.Spec.Ref == "" && sv.Spec.Ref != "" { + if (oldSv.Spec.Ref == nil || (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref == "")) && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `ref` when `value` was already previously set")) } } @@ -271,6 +267,17 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList { errs := make(field.ErrorList, 0) + // Limit the number of decrypters to 64 to not have it unbounded. + // The number was chosen arbitrarily and should be enough. + if len(decrypters) > 64 { + errs = append( + errs, + field.TooMany(field.NewPath("spec", "decrypters"), len(decrypters), 64), + ) + + return errs + } + decrypterNames := make(map[string]struct{}, 0) for i, decrypter := range decrypters { diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go index 3002be94706..113faf6f0aa 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go @@ -14,44 +14,37 @@ import ( func TestValidateSecureValue(t *testing.T) { t.Run("when creating a new securevalue", func(t *testing.T) { + keeper := "keeper" validSecureValue := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", - Value: "value", - Keeper: "keeper", - Decrypters: []string{"actor_app1", "actor_app2"}, + Description: "description", + Value: "value", + Keeper: &keeper, + Decrypters: []string{"actor_app1", "actor_app2"}, }, } - t.Run("the `title` must be present", func(t *testing.T) { + t.Run("the `description` must be present", func(t *testing.T) { sv := validSecureValue.DeepCopy() - sv.Spec.Title = "" + sv.Spec.Description = "" errs := ValidateSecureValue(sv, nil, admission.Create, nil) require.Len(t, errs, 1) - require.Equal(t, "spec.title", errs[0].Field) - }) - - t.Run("the `keeper` must be present", func(t *testing.T) { - sv := validSecureValue.DeepCopy() - sv.Spec.Keeper = "" - - errs := ValidateSecureValue(sv, nil, admission.Create, nil) - require.Len(t, errs, 1) - require.Equal(t, "spec.keeper", errs[0].Field) + require.Equal(t, "spec.description", errs[0].Field) }) t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) { sv := validSecureValue.DeepCopy() sv.Spec.Value = "" - sv.Spec.Ref = "" + sv.Spec.Ref = nil errs := ValidateSecureValue(sv, nil, admission.Create, nil) require.Len(t, errs, 1) require.Equal(t, "spec", errs[0].Field) + ref := "value" sv.Spec.Value = "value" - sv.Spec.Ref = "value" + sv.Spec.Ref = &ref errs = ValidateSecureValue(sv, nil, admission.Create, nil) require.Len(t, errs, 1) @@ -63,13 +56,14 @@ func TestValidateSecureValue(t *testing.T) { t.Run("when trying to switch from a `value` (old) to a `ref` (new), it returns an error", func(t *testing.T) { oldSv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Ref: "", // empty `ref` means a `value` was present. + Ref: nil, // empty `ref` means a `value` was present. }, } + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Ref: "ref", + Ref: &ref, }, } @@ -79,9 +73,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("when trying to switch from a `ref` (old) to a `value` (new), it returns an error", func(t *testing.T) { + ref := "non-empty" oldSv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Ref: "non-empty", + Ref: &ref, }, } @@ -97,16 +92,18 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) { + refNonEmpty := "non-empty" oldSv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Ref: "non-empty", + Ref: &refNonEmpty, }, } + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ Value: "value", - Ref: "ref", + Ref: &ref, }, } @@ -128,13 +125,13 @@ func TestValidateSecureValue(t *testing.T) { t.Run("when no changes are made, it returns no errors", func(t *testing.T) { oldSv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "old-title", + Description: "old-description", }, } sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "new-title", + Description: "new-description", }, } @@ -151,15 +148,17 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) { + keeperA := "a-keeper" + keeperAnother := "another-keeper" oldSv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Keeper: "a-keeper", + Keeper: &keeperA, }, } sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Keeper: "another-keeper", + Keeper: &keeperAnother, }, } @@ -170,9 +169,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("`decrypters` must have unique items", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: []string{ "actor_app1", @@ -187,9 +187,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("`decrypters` must match the expected format", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: []string{ "app1", @@ -215,9 +216,10 @@ func TestValidateSecureValue(t *testing.T) { decrypters := slices.Collect(maps.Keys(allowList)) t.Run("no matches, returns an error", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: []string{"actor_app3"}, }, @@ -228,9 +230,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("no decrypters, returns no error", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: []string{}, }, @@ -241,9 +244,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("one match, returns no errors", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: []string{decrypters[0]}, }, @@ -254,9 +258,10 @@ func TestValidateSecureValue(t *testing.T) { }) t.Run("all matches, returns no errors", func(t *testing.T) { + ref := "ref" sv := &secretv0alpha1.SecureValue{ Spec: secretv0alpha1.SecureValueSpec{ - Title: "title", Keeper: "keeper", Ref: "ref", + Description: "description", Ref: &ref, Decrypters: decrypters, }, @@ -266,4 +271,24 @@ func TestValidateSecureValue(t *testing.T) { require.Empty(t, errs) }) }) + + t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) { + decrypters := make([]string, 0, 64+1) + for i := 0; i < 64+1; i++ { + decrypters = append(decrypters, fmt.Sprintf("actor_app%d", i)) + } + + ref := "ref" + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: decrypters, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec.decrypters", errs[0].Field) + }) }