From 1022e264cc8cdb9cc135af10dba019524663390d Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Mon, 11 Oct 2021 17:22:34 +0200 Subject: [PATCH] Add ClusterClass variable and patch types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- api/v1alpha3/conversion_test.go | 15 + api/v1alpha4/conversion.go | 64 +++- api/v1alpha4/conversion_test.go | 57 +++- api/v1alpha4/zz_generated.conversion.go | 101 +++++-- api/v1beta1/cluster_types.go | 24 ++ api/v1beta1/clusterclass_types.go | 210 +++++++++++++ api/v1beta1/zz_generated.deepcopy.go | 284 ++++++++++++++++++ .../cluster.x-k8s.io_clusterclasses.yaml | 230 ++++++++++++++ .../crd/bases/cluster.x-k8s.io_clusters.yaml | 26 ++ ...56-cluster-class-and-managed-topologies.md | 116 ++++--- 10 files changed, 1031 insertions(+), 96 deletions(-) diff --git a/api/v1alpha3/conversion_test.go b/api/v1alpha3/conversion_test.go index 347b4d35f2ae..5356362e72fa 100644 --- a/api/v1alpha3/conversion_test.go +++ b/api/v1alpha3/conversion_test.go @@ -20,6 +20,7 @@ import ( "testing" fuzz "github.com/google/gofuzz" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/cluster-api/api/v1beta1" @@ -32,6 +33,7 @@ func TestFuzzyConversion(t *testing.T) { Hub: &v1beta1.Cluster{}, Spoke: &Cluster{}, SpokeAfterMutation: clusterSpokeAfterMutation, + FuzzerFuncs: []fuzzer.FuzzerFuncs{ClusterJSONFuzzFuncs}, })) t.Run("for Machine", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ @@ -123,3 +125,16 @@ func clusterSpokeAfterMutation(c conversion.Convertible) { // Point cluster.Status.Conditions and our slice that does not have ControlPlaneInitializedCondition cluster.Status.Conditions = tmp } + +func ClusterJSONFuzzFuncs(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + ClusterVariableFuzzer, + } +} + +func ClusterVariableFuzzer(in *v1beta1.ClusterVariable, c fuzz.Continue) { + c.FuzzNoCustom(in) + + // Not every random byte array is valid JSON, e.g. a string without `""`,so we're setting a valid value. + in.Value = apiextensionsv1.JSON{Raw: []byte("\"test-string\"")} +} diff --git a/api/v1alpha4/conversion.go b/api/v1alpha4/conversion.go index f25d9d544f88..e778a733a8ab 100644 --- a/api/v1alpha4/conversion.go +++ b/api/v1alpha4/conversion.go @@ -19,19 +19,43 @@ package v1alpha4 import ( apiconversion "k8s.io/apimachinery/pkg/conversion" "sigs.k8s.io/cluster-api/api/v1beta1" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/controller-runtime/pkg/conversion" ) func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*v1beta1.Cluster) - return Convert_v1alpha4_Cluster_To_v1beta1_Cluster(src, dst, nil) + if err := Convert_v1alpha4_Cluster_To_v1beta1_Cluster(src, dst, nil); err != nil { + return err + } + + // Manually restore data. + restored := &v1beta1.Cluster{} + if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { + return err + } + + if restored.Spec.Topology != nil { + dst.Spec.Topology.Variables = restored.Spec.Topology.Variables + } + + return nil } func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*v1beta1.Cluster) - return Convert_v1beta1_Cluster_To_v1alpha4_Cluster(src, dst, nil) + if err := Convert_v1beta1_Cluster_To_v1alpha4_Cluster(src, dst, nil); err != nil { + return err + } + + // Preserve Hub data on down-conversion except for metadata + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + return nil } func (src *ClusterList) ConvertTo(dstRaw conversion.Hub) error { @@ -49,13 +73,35 @@ func (dst *ClusterList) ConvertFrom(srcRaw conversion.Hub) error { func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*v1beta1.ClusterClass) - return Convert_v1alpha4_ClusterClass_To_v1beta1_ClusterClass(src, dst, nil) + if err := Convert_v1alpha4_ClusterClass_To_v1beta1_ClusterClass(src, dst, nil); err != nil { + return err + } + + // Manually restore data. + restored := &v1beta1.ClusterClass{} + if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { + return err + } + + dst.Spec.Patches = restored.Spec.Patches + dst.Spec.Variables = restored.Spec.Variables + + return nil } func (dst *ClusterClass) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*v1beta1.ClusterClass) - return Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(src, dst, nil) + if err := Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(src, dst, nil); err != nil { + return err + } + + // Preserve Hub data on down-conversion except for metadata + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + return nil } func (src *ClusterClassList) ConvertTo(dstRaw conversion.Hub) error { @@ -170,3 +216,13 @@ func Convert_v1alpha4_MachineStatus_To_v1beta1_MachineStatus(in *MachineStatus, // Status.version has been removed in v1beta1, thus requiring custom conversion function. the information will be dropped. return autoConvert_v1alpha4_MachineStatus_To_v1beta1_MachineStatus(in, out, s) } + +func Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1beta1.ClusterClassSpec, out *ClusterClassSpec, s apiconversion.Scope) error { + // spec.{variables,patches} has been added with v1beta1. + return autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in, out, s) +} + +func Convert_v1beta1_Topology_To_v1alpha4_Topology(in *v1beta1.Topology, out *Topology, s apiconversion.Scope) error { + // spec.topology.variables has been added with v1beta1. + return autoConvert_v1beta1_Topology_To_v1alpha4_Topology(in, out, s) +} diff --git a/api/v1alpha4/conversion_test.go b/api/v1alpha4/conversion_test.go index 686f7d5be71b..1b4475911139 100644 --- a/api/v1alpha4/conversion_test.go +++ b/api/v1alpha4/conversion_test.go @@ -17,9 +17,11 @@ limitations under the License. package v1alpha4 import ( + "encoding/json" "testing" fuzz "github.com/google/gofuzz" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/cluster-api/api/v1beta1" @@ -28,12 +30,14 @@ import ( func TestFuzzyConversion(t *testing.T) { t.Run("for Cluster", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ - Hub: &v1beta1.Cluster{}, - Spoke: &Cluster{}, + Hub: &v1beta1.Cluster{}, + Spoke: &Cluster{}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{ClusterJSONFuzzFuncs}, })) t.Run("for ClusterClass", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ - Hub: &v1beta1.ClusterClass{}, - Spoke: &ClusterClass{}, + Hub: &v1beta1.ClusterClass{}, + Spoke: &ClusterClass{}, + FuzzerFuncs: []fuzzer.FuzzerFuncs{ClusterClassJSONFuzzFuncs}, })) t.Run("for Machine", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{ @@ -71,3 +75,48 @@ func MachineStatusFuzzer(in *MachineStatus, c fuzz.Continue) { // data is going to be lost, so we're forcing zero values to avoid round trip errors. in.Version = nil } + +func ClusterJSONFuzzFuncs(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + ClusterVariableFuzzer, + } +} + +func ClusterVariableFuzzer(in *v1beta1.ClusterVariable, c fuzz.Continue) { + c.FuzzNoCustom(in) + + // Not every random byte array is valid JSON, e.g. a string without `""`,so we're setting a valid value. + in.Value = apiextensionsv1.JSON{Raw: []byte("\"test-string\"")} +} + +func ClusterClassJSONFuzzFuncs(_ runtimeserializer.CodecFactory) []interface{} { + return []interface{}{ + JSONPatchFuzzer, + JSONSchemaPropsFuzzer, + } +} + +func JSONPatchFuzzer(in *v1beta1.JSONPatch, c fuzz.Continue) { + c.FuzzNoCustom(in) + + // Not every random byte array is valid JSON, e.g. a string without `""`,so we're setting a valid value. + in.Value = &apiextensionsv1.JSON{Raw: []byte("5")} +} + +func JSONSchemaPropsFuzzer(in *v1beta1.JSONSchemaProps, c fuzz.Continue) { + c.FuzzNoCustom(in) + + // Not every random byte array is valid JSON, e.g. a string without `""`,so we're setting valid values. + in.Enum = []apiextensionsv1.JSON{ + {Raw: []byte("\"a\"")}, + {Raw: []byte("\"b\"")}, + {Raw: []byte("\"c\"")}, + } + in.Default = &apiextensionsv1.JSON{Raw: []byte("true")} + + // Not every random string is a valid JSON number, so we're setting a valid JSON number. + number := json.Number("1") + in.MultipleOf = &number + in.Minimum = &number + in.Maximum = &number +} diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index 1b138f920109..2d122cdbee8d 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -94,11 +94,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.ClusterClassSpec)(nil), (*ClusterClassSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(a.(*v1beta1.ClusterClassSpec), b.(*ClusterClassSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*ClusterList)(nil), (*v1beta1.ClusterList)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_ClusterList_To_v1beta1_ClusterList(a.(*ClusterList), b.(*v1beta1.ClusterList), scope) }); err != nil { @@ -439,11 +434,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.Topology)(nil), (*Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_Topology_To_v1alpha4_Topology(a.(*v1beta1.Topology), b.(*Topology), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*UnhealthyCondition)(nil), (*v1beta1.UnhealthyCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_UnhealthyCondition_To_v1beta1_UnhealthyCondition(a.(*UnhealthyCondition), b.(*v1beta1.UnhealthyCondition), scope) }); err != nil { @@ -479,6 +469,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.ClusterClassSpec)(nil), (*ClusterClassSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(a.(*v1beta1.ClusterClassSpec), b.(*ClusterClassSpec), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1beta1.Topology)(nil), (*Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Topology_To_v1alpha4_Topology(a.(*v1beta1.Topology), b.(*Topology), scope) + }); err != nil { + return err + } return nil } @@ -586,7 +586,17 @@ func Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in *v1beta1.ClusterCl func autoConvert_v1alpha4_ClusterClassList_To_v1beta1_ClusterClassList(in *ClusterClassList, out *v1beta1.ClusterClassList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]v1beta1.ClusterClass)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]v1beta1.ClusterClass, len(*in)) + for i := range *in { + if err := Convert_v1alpha4_ClusterClass_To_v1beta1_ClusterClass(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -597,7 +607,17 @@ func Convert_v1alpha4_ClusterClassList_To_v1beta1_ClusterClassList(in *ClusterCl func autoConvert_v1beta1_ClusterClassList_To_v1alpha4_ClusterClassList(in *v1beta1.ClusterClassList, out *ClusterClassList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]ClusterClass)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterClass, len(*in)) + for i := range *in { + if err := Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -634,17 +654,24 @@ func autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1bet if err := Convert_v1beta1_WorkersClass_To_v1alpha4_WorkersClass(&in.Workers, &out.Workers, s); err != nil { return err } + // WARNING: in.Variables requires manual conversion: does not exist in peer-type + // WARNING: in.Patches requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec is an autogenerated conversion function. -func Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1beta1.ClusterClassSpec, out *ClusterClassSpec, s conversion.Scope) error { - return autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in, out, s) -} - func autoConvert_v1alpha4_ClusterList_To_v1beta1_ClusterList(in *ClusterList, out *v1beta1.ClusterList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]v1beta1.Cluster)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]v1beta1.Cluster, len(*in)) + for i := range *in { + if err := Convert_v1alpha4_Cluster_To_v1beta1_Cluster(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -655,7 +682,17 @@ func Convert_v1alpha4_ClusterList_To_v1beta1_ClusterList(in *ClusterList, out *v func autoConvert_v1beta1_ClusterList_To_v1alpha4_ClusterList(in *v1beta1.ClusterList, out *ClusterList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]Cluster)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + if err := Convert_v1beta1_Cluster_To_v1alpha4_Cluster(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -698,7 +735,15 @@ func autoConvert_v1alpha4_ClusterSpec_To_v1beta1_ClusterSpec(in *ClusterSpec, ou } out.ControlPlaneRef = (*v1.ObjectReference)(unsafe.Pointer(in.ControlPlaneRef)) out.InfrastructureRef = (*v1.ObjectReference)(unsafe.Pointer(in.InfrastructureRef)) - out.Topology = (*v1beta1.Topology)(unsafe.Pointer(in.Topology)) + if in.Topology != nil { + in, out := &in.Topology, &out.Topology + *out = new(v1beta1.Topology) + if err := Convert_v1alpha4_Topology_To_v1beta1_Topology(*in, *out, s); err != nil { + return err + } + } else { + out.Topology = nil + } return nil } @@ -715,7 +760,15 @@ func autoConvert_v1beta1_ClusterSpec_To_v1alpha4_ClusterSpec(in *v1beta1.Cluster } out.ControlPlaneRef = (*v1.ObjectReference)(unsafe.Pointer(in.ControlPlaneRef)) out.InfrastructureRef = (*v1.ObjectReference)(unsafe.Pointer(in.InfrastructureRef)) - out.Topology = (*Topology)(unsafe.Pointer(in.Topology)) + if in.Topology != nil { + in, out := &in.Topology, &out.Topology + *out = new(Topology) + if err := Convert_v1beta1_Topology_To_v1alpha4_Topology(*in, *out, s); err != nil { + return err + } + } else { + out.Topology = nil + } return nil } @@ -1660,14 +1713,10 @@ func autoConvert_v1beta1_Topology_To_v1alpha4_Topology(in *v1beta1.Topology, out return err } out.Workers = (*WorkersTopology)(unsafe.Pointer(in.Workers)) + // WARNING: in.Variables requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta1_Topology_To_v1alpha4_Topology is an autogenerated conversion function. -func Convert_v1beta1_Topology_To_v1alpha4_Topology(in *v1beta1.Topology, out *Topology, s conversion.Scope) error { - return autoConvert_v1beta1_Topology_To_v1alpha4_Topology(in, out, s) -} - func autoConvert_v1alpha4_UnhealthyCondition_To_v1beta1_UnhealthyCondition(in *UnhealthyCondition, out *v1beta1.UnhealthyCondition, s conversion.Scope) error { out.Type = v1.NodeConditionType(in.Type) out.Status = v1.ConditionStatus(in.Status) diff --git a/api/v1beta1/cluster_types.go b/api/v1beta1/cluster_types.go index 4cb7fceb0499..b4c3273d19b8 100644 --- a/api/v1beta1/cluster_types.go +++ b/api/v1beta1/cluster_types.go @@ -23,6 +23,7 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" @@ -90,6 +91,12 @@ type Topology struct { // for the cluster. // +optional Workers *WorkersTopology `json:"workers,omitempty"` + + // Variables can be used to customize the Cluster through + // patches. They must comply to the corresponding + // VariableClasses defined in the ClusterClass. + // +optional + Variables []ClusterVariable `json:"variables,omitempty"` } // ControlPlaneTopology specifies the parameters for the control plane nodes in the cluster. @@ -144,6 +151,23 @@ type MachineDeploymentTopology struct { Replicas *int32 `json:"replicas,omitempty"` } +// ClusterVariable can be used to customize the Cluster through +// patches. It must comply to the corresponding +// ClusterClassVariable defined in the ClusterClass. +type ClusterVariable struct { + // Name of the variable. + Name string `json:"name"` + + // Value of the variable. + // Note: the value will be validated against the schema of the corresponding ClusterClassVariable + // from the ClusterClass. + // Note: We have to use apiextensionsv1.JSON instead of a custom JSON type, because controller-tools has a + // hard-coded schema for apiextensionsv1.JSON which cannot be produced by another type via controller-tools, + // i.e. it's not possible to have no type field. + // Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111 + Value apiextensionsv1.JSON `json:"value"` +} + // ANCHOR_END: ClusterSpec // ANCHOR: ClusterNetwork diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index a7ff7aea1ee4..1ad7196ddf11 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -17,7 +17,10 @@ limitations under the License. package v1beta1 import ( + "encoding/json" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -54,6 +57,17 @@ type ClusterClassSpec struct { // the worker nodes of the cluster. // +optional Workers WorkersClass `json:"workers,omitempty"` + + // Variables defines the variables which can be configured + // in the Cluster topology and are then used in patches. + // +optional + Variables []ClusterClassVariable `json:"variables,omitempty"` + + // Patches defines the patches which are applied to customize + // referenced templates of a ClusterClass. + // Note: Patches will be applied in the order of the array. + // +optional + Patches []ClusterClassPatch `json:"patches,omitempty"` } // ControlPlaneClass defines the class for the control plane. @@ -117,6 +131,202 @@ type MachineDeploymentClassTemplate struct { Infrastructure LocalObjectTemplate `json:"infrastructure"` } +// ClusterClassVariable defines a variable which can +// be configured in the Cluster topology and used in patches. +type ClusterClassVariable struct { + // Name of the variable. + Name string `json:"name"` + + // 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"` +} + +// VariableSchema defines the schema of a variable. +type VariableSchema struct { + // OpenAPIV3Schema defines the schema of a variable via OpenAPI v3 + // schema. The schema is a subset of the schema used in + // Kubernetes CRDs. + OpenAPIV3Schema JSONSchemaProps `json:"openAPIV3Schema"` +} + +// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/). +// This struct has been initially copied from apiextensionsv1.JSONSchemaProps, but all fields +// which are not supported in CAPI have been removed. +type JSONSchemaProps struct { + // Type is the type of the variable. + // Valid values are: string, integer, number or boolean. + Type string `json:"type"` + + // Nullable specifies if the variable can be set to null. + // +optional + Nullable bool `json:"nullable,omitempty"` + + // 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 + // +optional + Format string `json:"format,omitempty"` + + // MaxLength is the max length of a string variable. + // +optional + MaxLength *int64 `json:"maxLength,omitempty"` + + // MinLength is the min length of a string variable. + // +optional + MinLength *int64 `json:"minLength,omitempty"` + + // Pattern is the regex which a string variable must match. + // +optional + Pattern string `json:"pattern,omitempty"` + + // 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. + // +optional + Maximum *json.Number `json:"maximum,omitempty"` + + // ExclusiveMaximum specifies if the Maximum is exclusive. + // +optional + ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` + + // 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. + // +optional + Minimum *json.Number `json:"minimum,omitempty"` + + // ExclusiveMinimum specifies if the Minimum is exclusive. + // +optional + ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` + + // MultipleOf specifies the number of which the variable must be a multiple of. + // +optional + MultipleOf *json.Number `json:"multipleOf,omitempty"` + + // Enum is the list of valid values of the variable. + // +optional + Enum []apiextensionsv1.JSON `json:"enum,omitempty"` + + // Default is the default value of the variable. + // +optional + Default *apiextensionsv1.JSON `json:"default,omitempty"` +} + +// ClusterClassPatch defines a patch which is applied to customize the referenced templates. +type ClusterClassPatch struct { + // Name of the patch. + Name string `json:"name"` + + // Definitions define the patches inline. + // Note: Patches will be applied in the order of the array. + Definitions []PatchDefinition `json:"definitions"` +} + +// PatchDefinition defines a patch which is applied to customize the referenced templates. +type PatchDefinition struct { + // Selector defines on which templates the patch should be applied. + Selector PatchSelector `json:"selector"` + + // JSONPatches defines the patches which should be applied on the templates + // matching the selector. + // Note: Patches will be applied in the order of the array. + JSONPatches []JSONPatch `json:"jsonPatches"` +} + +// PatchSelector defines on which templates the patch should be applied. +// Note: Matching on APIVersion and Kind is mandatory, to enforce that the patches are +// written for the correct version. The version of the references in the ClusterClass may +// be automatically updated during reconciliation if there is a newer version for the same contract. +// Note: The results of selection based on the individual fields are ANDed. +type PatchSelector struct { + // APIVersion filters templates by apiVersion. + APIVersion string `json:"apiVersion"` + + // Kind filters templates by kind. + Kind string `json:"kind"` + + // MatchResources selects templates based on where they are referenced. + MatchResources PatchSelectorMatch `json:"matchResources"` +} + +// PatchSelectorMatch selects templates based on where they are referenced. +// Note: At least one of the fields must be set. +// Note: The results of selection based on the individual fields are ORed. +type PatchSelectorMatch struct { + // ControlPlane selects templates referenced in .spec.ControlPlane. + // Note: this will match the controlPlane and also the controlPlane + // machineInfrastructure (depending on the kind and apiVersion). + // +optional + ControlPlane *bool `json:"controlPlane,omitempty"` + + // InfrastructureCluster selects templates referenced in .spec.infrastructure. + // +optional + InfrastructureCluster *bool `json:"infrastructureCluster,omitempty"` + + // MachineDeploymentClass selects templates referenced in specific MachineDeploymentClasses in + // .spec.workers.machineDeployments. + // +optional + MachineDeploymentClass *PatchSelectorMatchMachineDeploymentClass `json:"machineDeploymentClass,omitempty"` +} + +// PatchSelectorMatchMachineDeploymentClass selects templates referenced +// in specific MachineDeploymentClasses in .spec.workers.machineDeployments. +type PatchSelectorMatchMachineDeploymentClass struct { + // Names selects templates by class names. + Names []string `json:"names"` +} + +// JSONPatch defines a JSON patch. +type JSONPatch struct { + // Op defines the operation of the patch. + // Note: Only `add`, `replace` and `remove` are supported. + Op string `json:"op"` + + // Path defines the path of the patch. + // Note: Only the spec of a template can be patched, thus the path has to start with /spec/. + // Note: For now the only allowed array modifications are `append` and `prepend`, i.e.: + // * for op: `add`: only index 0 (prepend) and - (append) are allowed + // * for op: `replace` or `remove`: no indexes are allowed + Path string `json:"path"` + + // Value defines the value of the patch. + // Note: Either Value or ValueFrom is required for add and replace + // operations. Only one of them is allowed to be set at the same time. + // Note: We have to use apiextensionsv1.JSON instead of our JSON type, + // because controller-tools has a hard-coded schema for apiextensionsv1.JSON + // which cannot be produced by another type (unset type field). + // Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111 + // +optional + Value *apiextensionsv1.JSON `json:"value,omitempty"` + + // ValueFrom defines the value of the patch. + // Note: Either Value or ValueFrom is required for add and replace + // operations. Only one of them is allowed to be set at the same time. + // +optional + ValueFrom *JSONPatchValue `json:"valueFrom,omitempty"` +} + +// JSONPatchValue defines the value of a patch. +// Note: Only one of the fields is allowed to be set at the same time. +type JSONPatchValue struct { + // Variable is the variable to be used as value. + // Variable can be one of the variables defined in .spec.variables or a builtin variable. + // +optional + Variable *string `json:"variable,omitempty"` + + // Template is the Go template to be used to calculate the value. + // A template can reference variables defined in .spec.variables and builtin variables. + // Note: The template must evaluate to a valid YAML or JSON value. + // +optional + Template *string `json:"template,omitempty"` +} + // LocalObjectTemplate defines a template for a topology Class. type LocalObjectTemplate struct { // Ref is a required reference to a custom resource diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f8d0c1993c06..b85850286e0e 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -21,7 +21,9 @@ limitations under the License. package v1beta1 import ( + "encoding/json" "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -153,12 +155,48 @@ func (in *ClusterClassList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassPatch) DeepCopyInto(out *ClusterClassPatch) { + *out = *in + if in.Definitions != nil { + in, out := &in.Definitions, &out.Definitions + *out = make([]PatchDefinition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassPatch. +func (in *ClusterClassPatch) DeepCopy() *ClusterClassPatch { + if in == nil { + return nil + } + out := new(ClusterClassPatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClassSpec) DeepCopyInto(out *ClusterClassSpec) { *out = *in in.Infrastructure.DeepCopyInto(&out.Infrastructure) in.ControlPlane.DeepCopyInto(&out.ControlPlane) in.Workers.DeepCopyInto(&out.Workers) + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]ClusterClassVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Patches != nil { + in, out := &in.Patches, &out.Patches + *out = make([]ClusterClassPatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassSpec. @@ -171,6 +209,22 @@ func (in *ClusterClassSpec) DeepCopy() *ClusterClassSpec { 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 + in.Schema.DeepCopyInto(&out.Schema) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassVariable. +func (in *ClusterClassVariable) DeepCopy() *ClusterClassVariable { + if in == nil { + return nil + } + out := new(ClusterClassVariable) + 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 @@ -308,6 +362,22 @@ func (in *ClusterStatus) DeepCopy() *ClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterVariable) DeepCopyInto(out *ClusterVariable) { + *out = *in + in.Value.DeepCopyInto(&out.Value) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterVariable. +func (in *ClusterVariable) DeepCopy() *ClusterVariable { + if in == nil { + return nil + } + out := new(ClusterVariable) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in @@ -431,6 +501,108 @@ func (in FailureDomains) DeepCopy() FailureDomains { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JSONPatch) DeepCopyInto(out *JSONPatch) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(JSONPatchValue) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatch. +func (in *JSONPatch) DeepCopy() *JSONPatch { + if in == nil { + return nil + } + out := new(JSONPatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JSONPatchValue) DeepCopyInto(out *JSONPatchValue) { + *out = *in + if in.Variable != nil { + in, out := &in.Variable, &out.Variable + *out = new(string) + **out = **in + } + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONPatchValue. +func (in *JSONPatchValue) DeepCopy() *JSONPatchValue { + if in == nil { + return nil + } + out := new(JSONPatchValue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JSONSchemaProps) DeepCopyInto(out *JSONSchemaProps) { + *out = *in + if in.MaxLength != nil { + in, out := &in.MaxLength, &out.MaxLength + *out = new(int64) + **out = **in + } + if in.MinLength != nil { + in, out := &in.MinLength, &out.MinLength + *out = new(int64) + **out = **in + } + if in.Maximum != nil { + in, out := &in.Maximum, &out.Maximum + *out = new(json.Number) + **out = **in + } + if in.Minimum != nil { + in, out := &in.Minimum, &out.Minimum + *out = new(json.Number) + **out = **in + } + if in.MultipleOf != nil { + in, out := &in.MultipleOf, &out.MultipleOf + *out = new(json.Number) + **out = **in + } + if in.Enum != nil { + in, out := &in.Enum, &out.Enum + *out = make([]apiextensionsv1.JSON, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONSchemaProps. +func (in *JSONSchemaProps) DeepCopy() *JSONSchemaProps { + if in == nil { + return nil + } + out := new(JSONSchemaProps) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalObjectTemplate) DeepCopyInto(out *LocalObjectTemplate) { *out = *in @@ -1166,6 +1338,95 @@ func (in *ObjectMeta) DeepCopy() *ObjectMeta { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchDefinition) DeepCopyInto(out *PatchDefinition) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + if in.JSONPatches != nil { + in, out := &in.JSONPatches, &out.JSONPatches + *out = make([]JSONPatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchDefinition. +func (in *PatchDefinition) DeepCopy() *PatchDefinition { + if in == nil { + return nil + } + out := new(PatchDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchSelector) DeepCopyInto(out *PatchSelector) { + *out = *in + in.MatchResources.DeepCopyInto(&out.MatchResources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSelector. +func (in *PatchSelector) DeepCopy() *PatchSelector { + if in == nil { + return nil + } + out := new(PatchSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchSelectorMatch) DeepCopyInto(out *PatchSelectorMatch) { + *out = *in + if in.ControlPlane != nil { + in, out := &in.ControlPlane, &out.ControlPlane + *out = new(bool) + **out = **in + } + if in.InfrastructureCluster != nil { + in, out := &in.InfrastructureCluster, &out.InfrastructureCluster + *out = new(bool) + **out = **in + } + if in.MachineDeploymentClass != nil { + in, out := &in.MachineDeploymentClass, &out.MachineDeploymentClass + *out = new(PatchSelectorMatchMachineDeploymentClass) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSelectorMatch. +func (in *PatchSelectorMatch) DeepCopy() *PatchSelectorMatch { + if in == nil { + return nil + } + out := new(PatchSelectorMatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchSelectorMatchMachineDeploymentClass) DeepCopyInto(out *PatchSelectorMatchMachineDeploymentClass) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSelectorMatchMachineDeploymentClass. +func (in *PatchSelectorMatchMachineDeploymentClass) DeepCopy() *PatchSelectorMatchMachineDeploymentClass { + if in == nil { + return nil + } + out := new(PatchSelectorMatchMachineDeploymentClass) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Topology) DeepCopyInto(out *Topology) { *out = *in @@ -1179,6 +1440,13 @@ func (in *Topology) DeepCopyInto(out *Topology) { *out = new(WorkersTopology) (*in).DeepCopyInto(*out) } + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]ClusterVariable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Topology. @@ -1207,6 +1475,22 @@ func (in *UnhealthyCondition) DeepCopy() *UnhealthyCondition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VariableSchema) DeepCopyInto(out *VariableSchema) { + *out = *in + in.OpenAPIV3Schema.DeepCopyInto(&out.OpenAPIV3Schema) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VariableSchema. +func (in *VariableSchema) DeepCopy() *VariableSchema { + if in == nil { + return nil + } + out := new(VariableSchema) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkersClass) DeepCopyInto(out *WorkersClass) { *out = *in diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 9bc997725fb3..5f9191e7a541 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -568,6 +568,236 @@ spec: required: - ref type: object + patches: + description: 'Patches defines the patches which are applied to customize + referenced templates of a ClusterClass. Note: Patches will be applied + in the order of the array.' + items: + description: ClusterClassPatch defines a patch which is applied + to customize the referenced templates. + properties: + definitions: + description: 'Definitions define the patches inline. Note: Patches + will be applied in the order of the array.' + items: + description: PatchDefinition defines a patch which is applied + to customize the referenced templates. + properties: + jsonPatches: + description: 'JSONPatches defines the patches which should + be applied on the templates matching the selector. Note: + Patches will be applied in the order of the array.' + items: + description: JSONPatch defines a JSON patch. + properties: + op: + description: 'Op defines the operation of the patch. + Note: Only `add`, `replace` and `remove` are supported.' + type: string + path: + description: 'Path defines the path of the patch. + Note: Only the spec of a template can be patched, + thus the path has to start with /spec/. Note: + For now the only allowed array modifications are + `append` and `prepend`, i.e.: * for op: `add`: + only index 0 (prepend) and - (append) are allowed + * for op: `replace` or `remove`: no indexes are + allowed' + type: string + value: + description: 'Value defines the value of the patch. + Note: Either Value or ValueFrom is required for + add and replace operations. Only one of them is + allowed to be set at the same time. Note: We have + to use apiextensionsv1.JSON instead of our JSON + type, because controller-tools has a hard-coded + schema for apiextensionsv1.JSON which cannot be + produced by another type (unset type field). Ref: + https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111' + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: 'ValueFrom defines the value of the + patch. Note: Either Value or ValueFrom is required + for add and replace operations. Only one of them + is allowed to be set at the same time.' + properties: + template: + description: 'Template is the Go template to + be used to calculate the value. A template + can reference variables defined in .spec.variables + and builtin variables. Note: The template + must evaluate to a valid YAML or JSON value.' + type: string + variable: + description: Variable is the variable to be + used as value. Variable can be one of the + variables defined in .spec.variables or a + builtin variable. + type: string + type: object + required: + - op + - path + type: object + type: array + selector: + description: Selector defines on which templates the patch + should be applied. + properties: + apiVersion: + description: APIVersion filters templates by apiVersion. + type: string + kind: + description: Kind filters templates by kind. + type: string + matchResources: + description: MatchResources selects templates based + on where they are referenced. + properties: + controlPlane: + description: 'ControlPlane selects templates referenced + in .spec.ControlPlane. Note: this will match + the controlPlane and also the controlPlane machineInfrastructure + (depending on the kind and apiVersion).' + type: boolean + infrastructureCluster: + description: InfrastructureCluster selects templates + referenced in .spec.infrastructure. + type: boolean + machineDeploymentClass: + description: MachineDeploymentClass selects templates + referenced in specific MachineDeploymentClasses + in .spec.workers.machineDeployments. + properties: + names: + description: Names selects templates by class + names. + items: + type: string + type: array + required: + - names + type: object + type: object + required: + - apiVersion + - kind + - matchResources + type: object + required: + - jsonPatches + - selector + type: object + type: array + name: + description: Name of the patch. + type: string + required: + - definitions + - name + type: object + type: array + variables: + description: Variables defines the variables which can be configured + in the Cluster topology and are then used in patches. + items: + description: ClusterClassVariable defines a variable which can be + configured in the Cluster topology and used in patches. + properties: + name: + description: Name of the variable. + 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: + default: + description: Default is the default value of the variable. + x-kubernetes-preserve-unknown-fields: true + enum: + description: Enum is the list of valid values of the + variable. + items: + x-kubernetes-preserve-unknown-fields: true + type: array + exclusiveMaximum: + description: ExclusiveMaximum specifies if the Maximum + is exclusive. + type: boolean + exclusiveMinimum: + description: ExclusiveMinimum specifies if the Minimum + is exclusive. + 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' + type: string + maxLength: + description: MaxLength is the max length of a string + variable. + 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. + type: string + minLength: + description: MinLength is the min length of a string + variable. + 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. + type: string + multipleOf: + description: MultipleOf specifies the number of which + the variable must be a multiple of. + type: string + nullable: + description: Nullable specifies if the variable can + be set to null. + type: boolean + pattern: + description: Pattern is the regex which a string variable + must match. + type: string + type: + description: 'Type is the type of the variable. Valid + values are: string, integer, number or boolean.' + type: string + required: + - type + type: object + required: + - openAPIV3Schema + type: object + required: + - name + - required + - schema + type: object + type: array workers: description: Workers describes the worker nodes for the cluster. It is a collection of node types which can be used to create the worker diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 0ecb60a52052..7cc7090c8375 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -874,6 +874,32 @@ spec: deployments. format: date-time type: string + variables: + description: Variables can be used to customize the Cluster through + patches. They must comply to the corresponding VariableClasses + defined in the ClusterClass. + items: + description: ClusterVariable can be used to customize the Cluster + through patches. It must comply to the corresponding ClusterClassVariable + defined in the ClusterClass. + properties: + name: + description: Name of the variable. + type: string + value: + description: 'Value of the variable. Note: the value will + be validated against the schema of the corresponding ClusterClassVariable + from the ClusterClass. Note: We have to use apiextensionsv1.JSON + instead of a custom JSON type, because controller-tools + has a hard-coded schema for apiextensionsv1.JSON which + cannot be produced by another type via controller-tools, + i.e. it''s not possible to have no type field. Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111' + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array version: description: The Kubernetes version of the cluster. type: string diff --git a/docs/proposals/202105256-cluster-class-and-managed-topologies.md b/docs/proposals/202105256-cluster-class-and-managed-topologies.md index 4ce1a2c1117d..63586cf30915 100644 --- a/docs/proposals/202105256-cluster-class-and-managed-topologies.md +++ b/docs/proposals/202105256-cluster-class-and-managed-topologies.md @@ -224,13 +224,13 @@ type ClusterClassSpec struct { // Variables defines the variables which can be configured // in the Cluster topology and are then used in patches. // +optional - Variables *VariablesClass `json:"variables,omitempty"` + Variables []ClusterClassVariable `json:"variables,omitempty"` // Patches defines the patches which are applied to customize // referenced templates of a ClusterClass. // Note: Patches will be applied in the order of the array. // +optional - Patches []PatchClass `json:"patches,omitempty"` + Patches []ClusterClassPatch `json:"patches,omitempty"` } // ControlPlaneClass defines the class for the control plane. @@ -292,19 +292,12 @@ type LocalObjectTemplate struct { } ``` -**VariablesClass** +**ClusterClassVariable** ```golang -// VariablesClass defines the variables which can be configured -// in the Cluster topology and used in patches. -type VariablesClass struct { - // Definitions define the variables inline. - Definitions []VariableDefinitionClass `json:"definitions,omitempty"` -} - -// VariableDefinitionClass defines a variable which can +// ClusterClassVariable defines a variable which can // be configured in the Cluster topology and used in patches. -type VariableDefinitionClass struct { +type ClusterClassVariable struct { // Name of the variable. Name string `json:"name"` @@ -315,11 +308,11 @@ type VariableDefinitionClass struct { Required bool `json:"required"` // Schema defines the schema of the variable. - Schema VariableDefinitionSchemaClass `json:"schema"` + Schema VariableSchema `json:"schema"` } -// VariableDefinitionSchemaClass defines the schema of a variable. -type VariableDefinitionSchemaClass struct{ +// VariableSchema defines the schema of a variable. +type VariableSchema struct{ // OpenAPIV3Schema defines the schema of a variable via OpenAPI v3 // schema. The schema is a subset of the schema used in // Kubernetes CRDs. @@ -340,11 +333,11 @@ for now (until further use cases emerge): - Through clusterctl we have an additional layer of templating for the Cluster resource which allows defaulting of Cluster variables via environment variables. -**PatchClass** +**ClusterClassPatch** ```golang -// PatchClass defines a patch which is applied to customize the referenced templates. -type PatchClass struct { +// ClusterClassPatch defines a patch which is applied to customize the referenced templates. +type ClusterClassPatch struct { // Name of the patch. Name string `json:"name"` @@ -356,26 +349,26 @@ type PatchClass struct { // PatchDefinition defines a patch which is applied to customize the referenced templates. type PatchDefinition struct { // Selector defines on which templates the patch should be applied. - Selector PatchDefinitionSelector `json:"selector"` + Selector PatchSelector `json:"selector"` // JSONPatches defines the patches which should be applied on the templates // matching the selector. // Note: Patches will be applied in the order of the array. - JSONPatches []JSONPatchDefinition `json:"jsonPatches"` + JSONPatches []JSONPatch `json:"jsonPatches"` } ``` **Note**: We are considering to add a field to allow defining optional patches. This would allow adding optional patches in the ClusterClass and then activating them on a per-cluster basis via opt-in. -**PatchDefinitionSelector** +**PatchSelector** ```golang -// PatchDefinitionSelector defines on which templates the patch should be applied. +// PatchSelector defines on which templates the patch should be applied. // Note: Matching on APIVersion and Kind is mandatory, to enforce that the patches are // written for the correct version. The version of the references may be automatically // updated during reconciliation if there is a newer version for the same contract. -type PatchDefinitionSelector struct { +type PatchSelector struct { // APIVersion filters templates by apiVersion. APIVersion string `json:"apiVersion"` @@ -383,13 +376,13 @@ type PatchDefinitionSelector struct { Kind string `json:"kind"` // MatchResources selects templates based on where they are referenced. - MatchResources PatchMatchResources `json:"matchResources"` + MatchResources PatchSelectorMatch `json:"matchResources"` } -// PatchMatchResources selects templates based on where they are referenced. +// PatchSelectorMatch selects templates based on where they are referenced. // Note: At least one of the fields must be set. // Note: The results of selection based on the individual fields are ORed. -type PatchMatchResources struct { +type PatchSelectorMatch struct { // ControlPlane selects templates referenced in .spec.ControlPlane. // Note: this will match the controlPlane and also the controlPlane // machineInfrastructure (depending on the kind and apiVersion). @@ -403,22 +396,22 @@ type PatchMatchResources struct { // MachineDeploymentClass selects templates referenced in specific MachineDeploymentClasses in // .spec.workers.machineDeployments. // +optional - MachineDeploymentClass *PatchMatchMachineDeploymentClass `json:"machineDeploymentClass,omitempty"` + MachineDeploymentClass *PatchSelectorMatchMachineDeploymentClass `json:"machineDeploymentClass,omitempty"` } -// PatchMatchMachineDeploymentClass selects templates referenced +// PatchSelectorMatchMachineDeploymentClass selects templates referenced // in specific MachineDeploymentClasses in .spec.workers.machineDeployments. -type PatchMatchMachineDeploymentClass struct { +type PatchSelectorMatchMachineDeploymentClass struct { // Names selects templates by class names. Names []string `json:"names"` } ``` -**JSONPatchDefinition** +**JSONPatch** ```golang -// JSONPatchDefinition defines a JSON patch. -type JSONPatchDefinition struct { +// JSONPatch defines a JSON patch. +type JSONPatch struct { // Op defines the operation of the patch. // Note: Only `add`, `replace` and `remove` are supported. Op string `json:"op"` @@ -440,12 +433,12 @@ type JSONPatchDefinition struct { // Note: Either Value or ValueFrom is required for add and replace // operations. Only one of them is allowed to be set at the same time. // +optional - ValueFrom *JSONPatchDefinitionValue `json:"valueFrom,omitempty"` + ValueFrom *JSONPatchValue `json:"valueFrom,omitempty"` } -// JSONPatchDefinitionValue defines the value of a patch. +// JSONPatchValue defines the value of a patch. // Note: Only one of the fields is allowed to be set at the same time. -type JSONPatchDefinitionValue struct { +type JSONPatchValue struct { // Variable is the variable to be used as value. // Variable can be one of the variables defined in .spec.variables or a builtin variable. // +optional @@ -498,14 +491,14 @@ Note: Builtin variables are defined in [Builtin variables](#builtin-variables) b // Variables can be used to customize the ClusterClass through // patches. They must comply to the corresponding - // VariableDefinitionClasses defined in the ClusterClass. + // ClusterClassVariable defined in the ClusterClass. // +optional - Variables []VariableTopology `json:"variables,omitempty"` + Variables []ClusterVariable `json:"variables,omitempty"` } ``` **Note**: We are intentionally using an array with named sub objects instead of a map, because: * it’s recommended by the [Kubernetes API conventions][] - * we want to be able to extend the `VariableTopology` struct in the future to get the values e.g. from secrets + * we want to be able to extend the `ClusterVariable` struct in the future to get the values e.g. from secrets * the list matches the way the variables are defined in the ClusterClass (and it’s also very similar to how e.g. env vars are defined on Pods) @@ -559,18 +552,18 @@ Note: Builtin variables are defined in [Builtin variables](#builtin-variables) b Replicas *int `json:"replicas,omitempty"` } ``` -1. The `VariableTopology` object represents an instance of a variable. +1. The `ClusterVariable` object represents an instance of a variable. ```golang - // VariableTopology can be used to customize the ClusterClass through + // ClusterVariable can be used to customize the ClusterClass through // patches. It must comply to the corresponding - // VariableDefinitionClass defined in the ClusterClass. - type VariableTopology struct { + // ClusterClassVariable defined in the ClusterClass. + type ClusterVariable struct { // Name of the variable. Name string `json:"name"` // Value of the variable. // Note: the value will be validated against the schema - // of the corresponding VariableDefinitionClass from the ClusterClass. + // of the corresponding ClusterClassVariable from the ClusterClass. Value apiextensionsv1.JSON `json:"value"` } ``` @@ -615,10 +608,10 @@ Builtin variables are available under the `builtin.` prefix. Some examples: - (defaulting) if namespace field is empty for a reference, default it to `metadata.Namespace` - all the reference must be in the same namespace of `metadata.Namespace` - `spec.workers.machineDeployments[i].class` field must be unique within a ClusterClass. - - `VariableDefinitionClass`: + - `ClusterClassVariable`: - names must be unique, not empty and not equal to `builtin` - schemas must be valid - - `PatchClass`: + - `ClusterClassPatch`: - names must be unique and not empty - `PatchDefinition`: - selector must have valid field values and match at least one template @@ -644,7 +637,7 @@ Builtin variables are available under the `builtin.` prefix. Some examples: - `spec.workers.machineDeployments[i].class` field must be unique within a ClusterClass. - `spec.workers.machineDeployments` supports adding new deployment classes. - changes should be compliant with the compatibility rules defined in this doc. - - `VariableDefinitionClass`: + - `ClusterClassVariable`: - names must be unique, not empty and not equal to `builtin` - schemas must be valid - schemas are mutable @@ -654,7 +647,7 @@ Builtin variables are available under the `builtin.` prefix. Some examples: - variables cannot be removed as long as they are used in Clusters - We are considering adding a field to allow deprecations of variables. When a variable would be deprecated no new usages of this variable would be allowed. - - `PatchClass` and `PatchDefinition`: same as for object creation + - `ClusterClassPatch` and `PatchDefinition`: same as for object creation ##### Cluster @@ -664,8 +657,8 @@ Builtin variables are available under the `builtin.` prefix. Some examples: - If `spec.topology` is set, `spec.topology.class` cannot be empty. - If `spec.topology` is set, `spec.topology.version` cannot be empty and must be a valid semver. - `spec.topology.workers.machineDeployments[i].name` field must be unique within a Cluster - - (defaulting) variables are defaulted according to the corresponding `VariableDefinitionClass` - - all required variables must exist and match the schema defined in the corresponding `VariableDefinitionClass` in the ClusterClass + - (defaulting) variables are defaulted according to the corresponding `ClusterClassVariable` + - all required variables must exist and match the schema defined in the corresponding `ClusterClassVariable` in the ClusterClass - For object updates: - If `spec.topology.class` is set it cannot be unset or modified, and if it's unset it cannot be set. @@ -673,8 +666,8 @@ Builtin variables are available under the `builtin.` prefix. Some examples: - `spec.topology.version` cannot be downgraded. - `spec.topology.workers.machineDeployments[i].name` field must be unique within a Cluster - A set of worker nodes can be added to or removed from the `spec.topology.workers.machineDeployments` list. - - (defaulting) variables are defaulted according to the corresponding `VariableDefinitionClass` - - all required variables must exist and match the schema defined in the corresponding `VariableDefinitionClass` in the ClusterClass + - (defaulting) variables are defaulted according to the corresponding `ClusterClassVariable` + - all required variables must exist and match the schema defined in the corresponding `ClusterClassVariable` in the ClusterClass #### ClusterClass compatibility @@ -844,17 +837,16 @@ to avoid creating separate ClusterClasses for every small deviation, e.g. a diff spec: [...] variables: - definitions: - - name: region - required: true - schema: - openAPIV3Schema: - type: string - - name: controlPlaneMachineType - schema: - openAPIV3Schema: - type: string - default: t3.large + - name: region + required: true + schema: + openAPIV3Schema: + type: string + - name: controlPlaneMachineType + schema: + openAPIV3Schema: + type: string + default: t3.large patches: - name: region definitions: