From ebeb2e2916f933c4a0bc20b929a3ed1d5ec5f179 Mon Sep 17 00:00:00 2001 From: killianmuldoon Date: Wed, 6 Apr 2022 13:31:24 +0100 Subject: [PATCH] Add API types for Runtime SDK ExtensionConfig Signed-off-by: killianmuldoon --- .golangci.yml | 3 + Makefile | 6 +- ...ime.cluster.x-k8s.io_extensionconfigs.yaml | 248 ++++++++++++++++++ config/manager/manager.yaml | 2 +- config/webhook/manifests.yaml | 44 ++++ exp/runtime/api/v1alpha1/doc.go | 18 ++ .../api/v1alpha1/extensionconfig_types.go | 198 ++++++++++++++ exp/runtime/api/v1alpha1/groupversion_info.go | 36 +++ .../api/v1alpha1/zz_generated.deepcopy.go | 233 ++++++++++++++++ feature/feature.go | 6 + internal/webhooks/runtime/doc.go | 18 ++ .../runtime/extensionconfig_webhook.go | 102 +++++++ .../runtime/extensionconfig_webhook_test.go | 102 +++++++ main.go | 11 + test/e2e/config/docker.yaml | 1 + 15 files changed, 1025 insertions(+), 3 deletions(-) create mode 100644 config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml create mode 100644 exp/runtime/api/v1alpha1/doc.go create mode 100644 exp/runtime/api/v1alpha1/extensionconfig_types.go create mode 100644 exp/runtime/api/v1alpha1/groupversion_info.go create mode 100644 exp/runtime/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 internal/webhooks/runtime/doc.go create mode 100644 internal/webhooks/runtime/extensionconfig_webhook.go create mode 100644 internal/webhooks/runtime/extensionconfig_webhook_test.go diff --git a/.golangci.yml b/.golangci.yml index 0cb83fc5046f..fa41d79de378 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -108,6 +108,9 @@ linters-settings: alias: addonsv1alpha4 - pkg: sigs.k8s.io/cluster-api/exp/addons/api/v1beta1 alias: addonsv1 + # CAPI exp runtime + - pkg: sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1 + alias: runtimev1 # CAPD - pkg: sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1alpha3 alias: infrav1alpha3 diff --git a/Makefile b/Makefile index d4f625e18f52..b20bb56b865c 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ help: # Display this help ALL_GENERATE_MODULES = core kubeadm-bootstrap kubeadm-control-plane .PHONY: generate -generate: ## Run all generate-manifests-*, generate-go-deepcopy-* and generate-go-conversions-* targets +generate: ## Run all generate-manifests-*, generate-go-deepcopy-*, generate-go-conversions-* targets $(MAKE) generate-modules generate-manifests generate-go-deepcopy generate-go-conversions $(MAKE) -C $(CAPD_DIR) generate @@ -196,6 +196,7 @@ generate-manifests-core: $(CONTROLLER_GEN) $(KUSTOMIZE) ## Generate manifests e. paths=./$(EXP_DIR)/internal/controllers/... \ paths=./$(EXP_DIR)/addons/api/... \ paths=./$(EXP_DIR)/addons/internal/controllers/... \ + paths=./$(EXP_DIR)/runtime/api/... \ crd:crdVersions=v1 \ rbac:roleName=manager-role \ output:crd:dir=./config/crd/bases \ @@ -243,6 +244,7 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co paths=./api/... \ paths=./$(EXP_DIR)/api/... \ paths=./$(EXP_DIR)/addons/api/... \ + paths=./$(EXP_DIR)/runtime/api/... \ paths=./cmd/clusterctl/... \ paths=./internal/test/builder/... @@ -799,7 +801,7 @@ $(CONVERSION_GEN): # Build conversion-gen from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(CONVERSION_GEN_PKG) $(CONVERSION_GEN_BIN) $(CONVERSION_GEN_VER) $(CONVERSION_VERIFIER): $(TOOLS_DIR)/go.mod # Build conversion-verifier from tools folder. - cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/conversion-verifier sigs.k8s.io/cluster-api/hack/tools/conversion-verifier + cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/$(CONVERSION_VERIFIER_BIN) sigs.k8s.io/cluster-api/hack/tools/conversion-verifier $(GOTESTSUM): # Build gotestsum from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOTESTSUM_PKG) $(GOTESTSUM_BIN) $(GOTESTSUM_VER) diff --git a/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml b/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml new file mode 100644 index 000000000000..fb4fc882a542 --- /dev/null +++ b/config/crd/bases/runtime.cluster.x-k8s.io_extensionconfigs.yaml @@ -0,0 +1,248 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: extensionconfigs.runtime.cluster.x-k8s.io +spec: + group: runtime.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: ExtensionConfig + listKind: ExtensionConfigList + plural: extensionconfigs + shortNames: + - ext + singular: extensionconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Time duration since creation of ExtensionConfig + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExtensionConfig is the Schema for the ExtensionConfig API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ExtensionConfigSpec is the desired state of the ExtensionConfig + properties: + clientConfig: + description: ClientConfig defines how to communicate with ExtensionHandlers. + properties: + caBundle: + description: CABundle is a PEM encoded CA bundle which will be + used to validate the ExtensionHandler's server certificate. + format: byte + type: string + service: + description: "Service is a reference to the Kubernetes service + for the ExtensionHandler. Either `service` or `url` must be + specified. \n If the ExtensionHandler is running within a cluster, + then you should use `service`." + properties: + name: + description: Name is the name of the service. + type: string + namespace: + description: Namespace is the namespace of the service. + type: string + path: + description: Path is an optional URL path which will be sent + in any request to this service. If a path is set it will + be used as prefix and the hook-specific path will be appended. + type: string + port: + description: Port is the port on the service that hosting + ExtensionHandler. Default to 8443. `port` should be a valid + port number (1-65535, inclusive). + format: int32 + type: integer + required: + - name + - namespace + type: object + url: + description: "URL gives the location of the ExtensionHandler, + in standard URL form (`scheme://host:port/path`). Exactly one + of `url` or `service` must be specified. \n The `host` should + not refer to a service running in the cluster; use the `service` + field instead. \n The scheme should be \"https\"; the URL should + begin with \"https://\". \"http\" is supported for insecure + development purposes only. \n A path is optional, and if present + may be any string permissible in a URL. If a path is set it + will be used as prefix and the hook-specific path will be appended. + \n Attempting to use a user or basic auth e.g. \"user:password@\" + is not allowed. Fragments (\"#...\") and query parameters (\"?...\") + are not allowed either." + type: string + type: object + namespaceSelector: + description: NamespaceSelector decides whether to run the webhook + on an object based on whether the namespace for that object matches + the selector. Default to the empty LabelSelector, which matches + everything. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + required: + - clientConfig + type: object + status: + description: ExtensionConfigStatus is the current state of the ExtensionConfig + properties: + conditions: + description: Conditions define the current service state of the ExtensionConfig. + items: + description: Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + handlers: + description: Handlers defines the current ExtensionHandlers supported + by an Extension. + items: + description: ExtensionHandler specifies the details of a handler + for a particular runtime hook registered by an Extension server. + properties: + failurePolicy: + description: FailurePolicy defines how failures in calls to + the ExtensionHandler should be handled by a client. Defaults + to Fail if not set. + type: string + name: + description: Name is the unique name of the ExtensionHandler. + type: string + requestHook: + description: RequestHook defines the versioned runtime hook + which this ExtensionHandler serves. + properties: + apiVersion: + description: APIVersion is the Version of the Hook. + type: string + hook: + description: Hook is the name of the hook. + type: string + required: + - apiVersion + - hook + type: object + timeoutSeconds: + description: TimeoutSeconds defines the timeout duration for + client calls to the ExtensionHandler. + format: int32 + type: integer + required: + - name + - requestHook + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 9a928eca182b..8590dfa92ad1 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -22,7 +22,7 @@ spec: args: - "--leader-elect" - "--metrics-bind-addr=localhost:8080" - - "--feature-gates=MachinePool=${EXP_MACHINE_POOL:=false},ClusterResourceSet=${EXP_CLUSTER_RESOURCE_SET:=false},ClusterTopology=${CLUSTER_TOPOLOGY:=false}" + - "--feature-gates=MachinePool=${EXP_MACHINE_POOL:=false},ClusterResourceSet=${EXP_CLUSTER_RESOURCE_SET:=false},ClusterTopology=${CLUSTER_TOPOLOGY:=false},RuntimeSDK=${EXP_RUNTIME_SDK:=false}" image: controller:latest name: manager ports: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 4e9b44e71ce9..08143916d7c0 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -159,6 +159,28 @@ webhooks: resources: - clusterclasses sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig + failurePolicy: Fail + matchPolicy: Equivalent + name: default.extensionconfig.runtime.addons.cluster.x-k8s.io + rules: + - apiGroups: + - runtime.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - extensionconfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -344,6 +366,28 @@ webhooks: resources: - clusterclasses sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-runtime-cluster-x-k8s-io-v1beta1-extensionconfig + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.extensionconfig.runtime.cluster.x-k8s.io + rules: + - apiGroups: + - runtime.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - extensionconfigs + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/exp/runtime/api/v1alpha1/doc.go b/exp/runtime/api/v1alpha1/doc.go new file mode 100644 index 000000000000..d7778dadc9ba --- /dev/null +++ b/exp/runtime/api/v1alpha1/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains the v1alpha1 implementation of ExtensionConfig. +package v1alpha1 diff --git a/exp/runtime/api/v1alpha1/extensionconfig_types.go b/exp/runtime/api/v1alpha1/extensionconfig_types.go new file mode 100644 index 000000000000..49829ec0a36d --- /dev/null +++ b/exp/runtime/api/v1alpha1/extensionconfig_types.go @@ -0,0 +1,198 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// ANCHOR: ExtensionConfigSpec + +// ExtensionConfigSpec defines the desired state of ExtensionConfig. +type ExtensionConfigSpec struct { + // ClientConfig defines how to communicate with ExtensionHandlers. + ClientConfig ClientConfig `json:"clientConfig"` + + // NamespaceSelector decides whether to run the webhook on an object based + // on whether the namespace for that object matches the selector. + // Default to the empty LabelSelector, which matches everything. + // +optional + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` +} + +// ClientConfig contains the information to make a client +// connection with an ExtensionHandler. +type ClientConfig struct { + // URL gives the location of the ExtensionHandler, in standard URL form + // (`scheme://host:port/path`). Exactly one of `url` or `service` + // must be specified. + // + // The `host` should not refer to a service running in the cluster; use + // the `service` field instead. + // + // The scheme should be "https"; the URL should begin with "https://". + // "http" is supported for insecure development purposes only. + // + // A path is optional, and if present may be any string permissible in + // a URL. If a path is set it will be used as prefix and the hook-specific + // path will be appended. + // + // Attempting to use a user or basic auth e.g. "user:password@" is not + // allowed. Fragments ("#...") and query parameters ("?...") are not + // allowed either. + // + // +optional + URL *string `json:"url,omitempty"` + + // Service is a reference to the Kubernetes service for the ExtensionHandler. + // Either `service` or `url` must be specified. + // + // If the ExtensionHandler is running within a cluster, then you should use `service`. + // + // +optional + Service *ServiceReference `json:"service,omitempty"` + + // CABundle is a PEM encoded CA bundle which will be used to validate the ExtensionHandler's server certificate. + // +optional + CABundle []byte `json:"caBundle,omitempty"` +} + +// ServiceReference holds a reference to a Kubernetes Service. +type ServiceReference struct { + // Namespace is the namespace of the service. + Namespace string `json:"namespace"` + + // Name is the name of the service. + Name string `json:"name"` + + // Path is an optional URL path which will be sent in any request to + // this service. If a path is set it will be used as prefix and the hook-specific + // path will be appended. + // +optional + Path *string `json:"path,omitempty"` + + // Port is the port on the service that hosting ExtensionHandler. + // Default to 8443. + // `port` should be a valid port number (1-65535, inclusive). + // +optional + Port *int32 `json:"port,omitempty"` +} + +// ANCHOR_END: ExtensionConfigSpec + +// ANCHOR: ExtensionConfigStatus + +// ExtensionConfigStatus defines the observed state of ExtensionConfig. +type ExtensionConfigStatus struct { + // Handlers defines the current ExtensionHandlers supported by an Extension. + // +optional + // +listType=map + // +listMapKey=name + Handlers []ExtensionHandler `json:"handlers,omitempty"` + + // Conditions define the current service state of the ExtensionConfig. + // +optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +// ExtensionHandler specifies the details of a handler for a particular runtime hook registered by an Extension server. +type ExtensionHandler struct { + // Name is the unique name of the ExtensionHandler. + Name string `json:"name"` + + // RequestHook defines the versioned runtime hook which this ExtensionHandler serves. + RequestHook GroupVersionHook `json:"requestHook"` + + // TimeoutSeconds defines the timeout duration for client calls to the ExtensionHandler. + // +optional + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + + // FailurePolicy defines how failures in calls to the ExtensionHandler should be handled by a client. + // Defaults to Fail if not set. + // +optional + FailurePolicy *FailurePolicy `json:"failurePolicy,omitempty"` +} + +// GroupVersionHook defines the runtime hook when the ExtensionHandler is called. +type GroupVersionHook struct { + // APIVersion is the Version of the Hook. + APIVersion string `json:"apiVersion"` + + // Hook is the name of the hook. + Hook string `json:"hook"` +} + +// FailurePolicy specifies a failure policy that defines how unrecognized errors from the admission endpoint are handled. +type FailurePolicy string + +const ( + // FailurePolicyIgnore means that an error calling the extension is ignored. + FailurePolicyIgnore FailurePolicy = "Ignore" + + // FailurePolicyFail means that an error calling the extension is propagated as an error. + FailurePolicyFail FailurePolicy = "Fail" +) + +// ANCHOR_END: ExtensionConfigStatus + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=extensionconfigs,shortName=ext,scope=Namespaced,categories=cluster-api +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of ExtensionConfig" + +// ExtensionConfig is the Schema for the ExtensionConfig API. +type ExtensionConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // ExtensionConfigSpec is the desired state of the ExtensionConfig + Spec ExtensionConfigSpec `json:"spec,omitempty"` + + // ExtensionConfigStatus is the current state of the ExtensionConfig + Status ExtensionConfigStatus `json:"status,omitempty"` +} + +// GetConditions returns the set of conditions for this object. +func (e *ExtensionConfig) GetConditions() clusterv1.Conditions { + return e.Status.Conditions +} + +// SetConditions sets the conditions on this object. +func (e *ExtensionConfig) SetConditions(conditions clusterv1.Conditions) { + e.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// ExtensionConfigList contains a list of ExtensionConfig. +type ExtensionConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ExtensionConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ExtensionConfig{}, &ExtensionConfigList{}) +} + +const ( + // RuntimeExtensionDiscovered is a condition set on an ExtensionConfig object once it has been discovered by the Runtime SDK client. + RuntimeExtensionDiscovered clusterv1.ConditionType = "Discovered" +) diff --git a/exp/runtime/api/v1alpha1/groupversion_info.go b/exp/runtime/api/v1alpha1/groupversion_info.go new file mode 100644 index 000000000000..bac33a979769 --- /dev/null +++ b/exp/runtime/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +kubebuilder:object:generate=true +// +groupName=runtime.cluster.x-k8s.io + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "runtime.cluster.x-k8s.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..a87475e6fb21 --- /dev/null +++ b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,233 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientConfig) DeepCopyInto(out *ClientConfig) { + *out = *in + if in.URL != nil { + in, out := &in.URL, &out.URL + *out = new(string) + **out = **in + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceReference) + (*in).DeepCopyInto(*out) + } + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConfig. +func (in *ClientConfig) DeepCopy() *ClientConfig { + if in == nil { + return nil + } + out := new(ClientConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionConfig) DeepCopyInto(out *ExtensionConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfig. +func (in *ExtensionConfig) DeepCopy() *ExtensionConfig { + if in == nil { + return nil + } + out := new(ExtensionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExtensionConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionConfigList) DeepCopyInto(out *ExtensionConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExtensionConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigList. +func (in *ExtensionConfigList) DeepCopy() *ExtensionConfigList { + if in == nil { + return nil + } + out := new(ExtensionConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExtensionConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionConfigSpec) DeepCopyInto(out *ExtensionConfigSpec) { + *out = *in + in.ClientConfig.DeepCopyInto(&out.ClientConfig) + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigSpec. +func (in *ExtensionConfigSpec) DeepCopy() *ExtensionConfigSpec { + if in == nil { + return nil + } + out := new(ExtensionConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionConfigStatus) DeepCopyInto(out *ExtensionConfigStatus) { + *out = *in + if in.Handlers != nil { + in, out := &in.Handlers, &out.Handlers + *out = make([]ExtensionHandler, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(v1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConfigStatus. +func (in *ExtensionConfigStatus) DeepCopy() *ExtensionConfigStatus { + if in == nil { + return nil + } + out := new(ExtensionConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionHandler) DeepCopyInto(out *ExtensionHandler) { + *out = *in + out.RequestHook = in.RequestHook + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.FailurePolicy != nil { + in, out := &in.FailurePolicy, &out.FailurePolicy + *out = new(FailurePolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionHandler. +func (in *ExtensionHandler) DeepCopy() *ExtensionHandler { + if in == nil { + return nil + } + out := new(ExtensionHandler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupVersionHook) DeepCopyInto(out *GroupVersionHook) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupVersionHook. +func (in *GroupVersionHook) DeepCopy() *GroupVersionHook { + if in == nil { + return nil + } + out := new(GroupVersionHook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceReference) DeepCopyInto(out *ServiceReference) { + *out = *in + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceReference. +func (in *ServiceReference) DeepCopy() *ServiceReference { + if in == nil { + return nil + } + out := new(ServiceReference) + in.DeepCopyInto(out) + return out +} diff --git a/feature/feature.go b/feature/feature.go index 354e4bf40826..79ab03fc305d 100644 --- a/feature/feature.go +++ b/feature/feature.go @@ -45,6 +45,11 @@ const ( // alpha: v0.4 ClusterTopology featuregate.Feature = "ClusterTopology" + // RuntimeSDK is a feature gate for the Runtime hooks and extensions functionality. + // + // alpha: v1.2 + RuntimeSDK featuregate.Feature = "RuntimeSDK" + // KubeadmBootstrapFormatIgnition is a feature gate for the Ignition bootstrap format // functionality. // @@ -64,4 +69,5 @@ var defaultClusterAPIFeatureGates = map[featuregate.Feature]featuregate.FeatureS ClusterResourceSet: {Default: true, PreRelease: featuregate.Beta}, ClusterTopology: {Default: false, PreRelease: featuregate.Alpha}, KubeadmBootstrapFormatIgnition: {Default: false, PreRelease: featuregate.Alpha}, + RuntimeSDK: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/internal/webhooks/runtime/doc.go b/internal/webhooks/runtime/doc.go new file mode 100644 index 000000000000..6bf9f066c8ef --- /dev/null +++ b/internal/webhooks/runtime/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package runtime contains the webhook implementation for runtime ExtensionConfig. +package runtime diff --git a/internal/webhooks/runtime/extensionconfig_webhook.go b/internal/webhooks/runtime/extensionconfig_webhook.go new file mode 100644 index 000000000000..eac945e5e2fe --- /dev/null +++ b/internal/webhooks/runtime/extensionconfig_webhook.go @@ -0,0 +1,102 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" + "sigs.k8s.io/cluster-api/feature" +) + +// ExtensionConfig is the webhook for runtimev1.ExtensionConfig. +type ExtensionConfig struct{} + +func (webhook *ExtensionConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&runtimev1.ExtensionConfig{}). + WithDefaulter(webhook). + WithValidator(webhook). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=validation.extensionconfig.runtime.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-runtime-cluster-x-k8s-io-v1alpha1-extensionconfig,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=runtime.cluster.x-k8s.io,resources=extensionconfigs,versions=v1alpha1,name=default.extensionconfig.runtime.addons.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +var _ webhook.CustomValidator = &ExtensionConfig{} +var _ webhook.CustomDefaulter = &ExtensionConfig{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type. +func (webhook *ExtensionConfig) Default(ctx context.Context, obj runtime.Object) error { + extensionConfig, ok := obj.(*runtimev1.ExtensionConfig) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj)) + } + // Default NamespaceSelector to an empty LabelSelector, which matches everything, if not set. + if extensionConfig.Spec.NamespaceSelector == nil { + extensionConfig.Spec.NamespaceSelector = &metav1.LabelSelector{} + } + return nil +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (webhook *ExtensionConfig) ValidateCreate(ctx context.Context, obj runtime.Object) error { + extensionConfig, ok := obj.(*runtimev1.ExtensionConfig) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", obj)) + } + return webhook.validate(ctx, nil, extensionConfig) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (webhook *ExtensionConfig) ValidateUpdate(ctx context.Context, old, updated runtime.Object) error { + oldExtensionConfig, ok := old.(*runtimev1.ExtensionConfig) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", old)) + } + newExtensionConfig, ok := updated.(*runtimev1.ExtensionConfig) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected an ExtensionConfig but got a %T", updated)) + } + return webhook.validate(ctx, oldExtensionConfig, newExtensionConfig) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (webhook *ExtensionConfig) validate(_ context.Context, _, _ *runtimev1.ExtensionConfig) error { + // NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag; the web hook + // must prevent creating and updating objects in case the feature flag is disabled. + if !feature.Gates.Enabled(feature.RuntimeSDK) { + return field.Forbidden( + field.NewPath("spec"), + "can be set only if the RuntimeSDK feature flag is enabled", + ) + } + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (webhook *ExtensionConfig) ValidateDelete(_ context.Context, _ runtime.Object) error { + return nil +} diff --git a/internal/webhooks/runtime/extensionconfig_webhook_test.go b/internal/webhooks/runtime/extensionconfig_webhook_test.go new file mode 100644 index 000000000000..3e17dde9ae6c --- /dev/null +++ b/internal/webhooks/runtime/extensionconfig_webhook_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runtime + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilfeature "k8s.io/component-base/featuregate/testing" + "k8s.io/utils/pointer" + + runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" + "sigs.k8s.io/cluster-api/feature" +) + +var ( + fakeScheme = runtime.NewScheme() +) + +func init() { + _ = runtimev1.AddToScheme(fakeScheme) +} + +func TestExtensionConfigValidationFeatureGated(t *testing.T) { + extension := &runtimev1.ExtensionConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-extension", + }, + Spec: runtimev1.ExtensionConfigSpec{ + ClientConfig: runtimev1.ClientConfig{ + URL: pointer.String("https://extension-address.com"), + }, + }, + } + updatedExtension := extension.DeepCopy() + updatedExtension.Spec.ClientConfig.URL = pointer.StringPtr("https://a-new-extension-address.com") + tests := []struct { + name string + new *runtimev1.ExtensionConfig + old *runtimev1.ExtensionConfig + featureGate bool + expectErr bool + }{ + { + name: "creation should fail if feature flag is disabled", + new: extension, + featureGate: false, + expectErr: true, + }, + { + name: "update should fail if feature flag is disabled", + old: extension, + new: updatedExtension, + featureGate: false, + expectErr: true, + }, + { + name: "creation should succeed if feature flag is enabled", + new: extension, + featureGate: true, + expectErr: false, + }, + { + name: "update should fail if feature flag is enabled", + old: extension, + new: updatedExtension, + featureGate: true, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.featureGate)() + webhook := ExtensionConfig{} + g := NewWithT(t) + err := webhook.validate(context.TODO(), tt.old, tt.new) + if tt.expectErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + }) + } +} diff --git a/main.go b/main.go index 356e2596dd01..bae1953b25a1 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,9 @@ import ( expv1alpha4 "sigs.k8s.io/cluster-api/exp/api/v1alpha4" expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" expcontrollers "sigs.k8s.io/cluster-api/exp/controllers" + runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" "sigs.k8s.io/cluster-api/feature" + runtimev1webhooks "sigs.k8s.io/cluster-api/internal/webhooks/runtime" "sigs.k8s.io/cluster-api/version" "sigs.k8s.io/cluster-api/webhooks" ) @@ -105,6 +107,8 @@ func init() { _ = addonsv1alpha4.AddToScheme(scheme) _ = addonsv1.AddToScheme(scheme) + _ = runtimev1.AddToScheme(scheme) + // +kubebuilder:scaffold:scheme } @@ -465,6 +469,13 @@ func setupWebhooks(mgr ctrl.Manager) { setupLog.Error(err, "unable to create webhook", "webhook", "MachineHealthCheck") os.Exit(1) } + + // NOTE: ExtensionConfig is behind the RuntimeSDK feature gate flag. The webhook will prevent creating or updating + // new objects if the feature flag is disabled. + if err := (&runtimev1webhooks.ExtensionConfig{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ExtensionConfig") + os.Exit(1) + } } func concurrency(c int) controller.Options { diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml index 8272eddbf93f..6598373f982d 100644 --- a/test/e2e/config/docker.yaml +++ b/test/e2e/config/docker.yaml @@ -219,6 +219,7 @@ variables: EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION: "true" EXP_MACHINE_POOL: "true" CLUSTER_TOPOLOGY: "true" + EXP_RUNTIME_SDK: "true" # NOTE: INIT_WITH_BINARY and INIT_WITH_KUBERNETES_VERSION are only used by the clusterctl upgrade test to initialize # the management cluster to be upgraded. # NOTE: We test the latest release with a previous contract.