diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index 19cdad710c2f..e2733d287c1d 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -382,10 +382,31 @@ type ClusterClassVariable struct { // required, this will be specified inside the schema. Required bool `json:"required"` + // Metadata is the metadata of a variable. + // It can be used to add additional data for higher level tools to + // a ClusterClassVariable. + Metadata ClusterClassVariableMetadata `json:"metadata,omitempty"` + // Schema defines the schema of the variable. Schema VariableSchema `json:"schema"` } +// ClusterClassVariableMetadata is the metadata of a variable. +// It can be used to add additional data for higher level tools to +// a ClusterClassVariable. +type ClusterClassVariableMetadata struct { + // Map of string keys and values that can be used to organize and categorize + // (scope and select) variables. + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // Annotations is an unstructured key value map that can be used to store and + // retrieve arbitrary metadata. + // They are not queryable. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + // VariableSchema defines the schema of a variable. type VariableSchema struct { // OpenAPIV3Schema defines the schema of a variable via OpenAPI v3 diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 7eb964845925..cb2844d15497 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -289,6 +289,7 @@ func (in *ClusterClassStatusVariableDefinition) DeepCopy() *ClusterClassStatusVa // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClassVariable) DeepCopyInto(out *ClusterClassVariable) { *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) in.Schema.DeepCopyInto(&out.Schema) } @@ -302,6 +303,35 @@ func (in *ClusterClassVariable) DeepCopy() *ClusterClassVariable { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassVariableMetadata) DeepCopyInto(out *ClusterClassVariableMetadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassVariableMetadata. +func (in *ClusterClassVariableMetadata) DeepCopy() *ClusterClassVariableMetadata { + if in == nil { + return nil + } + out := new(ClusterClassVariableMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterList) DeepCopyInto(out *ClusterList) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index a3644d79f97a..acbe1614306a 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -39,6 +39,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariable": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatusVariable(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariableDefinition": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatusVariableDefinition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariable(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariableMetadata": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariableMetadata(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterList": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterList(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterNetwork": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterNetwork(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterSpec": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterSpec(ref), @@ -582,6 +583,13 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariable(ref common.Re Format: "", }, }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Metadata is the metadata of a variable. It can be used to add additional data for higher level tools to a ClusterClassVariable.", + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariableMetadata"), + }, + }, "schema": { SchemaProps: spec.SchemaProps{ Description: "Schema defines the schema of the variable.", @@ -594,7 +602,52 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariable(ref common.Re }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.VariableSchema"}, + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariableMetadata", "sigs.k8s.io/cluster-api/api/v1beta1.VariableSchema"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariableMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterClassVariableMetadata is the metadata of a variable. It can be used to add additional data for higher level tools to a ClusterClassVariable.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "labels": { + SchemaProps: spec.SchemaProps{ + Description: "Map of string keys and values that can be used to organize and categorize (scope and select) variables.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "annotations": { + SchemaProps: spec.SchemaProps{ + Description: "Annotations is an unstructured key value map that can be used to store and retrieve arbitrary metadata. They are not queryable.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 4c0533672220..c996e47dc1bb 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -979,6 +979,28 @@ spec: ClusterClassVariable defines a variable which can be configured in the Cluster topology and used in patches. properties: + metadata: + description: |- + Metadata is the metadata of a variable. + It can be used to add additional data for higher level tools to + a ClusterClassVariable. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map that can be used to store and + retrieve arbitrary metadata. + They are not queryable. + type: object + labels: + additionalProperties: + type: string + description: |- + Map of string keys and values that can be used to organize and categorize + (scope and select) variables. + type: object + type: object name: description: Name of the variable. type: string diff --git a/internal/topology/variables/clusterclass_variable_validation.go b/internal/topology/variables/clusterclass_variable_validation.go index d6b47bce973d..d498441e3afb 100644 --- a/internal/topology/variables/clusterclass_variable_validation.go +++ b/internal/topology/variables/clusterclass_variable_validation.go @@ -25,6 +25,8 @@ import ( structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting" "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" @@ -77,6 +79,9 @@ func validateClusterClassVariable(ctx context.Context, variable *clusterv1.Clust // Validate variable name. allErrs = append(allErrs, validateClusterClassVariableName(variable.Name, fldPath.Child("name"))...) + // Validate variable metadata. + allErrs = append(allErrs, validateClusterClassVariableMetadata(variable.Metadata, fldPath.Child("metadata"))...) + // Validate schema. allErrs = append(allErrs, validateRootSchema(ctx, variable, fldPath.Child("schema", "openAPIV3Schema"))...) @@ -101,6 +106,19 @@ func validateClusterClassVariableName(variableName string, fldPath *field.Path) return allErrs } +// validateClusterClassVariableMetadata validates a variable metadata. +func validateClusterClassVariableMetadata(metadata clusterv1.ClusterClassVariableMetadata, fldPath *field.Path) field.ErrorList { + allErrs := metav1validation.ValidateLabels( + metadata.Labels, + fldPath.Child("labels"), + ) + allErrs = append(allErrs, apivalidation.ValidateAnnotations( + metadata.Annotations, + fldPath.Child("annotations"), + )...) + return allErrs +} + var validVariableTypes = sets.Set[string]{}.Insert("object", "array", "string", "number", "integer", "boolean") // validateRootSchema validates the schema. diff --git a/internal/topology/variables/clusterclass_variable_validation_test.go b/internal/topology/variables/clusterclass_variable_validation_test.go index 040005389899..0ab6b6c8c677 100644 --- a/internal/topology/variables/clusterclass_variable_validation_test.go +++ b/internal/topology/variables/clusterclass_variable_validation_test.go @@ -200,6 +200,62 @@ func Test_ValidateClusterClassVariable(t *testing.T) { }, wantErr: true, }, + { + name: "Valid variable metadata", + clusterClassVariable: &clusterv1.ClusterClassVariable{ + Name: "validVariable", + Metadata: clusterv1.ClusterClassVariableMetadata{ + Labels: map[string]string{ + "label-key": "label-value", + }, + Annotations: map[string]string{ + "annotation-key": "annotation-value", + }, + }, + Schema: clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "string", + MinLength: ptr.To[int64](1), + }, + }, + }, + }, + { + name: "fail on invalid variable label: key does not start with alphanumeric character", + clusterClassVariable: &clusterv1.ClusterClassVariable{ + Name: "path.tovariable", + Metadata: clusterv1.ClusterClassVariableMetadata{ + Labels: map[string]string{ + ".label-key": "label-value", + }, + }, + Schema: clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "string", + MinLength: ptr.To[int64](1), + }, + }, + }, + wantErr: true, + }, + { + name: "fail on invalid variable annotation: key does not start with alphanumeric character", + clusterClassVariable: &clusterv1.ClusterClassVariable{ + Name: "path.tovariable", + Metadata: clusterv1.ClusterClassVariableMetadata{ + Annotations: map[string]string{ + ".annotation-key": "annotation-value", + }, + }, + Schema: clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "string", + MinLength: ptr.To[int64](1), + }, + }, + }, + wantErr: true, + }, { name: "Valid default value regular string", clusterClassVariable: &clusterv1.ClusterClassVariable{ diff --git a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start.yaml b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start.yaml index a8c059f98ce9..50d9c2865e1b 100644 --- a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start.yaml +++ b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start.yaml @@ -81,6 +81,12 @@ spec: default: kindest - name: etcdImageTag required: true + # This metadata has just been added to verify that we can set metadata. + metadata: + labels: + testLabelKey: testLabelValue + annotations: + testAnnotationKey: testAnnotationValue schema: openAPIV3Schema: type: string