SecretsManager: Changes to specs as ref, description, system keeper (#105319)

* SecretsManager: Changes to specs as ref, description, system keeper

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec ref, description, system keeper

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec description

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec description

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

---------

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
pull/105339/head
Dana Axinte 2 months ago committed by GitHub
parent 9d6ce37f68
commit 5158dce936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/apis/secret/go.mod
  2. 89
      pkg/apis/secret/v0alpha1/keeper.go
  3. 8
      pkg/apis/secret/v0alpha1/register.go
  4. 36
      pkg/apis/secret/v0alpha1/secure_value.go
  5. 77
      pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go
  6. 144
      pkg/apis/secret/v0alpha1/zz_generated.openapi.go
  7. 2
      pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list
  8. 27
      pkg/registry/apis/secret/reststorage/keeper_rest.go
  9. 94
      pkg/registry/apis/secret/reststorage/keeper_rest_test.go
  10. 29
      pkg/registry/apis/secret/reststorage/secure_value_rest.go
  11. 91
      pkg/registry/apis/secret/reststorage/secure_value_rest_test.go

@ -11,6 +11,7 @@ require (
k8s.io/apimachinery v0.32.3 k8s.io/apimachinery v0.32.3
k8s.io/apiserver v0.32.3 k8s.io/apiserver v0.32.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
) )
require ( require (
@ -94,7 +95,6 @@ require (
k8s.io/client-go v0.32.3 // indirect k8s.io/client-go v0.32.3 // indirect
k8s.io/component-base v0.32.3 // indirect k8s.io/component-base v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // 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/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect

@ -16,26 +16,52 @@ type Keeper struct {
// This is the actual keeper schema. // This is the actual keeper schema.
// +patchStrategy=replace // +patchStrategy=replace
// +patchMergeKey=name // +patchMergeKey=name
Spec KeeperSpec `json:"spec,omitempty" patchStrategy:"replace" patchMergeKey:"name"` Spec KeeperSpec `json:"spec" patchStrategy:"replace" patchMergeKey:"name"`
} }
func (k *Keeper) IsSqlKeeper() bool { // KeeperType represents the type of a Keeper.
return k.Spec.SQL != nil && k.Spec.SQL.Encryption != nil 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 KeeperConfig interface {
Type() string Type() KeeperType
} }
type KeeperSpec struct { type KeeperSpec struct {
// Human friendly name for the keeper. // Short description for the Keeper.
Title string `json:"title"` // +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
// You can only chose one of the following. Description string `json:"description"`
SQL *SQLKeeperConfig `json:"sql,omitempty"`
AWS *AWSKeeperConfig `json:"aws,omitempty"` // AWS Keeper Configuration.
Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"` // +structType=atomic
GCP *GCPKeeperConfig `json:"gcp,omitempty"` // +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"` HashiCorp *HashiCorpKeeperConfig `json:"hashivault,omitempty"`
} }
@ -52,25 +78,6 @@ type KeeperList struct {
Items []Keeper `json:"items,omitempty"` 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. // Credentials of remote keepers.
type AWSCredentials struct { type AWSCredentials struct {
AccessKeyID CredentialValue `json:"accessKeyId"` AccessKeyID CredentialValue `json:"accessKeyId"`
@ -99,15 +106,19 @@ type HashiCorpCredentials struct {
type Envelope struct{} type Envelope struct{}
// Holds the way credentials are obtained. // Holds the way credentials are obtained.
// +union
type CredentialValue struct { type CredentialValue struct {
// The name of the secure value that holds the actual value. // The name of the secure value that holds the actual value.
// +optional
SecureValueName string `json:"secureValueName,omitempty"` SecureValueName string `json:"secureValueName,omitempty"`
// The value is taken from the environment variable. // The value is taken from the environment variable.
// +optional
ValueFromEnv string `json:"valueFromEnv,omitempty"` ValueFromEnv string `json:"valueFromEnv,omitempty"`
// The value is taken from the Grafana config file. // The value is taken from the Grafana config file.
// TODO: how do we explain that this is a path to the config file? // TODO: how do we explain that this is a path to the config file?
// +optional
ValueFromConfig string `json:"valueFromConfig,omitempty"` ValueFromConfig string `json:"valueFromConfig,omitempty"`
} }
@ -128,18 +139,18 @@ type HashiCorpKeeperConfig struct {
HashiCorpCredentials `json:",inline"` HashiCorpCredentials `json:",inline"`
} }
func (s *AWSKeeperConfig) Type() string { func (s *AWSKeeperConfig) Type() KeeperType {
return "aws" return AWSKeeperType
} }
func (s *AzureKeeperConfig) Type() string { func (s *AzureKeeperConfig) Type() KeeperType {
return "azure" return AzureKeeperType
} }
func (s *GCPKeeperConfig) Type() string { func (s *GCPKeeperConfig) Type() KeeperType {
return "gcp" return GCPKeeperType
} }
func (s *HashiCorpKeeperConfig) Type() string { func (s *HashiCorpKeeperConfig) Type() KeeperType {
return "hashicorp" return HashiCorpKeeperType
} }

@ -30,7 +30,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
// This defines the fields we view in `kubectl get`. Not related with the storage layer. // This defines the fields we view in `kubectl get`. Not related with the storage layer.
Definition: []metav1.TableColumnDefinition{ Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"}, {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: "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: "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"}, {Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"},
@ -41,7 +41,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
if ok { if ok {
return []interface{}{ return []interface{}{
r.Name, r.Name,
r.Spec.Title, r.Spec.Description,
r.Spec.Keeper, r.Spec.Keeper,
r.Spec.Ref, r.Spec.Ref,
r.Status.Phase, 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. // This defines the fields we view in `kubectl get`. Not related with the storage layer.
Definition: []metav1.TableColumnDefinition{ Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"}, {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`. // 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) { Reader: func(obj any) ([]interface{}, error) {
@ -73,7 +73,7 @@ var KeeperResourceInfo = utils.NewResourceInfo(
if ok { if ok {
return []interface{}{ return []interface{}{
r.Name, r.Name,
r.Spec.Title, r.Spec.Description,
}, nil }, nil
} }

@ -14,11 +14,11 @@ type SecureValue struct {
metav1.ObjectMeta `json:"metadata,omitempty"` metav1.ObjectMeta `json:"metadata,omitempty"`
// This is the actual secure value schema. // This is the actual secure value schema.
Spec SecureValueSpec `json:"spec,omitempty"` Spec SecureValueSpec `json:"spec"`
// Read-only observed status of the `SecureValue`. // Read-only observed status of the `SecureValue`.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status // 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 // +enum
@ -46,32 +46,40 @@ type SecureValueStatus struct {
// Only applicable if the `phase=Failed`. // Only applicable if the `phase=Failed`.
// +optional // +optional
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
// +optional
ExternalID string `json:"externalId,omitempty"`
} }
type SecureValueSpec struct { type SecureValueSpec struct {
// Human friendly name for the secure value. // Short description that explains the purpose of this SecureValue.
Title string `json:"title"` // +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. // 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`. // 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"` 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. // 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. // 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. // The Decrypters that are allowed to decrypt this secret.
// An empty list means no service can decrypt it. // An empty list means no service can decrypt it.
// Support and behavior is still TBD, but could likely look like: // +k8s:validation:maxItems=64
// * testdata.grafana.app/{name1} // +k8s:validation:uniqueItems=true
// * 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"}]
// +listType=atomic // +listType=atomic
// +optional // +optional
Decrypters []string `json:"decrypters"` Decrypters []string `json:"decrypters"`

@ -96,47 +96,6 @@ func (in *CredentialValue) DeepCopy() *CredentialValue {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Envelope) DeepCopyInto(out *Envelope) { func (in *Envelope) DeepCopyInto(out *Envelope) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KeeperSpec) DeepCopyInto(out *KeeperSpec) { func (in *KeeperSpec) DeepCopyInto(out *KeeperSpec) {
*out = *in *out = *in
if in.SQL != nil {
in, out := &in.SQL, &out.SQL
*out = new(SQLKeeperConfig)
(*in).DeepCopyInto(*out)
}
if in.AWS != nil { if in.AWS != nil {
in, out := &in.AWS, &out.AWS in, out := &in.AWS, &out.AWS
*out = new(AWSKeeperConfig) *out = new(AWSKeeperConfig)
@ -321,27 +275,6 @@ func (in *KeeperSpec) DeepCopy() *KeeperSpec {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecureValue) DeepCopyInto(out *SecureValue) { func (in *SecureValue) DeepCopyInto(out *SecureValue) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecureValueSpec) DeepCopyInto(out *SecureValueSpec) { func (in *SecureValueSpec) DeepCopyInto(out *SecureValueSpec) {
*out = *in *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 { if in.Decrypters != nil {
in, out := &in.Decrypters, &out.Decrypters in, out := &in.Decrypters, &out.Decrypters
*out = make([]string, len(*in)) *out = make([]string, len(*in))

@ -10,6 +10,7 @@ package v0alpha1
import ( import (
common "k8s.io/kube-openapi/pkg/common" common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec" spec "k8s.io/kube-openapi/pkg/validation/spec"
ptr "k8s.io/utils/ptr"
) )
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { 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.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.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.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.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.GCPCredentials": schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig": schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(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.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.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.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.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.SecureValueList": schema_pkg_apis_secret_v0alpha1_SecureValueList(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec": schema_pkg_apis_secret_v0alpha1_SecureValueSpec(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
}, },
}, },
}, },
}, VendorExtensible: spec.VendorExtensible{
} Extensions: spec.Extensions{
} "x-kubernetes-unions": []interface{}{
map[string]interface{}{
func schema_pkg_apis_secret_v0alpha1_Encryption(ref common.ReferenceCallback) common.OpenAPIDefinition { "fields-to-discriminateBy": map[string]interface{}{
return common.OpenAPIDefinition{ "secureValueName": "SecureValueName",
Schema: spec.Schema{ "valueFromConfig": "ValueFromConfig",
SchemaProps: spec.SchemaProps{ "valueFromEnv": "ValueFromEnv",
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"),
}, },
}, },
}, },
}, },
}, },
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{ Dependencies: []string{
@ -486,66 +459,66 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Type: []string{"object"}, Type: []string{"object"},
Properties: map[string]spec.Schema{ Properties: map[string]spec.Schema{
"title": { "description": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "Human friendly name for the keeper.", Description: "Short description for the Keeper.",
Default: "", Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"}, Type: []string{"string"},
Format: "", 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": { "aws": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{ 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": { "azurekeyvault": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{ 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": { "gcp": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{ 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": { "hashivault": {
SchemaProps: spec.SchemaProps{ VendorExtensible: spec.VendorExtensible{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"), Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
}, },
},
},
Required: []string{"title"},
},
},
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{ SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption"), Description: "HashiCorp Vault Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"),
}, },
}, },
}, },
Required: []string{"description"},
}, },
}, },
Dependencies: []string{ 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{ Dependencies: []string{
@ -654,10 +627,12 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Type: []string{"object"}, Type: []string{"object"},
Properties: map[string]spec.Schema{ Properties: map[string]spec.Schema{
"title": { "description": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "Human friendly name for the secure value.", Description: "Short description that explains the purpose of this SecureValue.",
Default: "", Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"}, Type: []string{"string"},
Format: "", Format: "",
}, },
@ -665,20 +640,25 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
"value": { "value": {
SchemaProps: spec.SchemaProps{ 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`.", 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"}, Type: []string{"string"},
Format: "", Format: "",
}, },
}, },
"ref": { "ref": {
SchemaProps: spec.SchemaProps{ 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"}, Type: []string{"string"},
Format: "", Format: "",
}, },
}, },
"keeper": { "keeper": {
SchemaProps: spec.SchemaProps{ 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"}, Type: []string{"string"},
Format: "", Format: "",
}, },
@ -690,7 +670,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
}, },
}, },
SchemaProps: spec.SchemaProps{ 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"}, Type: []string{"array"},
Items: &spec.SchemaOrArray{ Items: &spec.SchemaOrArray{
Schema: &spec.Schema{ 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: "", Format: "",
}, },
}, },
"externalId": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
}, },
Required: []string{"phase"}, Required: []string{"phase"},
}, },

@ -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,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,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,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,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,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,KeeperSpec,HashiCorp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID

@ -209,8 +209,8 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation
errs := make(field.ErrorList, 0) errs := make(field.ErrorList, 0)
if keeper.Spec.Title == "" { if keeper.Spec.Description == "" {
errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) 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. // 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 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 keeper.Spec.AWS != nil {
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyId"), keeper.Spec.AWS.AccessKeyID); err != nil { if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyId"), keeper.Spec.AWS.AccessKeyID); err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -295,7 +273,6 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation
func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error { func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error {
availableKeepers := map[string]bool{ availableKeepers := map[string]bool{
"sql": keeper.Spec.SQL != nil,
"aws": keeper.Spec.AWS != nil, "aws": keeper.Spec.AWS != nil,
"azure": keeper.Spec.Azure != nil, "azure": keeper.Spec.Azure != nil,
"gcp": keeper.Spec.GCP != nil, "gcp": keeper.Spec.GCP != nil,

@ -10,28 +10,33 @@ import (
func TestValidateKeeper(t *testing.T) { func TestValidateKeeper(t *testing.T) {
t.Run("when creating a new keeper", func(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{ keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ 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) errs := ValidateKeeper(keeper, admission.Create)
require.Len(t, errs, 1) 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) { t.Run("only one `keeper` must be present", func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{ keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ Spec: secretv0alpha1.KeeperSpec{
Title: "title", Description: "short description",
SQL: &secretv0alpha1.SQLKeeperConfig{}, AWS: &secretv0alpha1.AWSKeeperConfig{},
AWS: &secretv0alpha1.AWSKeeperConfig{}, Azure: &secretv0alpha1.AzureKeeperConfig{},
Azure: &secretv0alpha1.AzureKeeperConfig{}, GCP: &secretv0alpha1.GCPKeeperConfig{},
GCP: &secretv0alpha1.GCPKeeperConfig{}, HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{},
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) { t.Run("at least one `keeper` must be present", func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{ keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ 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) { t.Run("aws keeper validation", func(t *testing.T) {
validKeeperAWS := &secretv0alpha1.Keeper{ validKeeperAWS := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ Spec: secretv0alpha1.KeeperSpec{
Title: "title", Description: "description",
AWS: &secretv0alpha1.AWSKeeperConfig{ AWS: &secretv0alpha1.AWSKeeperConfig{
AWSCredentials: secretv0alpha1.AWSCredentials{ AWSCredentials: secretv0alpha1.AWSCredentials{
AccessKeyID: secretv0alpha1.CredentialValue{ AccessKeyID: secretv0alpha1.CredentialValue{
@ -122,7 +127,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("azure keeper validation", func(t *testing.T) { t.Run("azure keeper validation", func(t *testing.T) {
validKeeperAzure := &secretv0alpha1.Keeper{ validKeeperAzure := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ Spec: secretv0alpha1.KeeperSpec{
Title: "title", Description: "description",
Azure: &secretv0alpha1.AzureKeeperConfig{ Azure: &secretv0alpha1.AzureKeeperConfig{
AzureCredentials: secretv0alpha1.AzureCredentials{ AzureCredentials: secretv0alpha1.AzureCredentials{
KeyVaultName: "kv-name", KeyVaultName: "kv-name",
@ -191,7 +196,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("gcp keeper validation", func(t *testing.T) { t.Run("gcp keeper validation", func(t *testing.T) {
validKeeperGCP := &secretv0alpha1.Keeper{ validKeeperGCP := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ Spec: secretv0alpha1.KeeperSpec{
Title: "title", Description: "description",
GCP: &secretv0alpha1.GCPKeeperConfig{ GCP: &secretv0alpha1.GCPKeeperConfig{
GCPCredentials: secretv0alpha1.GCPCredentials{ GCPCredentials: secretv0alpha1.GCPCredentials{
ProjectID: "project-id", ProjectID: "project-id",
@ -223,7 +228,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("hashicorp keeper validation", func(t *testing.T) { t.Run("hashicorp keeper validation", func(t *testing.T) {
validKeeperHashiCorp := &secretv0alpha1.Keeper{ validKeeperHashiCorp := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{ Spec: secretv0alpha1.KeeperSpec{
Title: "title", Description: "description",
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{ HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{
HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{ HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{
Address: "http://address", 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)
})
}
})
})
} }

@ -218,19 +218,15 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss
func validateSecureValueCreate(sv *secretv0alpha1.SecureValue) field.ErrorList { func validateSecureValueCreate(sv *secretv0alpha1.SecureValue) field.ErrorList {
errs := make(field.ErrorList, 0) errs := make(field.ErrorList, 0)
if sv.Spec.Title == "" { if sv.Spec.Description == "" {
errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required")) errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required"))
} }
if sv.Spec.Keeper == "" { if sv.Spec.Value == "" && (sv.Spec.Ref == nil || (sv.Spec.Ref != nil && *sv.Spec.Ref == "")) {
errs = append(errs, field.Required(field.NewPath("spec", "keeper"), "a `keeper` is required"))
}
if sv.Spec.Value == "" && sv.Spec.Ref == "" {
errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required")) 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")) 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. // Only validate if one of the fields is being changed/set.
if sv.Spec.Value != "" || sv.Spec.Ref != "" { if sv.Spec.Value != "" || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
if oldSv.Spec.Ref != "" && sv.Spec.Value != "" { 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")) 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")) 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 { func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList {
errs := make(field.ErrorList, 0) 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) decrypterNames := make(map[string]struct{}, 0)
for i, decrypter := range decrypters { for i, decrypter := range decrypters {

@ -14,44 +14,37 @@ import (
func TestValidateSecureValue(t *testing.T) { func TestValidateSecureValue(t *testing.T) {
t.Run("when creating a new securevalue", func(t *testing.T) { t.Run("when creating a new securevalue", func(t *testing.T) {
keeper := "keeper"
validSecureValue := &secretv0alpha1.SecureValue{ validSecureValue := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Description: "description",
Value: "value", Value: "value",
Keeper: "keeper", Keeper: &keeper,
Decrypters: []string{"actor_app1", "actor_app2"}, 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 := validSecureValue.DeepCopy()
sv.Spec.Title = "" sv.Spec.Description = ""
errs := ValidateSecureValue(sv, nil, admission.Create, nil) errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1) require.Len(t, errs, 1)
require.Equal(t, "spec.title", errs[0].Field) require.Equal(t, "spec.description", 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)
}) })
t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) { t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) {
sv := validSecureValue.DeepCopy() sv := validSecureValue.DeepCopy()
sv.Spec.Value = "" sv.Spec.Value = ""
sv.Spec.Ref = "" sv.Spec.Ref = nil
errs := ValidateSecureValue(sv, nil, admission.Create, nil) errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1) require.Len(t, errs, 1)
require.Equal(t, "spec", errs[0].Field) require.Equal(t, "spec", errs[0].Field)
ref := "value"
sv.Spec.Value = "value" sv.Spec.Value = "value"
sv.Spec.Ref = "value" sv.Spec.Ref = &ref
errs = ValidateSecureValue(sv, nil, admission.Create, nil) errs = ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1) 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) { 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{ oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ 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{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ 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) { 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{ oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ 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) { t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) {
refNonEmpty := "non-empty"
oldSv := &secretv0alpha1.SecureValue{ oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Ref: "non-empty", Ref: &refNonEmpty,
}, },
} }
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Value: "value", 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) { t.Run("when no changes are made, it returns no errors", func(t *testing.T) {
oldSv := &secretv0alpha1.SecureValue{ oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "old-title", Description: "old-description",
}, },
} }
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ 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) { 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{ oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Keeper: "a-keeper", Keeper: &keeperA,
}, },
} }
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ 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) { t.Run("`decrypters` must have unique items", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: []string{ Decrypters: []string{
"actor_app1", "actor_app1",
@ -187,9 +187,10 @@ func TestValidateSecureValue(t *testing.T) {
}) })
t.Run("`decrypters` must match the expected format", func(t *testing.T) { t.Run("`decrypters` must match the expected format", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: []string{ Decrypters: []string{
"app1", "app1",
@ -215,9 +216,10 @@ func TestValidateSecureValue(t *testing.T) {
decrypters := slices.Collect(maps.Keys(allowList)) decrypters := slices.Collect(maps.Keys(allowList))
t.Run("no matches, returns an error", func(t *testing.T) { t.Run("no matches, returns an error", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: []string{"actor_app3"}, Decrypters: []string{"actor_app3"},
}, },
@ -228,9 +230,10 @@ func TestValidateSecureValue(t *testing.T) {
}) })
t.Run("no decrypters, returns no error", func(t *testing.T) { t.Run("no decrypters, returns no error", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: []string{}, Decrypters: []string{},
}, },
@ -241,9 +244,10 @@ func TestValidateSecureValue(t *testing.T) {
}) })
t.Run("one match, returns no errors", func(t *testing.T) { t.Run("one match, returns no errors", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: []string{decrypters[0]}, Decrypters: []string{decrypters[0]},
}, },
@ -254,9 +258,10 @@ func TestValidateSecureValue(t *testing.T) {
}) })
t.Run("all matches, returns no errors", func(t *testing.T) { t.Run("all matches, returns no errors", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{ sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{ Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref", Description: "description", Ref: &ref,
Decrypters: decrypters, Decrypters: decrypters,
}, },
@ -266,4 +271,24 @@ func TestValidateSecureValue(t *testing.T) {
require.Empty(t, errs) 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)
})
} }

Loading…
Cancel
Save