From 6a0737a02058423a1e802c862f1a29a082cb6272 Mon Sep 17 00:00:00 2001 From: killianmuldoon Date: Tue, 24 Jan 2023 18:12:36 +0000 Subject: [PATCH] Add ClusterClass variables to status on reconcile --- api/v1beta1/clusterclass_types.go | 32 +++ api/v1beta1/common_types.go | 4 + api/v1beta1/zz_generated.deepcopy.go | 45 +++++ api/v1beta1/zz_generated.openapi.go | 102 +++++++++- .../cluster.x-k8s.io_clusterclasses.yaml | 188 ++++++++++++++++++ .../implement-topology-mutation-hook.md | 32 +-- .../20220330-topology-mutation-hook.md | 3 +- .../clusterclass/clusterclass_controller.go | 14 ++ .../clusterclass_controller_test.go | 54 +++++ 9 files changed, 456 insertions(+), 18 deletions(-) diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index d9ecb15ae7b5..667352012a99 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -553,6 +553,10 @@ type LocalObjectTemplate struct { // ClusterClassStatus defines the observed state of the ClusterClass. type ClusterClassStatus struct { + // Variables is a list of ClusterClassStatusVariable that are defined for the ClusterClass. + // +optional + Variables []ClusterClassStatusVariable `json:"variables,omitempty"` + // Conditions defines current observed state of the ClusterClass. // +optional Conditions Conditions `json:"conditions,omitempty"` @@ -562,6 +566,34 @@ type ClusterClassStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } +// ClusterClassStatusVariable defines a variable which appears in the status of a ClusterClass. +type ClusterClassStatusVariable struct { + // Name is the name of the variable. + Name string `json:"name"` + + // DefintionsConflict specifies whether or not there are conflicting definitions for a single variable name. + //+optional + DefintionsConflict bool `json:"defintionsConflict,omitempty"` + + // Definitions is a list of definitions for a variable. + Definitions []ClusterClassStatusVariableDefinition `json:"definitions"` +} + +// ClusterClassStatusVariableDefinition defines a variable which appears in the status of a ClusterClass. +type ClusterClassStatusVariableDefinition struct { + // From specifies the origin of the variable definition. + From string `json:"from"` + + // Required specifies if the variable is required. + // Note: this applies to the variable as a whole and thus the + // top-level object defined in the schema. If nested fields are + // required, this will be specified inside the schema. + Required bool `json:"required"` + + // Schema defines the schema of the variable. + Schema VariableSchema `json:"schema"` +} + // GetConditions returns the set of conditions for this object. func (c *ClusterClass) GetConditions() Conditions { return c.Status.Conditions diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go index d505d1420413..3db08c79af79 100644 --- a/api/v1beta1/common_types.go +++ b/api/v1beta1/common_types.go @@ -120,6 +120,10 @@ const ( // instead of being a source of truth for eventual consistency. // This annotation can be used to inform MachinePool status during in-progress scaling scenarios. ReplicasManagedByAnnotation = "cluster.x-k8s.io/replicas-managed-by" + + // VariableDefinitionFromInline indicates a patch or variable was defined in the `.spec` of a ClusterClass + // rather than from an external patch extension. + VariableDefinitionFromInline = "inline" ) const ( diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 7f8125f44cb2..bbc33e05ca99 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -223,6 +223,13 @@ func (in *ClusterClassSpec) DeepCopy() *ClusterClassSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClassStatus) DeepCopyInto(out *ClusterClassStatus) { *out = *in + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]ClusterClassStatusVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make(Conditions, len(*in)) @@ -242,6 +249,44 @@ func (in *ClusterClassStatus) DeepCopy() *ClusterClassStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassStatusVariable) DeepCopyInto(out *ClusterClassStatusVariable) { + *out = *in + if in.Definitions != nil { + in, out := &in.Definitions, &out.Definitions + *out = make([]ClusterClassStatusVariableDefinition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassStatusVariable. +func (in *ClusterClassStatusVariable) DeepCopy() *ClusterClassStatusVariable { + if in == nil { + return nil + } + out := new(ClusterClassStatusVariable) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassStatusVariableDefinition) DeepCopyInto(out *ClusterClassStatusVariableDefinition) { + *out = *in + in.Schema.DeepCopyInto(&out.Schema) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassStatusVariableDefinition. +func (in *ClusterClassStatusVariableDefinition) DeepCopy() *ClusterClassStatusVariableDefinition { + if in == nil { + return nil + } + out := new(ClusterClassStatusVariableDefinition) + in.DeepCopyInto(out) + return out +} + // 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 diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index a496c284f5ad..4c80dcca0c85 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -38,6 +38,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassPatch(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassSpec": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatus": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatus(ref), + "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.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), @@ -423,6 +425,20 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatus(ref common.Refe Description: "ClusterClassStatus defines the observed state of the ClusterClass.", Type: []string{"object"}, Properties: map[string]spec.Schema{ + "variables": { + SchemaProps: spec.SchemaProps{ + Description: "Variables is a list of ClusterClassStatusVariable that are defined for the ClusterClass.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariable"), + }, + }, + }, + }, + }, "conditions": { SchemaProps: spec.SchemaProps{ Description: "Conditions defines current observed state of the ClusterClass.", @@ -448,7 +464,91 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatus(ref common.Refe }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.Condition"}, + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariable", "sigs.k8s.io/cluster-api/api/v1beta1.Condition"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatusVariable(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterClassStatusVariable defines a variable which appears in the status of a ClusterClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the name of the variable.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "defintionsConflict": { + SchemaProps: spec.SchemaProps{ + Description: "RequiresNamespace specifies whether or not a variable must be addressed by namespace when a value is provided for it in a Cluster.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "definitions": { + SchemaProps: spec.SchemaProps{ + Description: "Definitions is a list of variables with a schema for each namespace the variable is defined in.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariableDefinition"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "definitions"}, + }, + }, + Dependencies: []string{ + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatusVariableDefinition"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatusVariableDefinition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterClassStatusVariableDefinition defines a variable which appears in the status of a ClusterClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "from": { + SchemaProps: spec.SchemaProps{ + Description: "From specifies the origin of the variable definition.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "required": { + SchemaProps: spec.SchemaProps{ + Description: "Required specifies if the variable is required. Note: this applies to the variable as a whole and thus the top-level object defined in the schema. If nested fields are required, this will be specified inside the schema.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "schema": { + SchemaProps: spec.SchemaProps{ + Description: "Schema defines the schema of the variable.", + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.VariableSchema"), + }, + }, + }, + Required: []string{"from", "required", "schema"}, + }, + }, + Dependencies: []string{ + "sigs.k8s.io/cluster-api/api/v1beta1.VariableSchema"}, } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index b369462229e8..f620ec4c8cab 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -1445,6 +1445,194 @@ spec: by the controller. format: int64 type: integer + variables: + description: Variables is a list of ClusterClassStatusVariable that + are defined for the ClusterClass. + items: + description: ClusterClassStatusVariable defines a variable which + appears in the status of a ClusterClass. + properties: + definitions: + description: Definitions is a list of variables with a schema + for each namespace the variable is defined in. + items: + description: ClusterClassStatusVariableDefinition defines + a variable which appears in the status of a ClusterClass. + properties: + from: + description: From specifies the origin of the variable + definition. + type: string + required: + description: 'Required specifies if the variable is required. + Note: this applies to the variable as a whole and thus + the top-level object defined in the schema. If nested + fields are required, this will be specified inside the + schema.' + type: boolean + schema: + description: Schema defines the schema of the variable. + properties: + openAPIV3Schema: + description: OpenAPIV3Schema defines the schema of + a variable via OpenAPI v3 schema. The schema is + a subset of the schema used in Kubernetes CRDs. + properties: + additionalProperties: + description: 'AdditionalProperties specifies the + schema of values in a map (keys are always strings). + NOTE: Can only be set if type is object. NOTE: + AdditionalProperties is mutually exclusive with + Properties. NOTE: This field uses PreserveUnknownFields + and Schemaless, because recursive validation + is not possible.' + x-kubernetes-preserve-unknown-fields: true + default: + description: 'Default is the default value of + the variable. NOTE: Can be set for all types.' + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is a human-readable description + of this variable. + type: string + enum: + description: 'Enum is the list of valid values + of the variable. NOTE: Can be set for all types.' + items: + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: Example is an example for this variable. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + description: 'ExclusiveMaximum specifies if the + Maximum is exclusive. NOTE: Can only be set + if type is integer or number.' + type: boolean + exclusiveMinimum: + description: 'ExclusiveMinimum specifies if the + Minimum is exclusive. NOTE: Can only be set + if type is integer or number.' + type: boolean + format: + description: 'Format is an OpenAPI v3 format string. + Unknown formats are ignored. For a list of supported + formats please see: (of the k8s.io/apiextensions-apiserver + version we''re currently using) https://github.com/kubernetes/apiextensions-apiserver/blob/master/pkg/apiserver/validation/formats.go + NOTE: Can only be set if type is string.' + type: string + items: + description: 'Items specifies fields of an array. + NOTE: Can only be set if type is array. NOTE: + This field uses PreserveUnknownFields and Schemaless, + because recursive validation is not possible.' + x-kubernetes-preserve-unknown-fields: true + maxItems: + description: 'MaxItems is the max length of an + array variable. NOTE: Can only be set if type + is array.' + format: int64 + type: integer + maxLength: + description: 'MaxLength is the max length of a + string variable. NOTE: Can only be set if type + is string.' + format: int64 + type: integer + maximum: + description: 'Maximum is the maximum of an integer + or number variable. If ExclusiveMaximum is false, + the variable is valid if it is lower than, or + equal to, the value of Maximum. If ExclusiveMaximum + is true, the variable is valid if it is strictly + lower than the value of Maximum. NOTE: Can only + be set if type is integer or number.' + format: int64 + type: integer + minItems: + description: 'MinItems is the min length of an + array variable. NOTE: Can only be set if type + is array.' + format: int64 + type: integer + minLength: + description: 'MinLength is the min length of a + string variable. NOTE: Can only be set if type + is string.' + format: int64 + type: integer + minimum: + description: 'Minimum is the minimum of an integer + or number variable. If ExclusiveMinimum is false, + the variable is valid if it is greater than, + or equal to, the value of Minimum. If ExclusiveMinimum + is true, the variable is valid if it is strictly + greater than the value of Minimum. NOTE: Can + only be set if type is integer or number.' + format: int64 + type: integer + pattern: + description: 'Pattern is the regex which a string + variable must match. NOTE: Can only be set if + type is string.' + type: string + properties: + description: 'Properties specifies fields of an + object. NOTE: Can only be set if type is object. + NOTE: Properties is mutually exclusive with + AdditionalProperties. NOTE: This field uses + PreserveUnknownFields and Schemaless, because + recursive validation is not possible.' + x-kubernetes-preserve-unknown-fields: true + required: + description: 'Required specifies which fields + of an object are required. NOTE: Can only be + set if type is object.' + items: + type: string + type: array + type: + description: 'Type is the type of the variable. + Valid values are: object, array, string, integer, + number or boolean.' + type: string + uniqueItems: + description: 'UniqueItems specifies if items in + an array must be unique. NOTE: Can only be set + if type is array.' + type: boolean + x-kubernetes-preserve-unknown-fields: + description: XPreserveUnknownFields allows setting + fields in a variable object which are not defined + in the variable schema. This affects fields + recursively, except if nested properties or + additionalProperties are specified in the schema. + type: boolean + required: + - type + type: object + required: + - openAPIV3Schema + type: object + required: + - from + - required + - schema + type: object + type: array + defintionsConflict: + description: RequiresNamespace specifies whether or not a variable + must be addressed by namespace when a value is provided for + it in a Cluster. + type: boolean + name: + description: Name is the name of the variable. + type: string + required: + - definitions + - name + type: object + type: array type: object type: object served: true diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md index 4f7568cfb3f7..f2ee445ab73e 100644 --- a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md @@ -86,7 +86,7 @@ status: variables: - name: no-proxy definitions: - - namespace: inline + - from: inline required: true schema: openAPIV3Schema: @@ -95,15 +95,17 @@ status: example: "internal.com" description: "comma-separated list of machine or domain names excluded from using the proxy." - name: http-proxy + # definitionsConflict is true if there are non-equal definitions for a variable with the same name. + definitionsConflict: true definitions: - - namespace: inline + - from: inline schema: openAPIV3Schema: type: string default: "proxy.example.com" example: "proxy.example.com" description: "proxy for http calls." - - namespace: lbImageRepository + - from: lbImageRepository schema: openAPIV3Schema: type: string @@ -112,19 +114,19 @@ status: description: "proxy for http calls." ``` -### Variable namespacing +### Variable definition conflicts Variable definitions can be inline in the ClusterClass or from any number of external DiscoverVariables hooks. The source -of a variable definition is recorded in the `namespace` field in ClusterClass `.status.variables`. -Variables that are defined by an external DiscoverVariables hook will have the name of the patch they are associated with as their namespace. -Variables that are defined in the ClusterClass `.spec.variables` will have the namespace `inline`. -Note: `inline` is a reserved namespace. It can not be used as the name of an external patch to avoid conflicts. +of a variable definition is recorded in the `from` field in ClusterClass `.status.variables`. +Variables that are defined by an external DiscoverVariables hook will have the name of the patch they are associated with as the value of `from`. +Variables that are defined in the ClusterClass `.spec.variables` will have `inline` as the value of `from`. +Note: `inline` is a reserved name for patches. It can not be used as the name of an external patch to avoid conflicts. -If all variables that share a name have equivalent schemas the variables are considered `global` . `global` variables can -be set without providing a namespace - [see below](#setting-values-for-variables-in-the-cluster). The CAPI components will +If all variables that share a name have equivalent schemas the variable definitions are not in conflict. These variables can +be set without providing `definitionFrom` value - [see below](#setting-values-for-variables-in-the-cluster). The CAPI components will consider variable definitions to be equivalent when they share a name and their schema is exactly equal. ### Setting values for variables in the Cluster -Setting variables that are defined with external variable definitions requires attention to be paid to variable namespacing, as exposed in the ClusterClass status. +Setting variables that are defined with external variable definitions requires attention to be paid to variable definition conflicts, as exposed in the ClusterClass status. Variable values are set in Cluster `.spec.topology.variables`. ```yaml @@ -134,15 +136,15 @@ kind: Cluster spec: topology: variables: - # namespace is not needed as this variable is global. + # no `definitionFrom` is not needed as this variable is global. - name: no-proxy value: "internal.domain.com" - # namespaced variables require values for each individual schema. + # variables with the same name but different definitions require values for each individual schema. - name: http-proxy - namespace: inline + definitionFrom: inline value: http://proxy.example2.com:1234 - name: http-proxy - namespace: lbImageRepository + definitionFrom: lbImageRepository value: host: proxy.example2.com port: 1234 diff --git a/docs/proposals/20220330-topology-mutation-hook.md b/docs/proposals/20220330-topology-mutation-hook.md index 2af82d7439a2..6e8a0034da14 100644 --- a/docs/proposals/20220330-topology-mutation-hook.md +++ b/docs/proposals/20220330-topology-mutation-hook.md @@ -235,8 +235,7 @@ Mitigations: Variable definitions supplied externally by an External Patch Extension through a Variable Discovery Hook can change when the definition in the External Patch Extension changes. This can lead to a clash where variables that previously had the same name and definition no longer have the same definition. Mitigations: -* Variable Discovery Hooks allow addressing variables using namespacing, where the variable value setting in the Cluster - includes the name of the Patch as a namespace. +* Variable Discovery Hooks allow addressing conflicting variables individually by specifying the source of the variable's definition when setting the variable value in the Cluster. * ClusterClass authors should pro-actively test any changes to ClusterClasses and associated Runtime Extensions to avoid clashing variable definitions. * External Patch extension authors should extensively document their patches, variables and their usage. diff --git a/internal/controllers/clusterclass/clusterclass_controller.go b/internal/controllers/clusterclass/clusterclass_controller.go index d1f3b8fbafee..df64a559b7b9 100644 --- a/internal/controllers/clusterclass/clusterclass_controller.go +++ b/internal/controllers/clusterclass/clusterclass_controller.go @@ -177,6 +177,20 @@ func (r *Reconciler) reconcile(ctx context.Context, clusterClass *clusterv1.Clus return ctrl.Result{}, kerrors.NewAggregate(errs) } + // Ensure the variables are added to the ClusterClass status. + clusterClass.Status.Variables = []clusterv1.ClusterClassStatusVariable{} + for _, variable := range clusterClass.Spec.Variables { + clusterClass.Status.Variables = append(clusterClass.Status.Variables, + clusterv1.ClusterClassStatusVariable{ + Name: variable.Name, + Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ + { + From: clusterv1.VariableDefinitionFromInline, + Required: variable.Required, + Schema: variable.Schema, + }, + }}) + } reconcileConditions(clusterClass, outdatedRefs) return ctrl.Result{}, nil diff --git a/internal/controllers/clusterclass/clusterclass_controller_test.go b/internal/controllers/clusterclass/clusterclass_controller_test.go index aa3dfe7c2d7e..2dfe4f381fa8 100644 --- a/internal/controllers/clusterclass/clusterclass_controller_test.go +++ b/internal/controllers/clusterclass/clusterclass_controller_test.go @@ -19,10 +19,12 @@ package clusterclass import ( "context" "fmt" + "reflect" "testing" "time" . "github.com/onsi/gomega" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -75,6 +77,24 @@ func TestClusterClassReconciler_reconcile(t *testing.T) { WithControlPlaneTemplate(controlPlaneTemplate). WithControlPlaneInfrastructureMachineTemplate(infraMachineTemplateControlPlane). WithWorkerMachineDeploymentClasses(*machineDeploymentClass1, *machineDeploymentClass2). + WithVariables( + clusterv1.ClusterClassVariable{ + Name: "hdd", + Required: true, + Schema: clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + clusterv1.ClusterClassVariable{ + Name: "cpu", + Schema: clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "integer", + }, + }, + }). Build() // Create the set of initObjects from the objects above to add to the API server when the test environment starts. @@ -106,10 +126,44 @@ func TestClusterClassReconciler_reconcile(t *testing.T) { g.Expect(assertMachineDeploymentClasses(ctx, actualClusterClass, ns)).Should(Succeed()) + g.Expect(assertStatusVariables(actualClusterClass)).Should(Succeed()) return nil }, timeout).Should(Succeed()) } +func assertStatusVariables(actualClusterClass *clusterv1.ClusterClass) error { + // Assert that each inline variable definition has been exposed in the ClusterClass status. + for _, specVar := range actualClusterClass.Spec.Variables { + var found bool + for _, statusVar := range actualClusterClass.Status.Variables { + if specVar.Name != statusVar.Name { + continue + } + found = true + if statusVar.DefintionsConflict { + return errors.Errorf("ClusterClass status %s variable RequiresNamespace does not match. Expected %v , got %v", specVar.Name, false, statusVar.DefintionsConflict) + } + if len(statusVar.Definitions) != 1 { + return errors.Errorf("ClusterClass status has multiple definitions for variable %s. Expected a single definition", specVar.Name) + } + // For this test assume there is only one status variable definition, and that it should match the spec. + statusVarDefinition := statusVar.Definitions[0] + if statusVarDefinition.From != clusterv1.VariableDefinitionFromInline { + return errors.Errorf("ClusterClass status variable %s namespace field does not match. Expected %s. Got %s", statusVar.Name, clusterv1.VariableDefinitionFromInline, statusVarDefinition.From) + } + if specVar.Required != statusVarDefinition.Required { + return errors.Errorf("ClusterClass status variable %s required field does not match. Expecte %v. Got %v", specVar.Name, statusVarDefinition.Required, statusVarDefinition.Required) + } + if !reflect.DeepEqual(specVar.Schema, statusVarDefinition.Schema) { + return errors.Errorf("ClusterClass status variable %s schema does not match. Expected %v. Got %v", specVar.Name, specVar.Schema, statusVarDefinition.Schema) + } + } + if !found { + return errors.Errorf("ClusterClass does not define variable %s", specVar.Name) + } + } + return nil +} func assertInfrastructureClusterTemplate(ctx context.Context, actualClusterClass *clusterv1.ClusterClass, ns *corev1.Namespace) error { // Assert the infrastructure cluster template has the correct owner reference. actualInfraClusterTemplate := builder.InfrastructureClusterTemplate("", "").Build()