From cf6c01d59ce137748d35907d83793b503908835b Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Tue, 28 Jun 2022 14:38:40 +0200 Subject: [PATCH] book: extend Runtime SDK documentation & update proposal --- docs/book/src/SUMMARY.md | 6 +- .../images/runtime-sdk-lifecycle-hooks.png | Bin 0 -> 53679 bytes .../runtime-sdk-topology-mutation.plantuml | 56 ++ .../images/runtime-sdk-topology-mutation.png | Bin 0 -> 73258 bytes .../cluster-class/index.md | 2 +- .../experimental-features.md | 2 +- .../experimental-features/runtime-sdk.md | 12 - .../runtime-sdk/deploy-runtime-extension.md | 51 ++ .../runtime-sdk/implement-extensions.md | 265 ++++++++++ .../runtime-sdk/implement-lifecycle-hooks.md | 233 +++++++++ .../implement-topology-mutation-hook.md | 129 +++++ .../runtime-sdk/index.md | 29 + docs/proposals/20220221-runtime-SDK.md | 494 ++++++------------ .../20220330-topology-mutation-hook.md | 109 +--- docs/proposals/20220414-runtime-hooks.md | 222 +------- test/extension/main.go | 4 +- 16 files changed, 942 insertions(+), 672 deletions(-) create mode 100644 docs/book/src/images/runtime-sdk-lifecycle-hooks.png create mode 100644 docs/book/src/images/runtime-sdk-topology-mutation.plantuml create mode 100644 docs/book/src/images/runtime-sdk-topology-mutation.png delete mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk.md create mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk/deploy-runtime-extension.md create mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md create mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md create mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md create mode 100644 docs/book/src/tasks/experimental-features/runtime-sdk/index.md diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 55558523e6d2..71ddcb8a07d5 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -24,7 +24,11 @@ - [Writing a ClusterClass](./tasks/experimental-features/cluster-class/write-clusterclass.md) - [Changing a ClusterClass](./tasks/experimental-features/cluster-class/change-clusterclass.md) - [Operating a managed Cluster](./tasks/experimental-features/cluster-class/operate-cluster.md) - - [Runtime SDK](./tasks/experimental-features/runtime-sdk.md) + - [Runtime SDK](tasks/experimental-features/runtime-sdk/index.md) + - [Implementing Runtime Extensions](./tasks/experimental-features/runtime-sdk/implement-extensions.md) + - [Implementing Lifecycle Hook Extensions](./tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md) + - [Implementing Topology Mutation Hook Extensions](./tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md) + - [Deploying Runtime Extensions](./tasks/experimental-features/runtime-sdk/deploy-runtime-extension.md) - [Ignition Bootstrap configuration](./tasks/experimental-features/ignition.md) - [Security Guidelines](./security/index.md) - [Pod Security Standards](./security/pod-security-standards.md) diff --git a/docs/book/src/images/runtime-sdk-lifecycle-hooks.png b/docs/book/src/images/runtime-sdk-lifecycle-hooks.png new file mode 100644 index 0000000000000000000000000000000000000000..7153ee288aef8c510c6589d77bdc944dbfbae89d GIT binary patch literal 53679 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 "Cluster Topology Controller": Cluster reconcile event +activate "Cluster Topology Controller" + +"Cluster Topology Controller" -> "API Server": Get current Cluster topology +activate "API Server" +"API Server" -> "Cluster Topology Controller": +deactivate "API Server" + +group Compute desired State + "Cluster Topology Controller" -> "Cluster Topology Controller": Compute desired State + loop Ordered list of Patches + alt + "Cluster Topology Controller" -> "Cluster Topology Controller": Generate inline patches + else + "Cluster Topology Controller" -> "External Patch Extensions": Generate external patches + activate "External Patch Extensions" + "External Patch Extensions" -> "Cluster Topology Controller": + deactivate "External Patch Extensions" + end + "Cluster Topology Controller" -> "Cluster Topology Controller": Apply patches to desired State + end loop + + loop External Patches + "Cluster Topology Controller" -> "External Patch Extensions": ValidateTopology + activate "External Patch Extensions" + "External Patch Extensions" -> "Cluster Topology Controller": + deactivate "External Patch Extensions" + end loop +end group + +"Cluster Topology Controller" -> "API Server": Reconcile Cluster topology + +deactivate "Cluster Topology Controller" + +hide footbox +@enduml diff --git a/docs/book/src/images/runtime-sdk-topology-mutation.png b/docs/book/src/images/runtime-sdk-topology-mutation.png new file mode 100644 index 0000000000000000000000000000000000000000..c26f52ee782a366d70b4c00630ccf63b94c93eb4 GIT binary patch literal 73258 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 Rl}F%@ELuS_S6t8I{{cu{qapwR literal 0 HcmV?d00001 diff --git a/docs/book/src/tasks/experimental-features/cluster-class/index.md b/docs/book/src/tasks/experimental-features/cluster-class/index.md index 59433a4b8173..d708451fc465 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/index.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/index.md @@ -16,7 +16,7 @@ In order to use the ClusterClass (alpha) experimental feature the Kubernetes Ver **Variable name to enable/disable the feature gate**: `CLUSTER_TOPOLOGY` Additional documentation: -* Background information: [ClusterClass and Managed Topologies CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210526-cluster-class-and-managed-topologies.md) +* Background information: [ClusterClass and Managed Topologies CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210526-cluster-class-and-managed-topologies.md) * For ClusterClass authors: * [Writing a ClusterClass](./write-clusterclass.md) * [Changing a ClusterClass](./change-clusterclass.md) diff --git a/docs/book/src/tasks/experimental-features/experimental-features.md b/docs/book/src/tasks/experimental-features/experimental-features.md index c8d43115643e..3564980e34ff 100644 --- a/docs/book/src/tasks/experimental-features/experimental-features.md +++ b/docs/book/src/tasks/experimental-features/experimental-features.md @@ -78,7 +78,7 @@ kubectl describe -n capi-system deployment.apps/capi-controller-manager * [ClusterResourceSet](./cluster-resource-set.md) * [ClusterClass](./cluster-class/index.md) * [Ignition Bootstrap configuration](./ignition.md) -* [Runtime SDK](./runtime-sdk.md) +* [Runtime SDK](runtime-sdk/index.md) **Warning**: Experimental features are unreliable, i.e., some may one day be promoted to the main repository, or they may be modified arbitrarily or even disappear altogether. In short, they are not subject to any compatibility or deprecation promise. diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk.md b/docs/book/src/tasks/experimental-features/runtime-sdk.md deleted file mode 100644 index f27a2b5cd0eb..000000000000 --- a/docs/book/src/tasks/experimental-features/runtime-sdk.md +++ /dev/null @@ -1,12 +0,0 @@ -# Experimental Feature: Runtime SDK - -The Runtime SDK feature provides an extensibility mechanism that allows systems, products, and services built on top of Cluster API to hook into a workload cluster’s lifecycle. - - -**Feature gate name**: `RuntimeSDK` - -**Variable name to enable/disable the feature gate**: `EXP_RUNTIME_SDK` - - -More details on the Runtime SDK can be found at: -[RuntimeSDK CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220221-runtime-SDK.md) diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/deploy-runtime-extension.md b/docs/book/src/tasks/experimental-features/runtime-sdk/deploy-runtime-extension.md new file mode 100644 index 000000000000..ff8a7e67f816 --- /dev/null +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/deploy-runtime-extension.md @@ -0,0 +1,51 @@ +# Deploy Runtime Extensions + + + +Cluster API requires that each Runtime Extension must be deployed using an endpoint accessible from the Cluster API +controllers; additionally, Runtime Extensions must always be executed out of process (not in the same process as +the Cluster API runtime). + +This proposal assumes that the default solution that implementers are going to adopt is to deploy Runtime Extension +in the management cluster by: + +- Packing the Runtime Extension in a container image; +- Using a Kubernetes Deployment to run the above container inside the Management Cluster; +- Using a Cluster IP service to make the Runtime Extension instances accessible via a stable DNS name; +- Using a cert-manager generated Certificate to protect the endpoint. + +There are a set of important guidelines that must be considered while choosing the deployment method: + +## Availability + +It is recommended that Runtime Extensions should leverage some form of load-balancing, to provide high availability +and performance benefits; you can run multiple Runtime Extension backends behind a service to leverage the +load-balancing that services support. + +## Identity and access management + +The security model for each Runtime Extensions should be carefully defined, similar to any other application deployed +in the Cluster: the deployment must use a dedicated service account with limited RBAC permission. + +On top of that, the container image for the Runtime Extension should be carefully designed in order to avoid +privilege escalation (e.g using [distroless](https://github.com/GoogleContainerTools/distroless) base images) and +the Pod spec in the Deployment manifest should enforce security best practices (e.g. do not use privileged pods etc.). + +## Alternative deployments methods + +Alternative deployment methods can be used given that the requirement about the HTTP endpoint accessibility +is satisfied, like e.g. + +- deploying the HTTP Server as a part of another component, e.g. a controller; +- deploying the HTTP Server outside the Management Cluster. + +In these cases above recommendations about availability and identity and access management apply as well. + +## Example + +For an example, please see our [test extension](https://github.com/kubernetes-sigs/cluster-api/tree/main/test/extension) +which follows the kubebuilder setup we usually use for our controllers as close as possible. diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md new file mode 100644 index 000000000000..70664c5bb461 --- /dev/null +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md @@ -0,0 +1,265 @@ +# Implementing Runtime Extensions + + + +## Introduction + +As a developer building systems on top of Cluster API, if you want to hook in the Cluster’s lifecycle via +a Runtime Hook, you are required to implement a Runtime Extension handling requests according to the +OpenAPI specification for the Runtime Hook you are interested in. + +Runtime Extensions by design are very powerful and flexible, however given that with great power comes +great responsibility, a few key consideration should always be kept in mind (more details in the following paragraphs): + +- Runtime Extension are components that should be designed, written and deployed with great caution given that they + can affect the proper functioning of the Cluster API runtime. +- Cluster administrators should carefully vet any Runtime Extension registration, thus preventing malicious components + from being added to the system. + +Please note that following similar practices is already commonly accepted in the Kubernetes ecosystem for +Kubernetes API server admission webhooks, and Runtime Extensions share the same foundation and most of the same +considerations/concerns apply. + +## Implementation + +As mentioned above as a developer building systems on top of Cluster API, if you want to hook in the Cluster’s lifecycle via a +Runtime Extension, you have to implement an HTTP server handling a discovery request and a set of +additional requests according to the OpenAPI specification for the Runtime Hook you are interested in. + +The following shows a minimal example of a Runtime Extension server implementation: + +```go +var ( + catalog = runtimecatalog.New() + setupLog = ctrl.Log.WithName("setup") + + // Flags. + profilerAddress string + webhookPort int + webhookCertDir string + logOptions = logs.NewOptions() +) + +func init() { + // Register the RuntimeHook types into the catalog. + _ = runtimehooksv1.AddToCatalog(catalog) +} + +// InitFlags initializes the flags. +func InitFlags(fs *pflag.FlagSet) { + logs.AddFlags(fs, logs.SkipLoggingConfigurationFlags()) + logOptions.AddFlags(fs) + + fs.StringVar(&profilerAddress, "profiler-address", "", "Bind address to expose the pprof profiler (e.g. localhost:6060)") + + fs.IntVar(&webhookPort, "webhook-port", 9443, "Webhook Server port") + + fs.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/", + "Webhook cert dir, only used when webhook-port is specified.") +} + +func main() { + InitFlags(pflag.CommandLine) + pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + + if err := logOptions.ValidateAndApply(nil); err != nil { + setupLog.Error(err, "unable to start extension") + os.Exit(1) + } + + // klog.Background will automatically use the right logger. + ctrl.SetLogger(klog.Background()) + + if profilerAddress != "" { + klog.Infof("Profiler listening for requests at %s", profilerAddress) + go func() { + klog.Info(http.ListenAndServe(profilerAddress, nil)) + }() + } + + ctx := ctrl.SetupSignalHandler() + + webhookServer, err := server.NewServer(server.Options{ + Catalog: catalog, + Port: webhookPort, + CertDir: webhookCertDir, + }) + if err != nil { + setupLog.Error(err, "error creating webhook server") + os.Exit(1) + } + + if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{ + Hook: runtimehooksv1.BeforeClusterCreate, + Name: "before-cluster-create", + HandlerFunc: DoBeforeClusterCreate, + TimeoutSeconds: pointer.Int32(5), + FailurePolicy: toPtr(runtimehooksv1.FailurePolicyFail), + }); err != nil { + setupLog.Error(err, "error adding handler") + os.Exit(1) + } + + if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{ + Hook: runtimehooksv1.DoBeforeClusterUpgrade, + Name: "before-cluster-upgrade", + HandlerFunc: DoBeforeClusterUpgrade, + TimeoutSeconds: pointer.Int32(5), + FailurePolicy: toPtr(runtimehooksv1.FailurePolicyFail), + }); err != nil { + setupLog.Error(err, "error adding handler") + os.Exit(1) + } + + setupLog.Info("Starting Runtime Extension server", "version", version.Get().String()) + if err := webhookServer.Start(ctx); err != nil { + setupLog.Error(err, "error running webhook server") + os.Exit(1) + } +} + +func DoBeforeClusterCreate(ctx context.Context, request *runtimehooksv1.BeforeClusterCreateRequest, response *runtimehooksv1.BeforeClusterCreateResponse) { + log := ctrl.LoggerFrom(ctx) + log.Info("BeforeClusterCreate is called") + // Your actual implementation... +} + +func DoBeforeClusterUpgrade(ctx context.Context, request *runtimehooksv1.BeforeClusterUpgradeRequest, response *runtimehooksv1.BeforeClusterUpgradeResponse) { + log := ctrl.LoggerFrom(ctx) + log.Info("BeforeClusterUpgrade is called") + // Your actual implementation... +} + +func toPtr(f runtimehooksv1.FailurePolicy) *runtimehooksv1.FailurePolicy { + return &f +} +``` + +For a full example see our [test extension](https://github.com/kubernetes-sigs/cluster-api/tree/main/test/extension). + +Please note that each Runtime Extension server can answer to calls for multiple Runtime Hooks (in the example above +`BeforeClusterCreate` and `BeforeClusterUpgrade`) at the same time. Each of them are handled at a different path, like the +Kubernetes API server does for different API resources. The exact format of those paths is handled by the `server` +package automatically. + +There is an additional `Discovery` endpoint which is automatically handled by the `Server`. The `Discovery` endpoint +returns a list of the implemented Runtime Hooks to inform Cluster API which Runtime Hooks are implemented by this +Runtime Extension server. + +Please note that Cluster API only enforces request and response types as defined by a Runtime Hook version, +and developers are fully responsible for all other elements of the design of a Runtime Extension implementation, +including: + +- To choose which programming language to use; please note that for sake of this proposal Golang is the language + of choice, and we are not planning to test/provide tooling/libraries for other languages. Nevertheless, given that + we rely on Open API and plain HTTP(s) calls, other languages should just work but support will be provided at + best effort. +- To choose if to have a dedicated HTTP server(s) for Runtime Extensions only or if to use the HTTP server for other + purposes as well (e.g. to serve a metric endpoint). + +In case the Runtime Extension is developed in Golang, the implementer can benefit from the following packages (exported by +`sigs.k8s.io/cluster-api`) as shown in the example above: + +- `exp/runtime/hooks/api/v1alpha1` contains the Runtime Hook Golang API types, which are also used to generate the OpenAPI specification. +- `exp/runtime/catalog` provides the `Catalog` object to register Runtime Hook definitions. The `Catalog` is then used by the `server` + package to handle requests. `Catalog` is similar to the `runtime.Scheme` of the `k8s.io/apimachinery/pkg/runtime` + package just for Runtime Hooks. +* `./exp/runtime/server`: The `server` package provides a `Server` object which makes it easy to implement + a Runtime Extension server. The `Server` will automatically handle tasks like Marshal/Unmarshal. A Runtime Extension + developer only has to implement a strongly typed function that contains the actual extension implementation. + +## Guidelines + +While writing the actual code of the Runtime Extension a set of important guidelines must be considered: + +### Timeouts + +Runtime Extension processing adds to network request latency, they should run as quickly as possible +(typically in milliseconds); Cluster Administrator will be allowed to configure how long the Cluster API Runtime +should wait for a Runtime Extension to respond before treating the call as a failure (max 10s). + +### Availability + +Runtime Extension failure could result in errors in handling the workload clusters lifecycle, and so the implementation +should be robust, have proper error handling, avoid panics, etc.. It will be allowed to set up +failure policies preventing a Runtime Extension failure to have negative effects on the Cluster API Runtime, but +this option can’t be used in all use cases (see [Error Management](#error-management)). + +### Blocking Hooks + +A Runtime Hook can be defined as "blocking", e.g. the `BeforeClusterUpgrade` hook allows a Runtime Extension +to prevent the upgrade to start. A Runtime Extension registered for the `BeforeClusterUpgrade` hook can block +by retuning a non-zero `retryAfterSeconds` value. Following consideration apply: + +- The system might decide to retry the same Runtime Extension even before the `retryAfterSeconds` period expires, + e.g. due to other changes in the Cluster, so `retryAfterSeconds` should be considered as an approximate maximum + time before the next reconcile. +- If there is more than one Runtime Extension registered for the same Runtime Hook and more than one returns + `retryAfterSeconds`, the shortest non-zero value will be used. +- If there is more than one Runtime Extension registered for the same Runtime Hook and at least one returns + `retryAfterSeconds`, all Runtime Extension will be re-tried. + +Detailed description of what "blocking" means for each specific Runtime Hooks will be documented case by case. + +### Side Effects + +It is recommended that Runtime Extensions should avoid side effects if possible, which means to operate only on +the content of the request sent to them, and not make out-of-band changes. +If side effects are required, rules defined in the following paragraphs apply. + +### Idempotence + +An idempotent Runtime Extension is able to successfully accomplish its task also in case it has already been completed +(the Runtime Extension checks current state and changes it only if necessary). + +A practical example that explains why idempotence is relevant is the fact that extensions could be called more than once +for the same lifecycle transition, e.g. + +- Two Runtime Extensions are registered for the `BeforeClusterUpgrade` hook. +- Before a Cluster upgrades both extensions are called, but one of them temporarily block the operation asking to retry after 30s. +- After 30s the system retries the lifecycle transition, and both the extensions are called again to re-evaluate + if it is now possible to proceed with the Cluster upgrade. + +### Avoid dependencies + +Each Runtime Extension should accomplish its task without depending on other Runtime Extensions. +Introducing dependencies across Runtime Extensions makes the system fragile, and it is probably a consequence of +poor "Separation of Concerns" while designing such components. + +### Deterministic result + +A deterministic Runtime Extension is implemented in such a way that given the same input it will always return +the same output. + +Some Runtime Hook, like e.g. external patches, might explicitly request for corresponding +Runtime Extensions to support this property, but we encourage developers to follow +this pattern more generally given that it fits well with practices like unit testing and +generally makes the entire system more predictable and easier to troubleshoot. + +### Error Management + +In case a Runtime Extension returns an error, the error will be handled according to the corresponding FailurePolicy +defined in the response to the Discovery call. + +If the failure policy is `Ignore` the error is going to be recorded in controller's logs, but the processing will continue; +however we recognize that this failure policy cannot be used in most of the use cases because Runtime Extension +implementers want to ensure that the task implemented by an extension is completed before continuing with the cluster's lifecycle. + +If instead the failure policy is `Fail` the system will retry the operation until it passes. +Following general considerations apply: + +- It is the responsibility of Cluster API components to surface Runtime Extension errors using conditions. +- Operations will be retried with an exponential backoff or whenever the state of Cluster changes (we are going to rely + on controller runtime exponential backoff/watches). +- If the operation is defined as "blocking", the error is going to block a lifecycle transition, e.g. returning a non-zero + `retryAfterSeconds` on a Runtime Extension for the `BeforeClusterUpgrade` hook is going to block the Cluster upgrade. +- If there is more than one Runtime Extension registered for the same Runtime Hook and at least one of them fails, + all the registered Runtime Extension will be retried. see [Idempotence](#idempotence) + +Additional considerations about errors that apply only to a specific Runtime Hook will be documented case by case. diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md new file mode 100644 index 000000000000..1cfc8bb69968 --- /dev/null +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md @@ -0,0 +1,233 @@ +# Implementing Lifecycle Hook Runtime Extensions + + + +## Introduction + +The lifecycle hooks allow hooking into the Cluster lifecycle. The following diagram provides an overview: + +![Lifecycle Hooks overview](../../../images/runtime-sdk-lifecycle-hooks.png) + +Please see the corresponding [CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220414-runtime-hooks.md) for additional background information. + +## Guidelines + +All guidelines defined in [Implementing Runtime Extensions](implement-extensions.md#guidelines) apply to the implementation of Runtime Extensions for lifecycle hooks. + +TL;DR; Runtime Extensions are components that should be designed, written and deployed with great caution given that they can affect the proper functioning of the Cluster API runtime. A poorly implemented Runtime Extension could potentially block lifecycle transitions from happening. + +Following recommendations are especially relevant: + +* [Blocking and non Blocking](implement-extensions.md#blocking-hooks) +* [Error management](implement-extensions.md#error-management) +* [Avoid dependencies](implement-extensions.md#avoid-dependencies) + +## Definitions + +### Before Cluster Create + +This hook is called after the Cluster object has been created by the user, immediately before all the objects which are part of a Cluster topology(*) are going to be created. Runtime Extension implementers can use this hook to determine/prepare add-ons for the Cluster and block the creation of those objects until everything is ready. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterCreateRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterCreateResponse +status: Success +message: "error message if status == Failure" +retryAfterSeconds: 10 +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). + +(*) The objects which are part of a Cluster topology are the infrastructure Cluster, the control plane, the MachineDeployments and the templates derived from the ClusterClass. + +### After Control Plane Initialized + +This hook is called after the ControlPlane for the Cluster is marked as available for the first time. Runtime Extension implementers can use this hook to execute tasks, for example component installation on workload clusters, that are only possible once the Control Plane is available. This hook does not block any further changes to the Cluster. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterControlPlaneInitializedRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterControlPlaneInitializedResponse +status: Success +message: "error message if status == Failure" +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). + +### Before Cluster Upgrade + +This hook is called after the Cluster object has been updated with a new spec.topology.version by the user, and immediately before the new version is going to be propagated to the control plane (*). Runtime Extension implementers can use this hook to execute pre-upgrade add-on tasks and block upgrades of the ControlPlane and Workers. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterUpgradeRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +fromKubernetesVersion: "v1.21.2" +toKubernetesVersion: "v1.22.0" +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterUpgradeResponse +status: Success +message: "error message if status == Failure" +retryAfterSeconds: 10 +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). + +* Under normal circumstances spec.topology.version gets propagated to the control plane immediately; however if previous upgrades or worker machine rollouts are still in progress, the system waits for those operations to complete before starting the new upgrade. + +### After Control Plane Upgrade + +This hook is called after the control plane has been upgraded to the version specified in spec.topology.version, and immediately before the new version is going to be propagated to the MachineDeployments existing in the Cluster. Runtime Extension implementers can use this hook to execute post-upgrade add-on tasks and block upgrades to workers until everything is ready. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterControlPlaneUpgradeRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +kubernetesVersion: "v1.22.0" +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterControlPlaneUpgradeResponse +status: Success +message: "error message if status == Failure" +retryAfterSeconds: 10 +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). + +### After Cluster Upgrade + +This hook is called after the Cluster, control plane and workers have been upgraded to the version specified in spec.topology.version. Runtime Extensions implementers can use this hook to execute post-upgrade add-on tasks. This hook does not block any further changes or upgrades to the Cluster. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterClusterUpgradeRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +kubernetesVersion: "v1.22.0" +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: AfterClusterUpgradeResponse +status: Success +message: "error message if status == Failure" +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). + +### Before Cluster Delete + +This hook is called after the Cluster has been deleted by the user, and immediately before objects existing in the Cluster are going to be deleted. Runtime Extension implementers can use this hook to execute cleanup tasks for the add-ons and block deletion of the Cluster and descendant objects until everything is ready. + +#### Example Request: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterDeleteRequest +cluster: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: Cluster + metadata: + name: test-cluster + namespace: test-ns + spec: + ... + status: + ... +``` + +#### Example Response: + +```yaml +apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 +kind: BeforeClusterDeleteResponse +status: Success +message: "error message if status == Failure" +retryAfterSeconds: 10 +``` + +For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md new file mode 100644 index 000000000000..07bc228c863b --- /dev/null +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md @@ -0,0 +1,129 @@ +# Implementing Topology Mutation Hook Runtime Extensions + + + +## Introduction + +The Topology Mutation Hooks are going to be called during each Cluster topology reconciliation. More specifically we are going to call two different hooks for each reconciliation: + +* **GeneratePatches**: GeneratePatches is responsible for generating patches for the entire Cluster topology. +* **ValidateTopology**: ValidateTopology is called after all patches have been applied and thus allows the External Patch Extension developer to validate the resulting objects. + +![Cluster topology reconciliation](../../../images/runtime-sdk-topology-mutation.png) + +Please see the corresponding [CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220330-topology-mutation-hook.md) for additional background information. + +## When to use inline vs external patches + +FIXME(sbueringer): pro/cons (partially from proposal) +* Considerations for external patches: + * trade off writing many external patch RuntimeExtensions vs one + * many => many http calls + operational/management overhead + Conway Law (:)) + * one/few => granular patches inside of one external patch RuntimeExtension + * some mechanism to switch them on/off (e.g. variable value/existence or config for the RuntimeExtensions (ConfigMap/cmd flags)) + +## Guidelines + +For general Runtime Extension developer guidelines please refer to the guidelines in [Implementing Runtime Extensions](implement-extensions.md#guidelines). This section outlines considerations specific to Topology Mutation hooks: + +* **Input validation**: An External Patch Extension must always validate its input, i.e. it must validate that all variables exist and have the right type and it must validate the kind and apiVersion of the templates which should be patched. +* **Timeouts**: As External Patch Extensions are called during each Cluster topology reconciliation, they must respond as fast as possible (<=200ms) to avoid delaying individual reconciles and congestion. +* **Availability**: An External Patch Extension must be always available, otherwise Cluster topologies won’t be reconciled anymore. +* **Side Effects**: An External Patch Extension must not make out-of-band changes. If necessary external data can be retrieved, but be aware of performance impact. +* **Deterministic results**: For a given request (a set of templates and variables) an External Patch Extension must always return the same response (a set of patches). Otherwise the Cluster topology will never reach a stable state. +* **Idempotence**: An External Patch Extension must only return patches if changes to the templates are required, i.e. unnecessary patches when the template is already in the desired state must be avoided. +* **Avoid Dependencies**: An External Patch Extension must be independent of other External Patch Extensions. However if dependencies cannot be avoided, it is possible to control the order in which patches are executed via the ClusterClass. + +## Definitions + +### GeneratePatches Hook + +A GeneratePatches call generates patches for the entire Cluster topology. Accordingly the request contains all templates, the global variables and the template-specific variables. The response contains generated patches. + +Example request: +* Generating patches for a Cluster topology is done via a single call to allow External Patch Extensions a holistic view of the entire Cluster topology. Additionally this allows us to reduce the number of round-trips. +* Each item in the request will contain the template as a raw object. Additionally information about where the template is used is provided via `holderReference`. +```yaml +variables: +- name: + value: + ... +items: +- uid: 7091de79-e26c-4af5-8be3-071bc4b102c9 + holderReference: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: MachineDeployment + namespace: default + name: cluster-md1-xyz + fieldPath: spec.template.spec.infrastructureRef + object: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: AWSMachineTemplate + spec: + ... + variables: + - name: + value: + ... +``` + +Example response: +* The response contains patches instead of full objects to reduce the payload. +* Templates in the request and patches in the response will be correlated via UIDs. +* Like for inline patches external patches are only allowed to change `spec.template.spec`. +```yaml +status: Success # or Failure +message: "error message if status == Failure" +items: +- uid: 7091de79-e26c-4af5-8be3-071bc4b102c9 + patchType: JSONPatch + patch: +``` + +The full OpenAPI specification (draft) of the GeneratePatches hook can be seen via the [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml). + +During implementation we will consider introducing a library to facilitate development of External Patch Extensions. It will provide capabilities like: +* Access builtin variables +* Extract certain templates from a GeneratePatches request (e.g. all bootstrap templates) + +### ValidateTopology Hook + +A ValidateTopology call validates the topology after all patches have been applied. The request contains all templates of the Cluster topology, the global variables and the template-specific variables. The response contains the result of the validation. + +Example request: +* The request is the same as the GeneratePatches request except the `uid` fields. We don't + need them as we don't have to correlate anything in the response. +```yaml +variables: +- name: + value: + ... +items: +- holderReference: + apiVersion: cluster.x-k8s.io/v1beta1 + kind: MachineDeployment + namespace: default + name: cluster-md1-xyz + fieldPath: spec.template.spec.infrastructureRef + object: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: AWSMachineTemplate + spec: + ... + variables: + - name: + value: + ... +``` + +Example response: +```yaml +status: Success # or Failure +message: "error message if status == Failure" +``` + +The full OpenAPI specification (draft) of the ValidateTopology hook can be seen via the [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml). diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/index.md b/docs/book/src/tasks/experimental-features/runtime-sdk/index.md new file mode 100644 index 000000000000..3d1ad3bf7b07 --- /dev/null +++ b/docs/book/src/tasks/experimental-features/runtime-sdk/index.md @@ -0,0 +1,29 @@ +# Experimental Feature: Runtime SDK + +The Runtime SDK feature provides an extensibility mechanism that allows systems, products, and services built on top of Cluster API to hook into a workload cluster’s lifecycle. + + + + + +**Feature gate name**: `RuntimeSDK` + +**Variable name to enable/disable the feature gate**: `EXP_RUNTIME_SDK` + +Additional documentation: + +* Background information: [Runtime SDK CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220221-runtime-SDK.md) +* For Runtime Extension developers: + * [Implementing Runtime Extensions](./implement-extensions.md) + * [Implementing Lifecycle Hook Extensions](./implement-lifecycle-hooks.md) + * [Implementing Topology Mutation Hook Extensions](./implement-topology-mutation-hook.md) +* For Cluster operators: + * [Deploying Runtime Extensions](./deploy-runtime-extension.md) diff --git a/docs/proposals/20220221-runtime-SDK.md b/docs/proposals/20220221-runtime-SDK.md index 347cc822f69c..8220eccc423c 100644 --- a/docs/proposals/20220221-runtime-SDK.md +++ b/docs/proposals/20220221-runtime-SDK.md @@ -4,7 +4,7 @@ authors: - "@fabriziopandini" - "@sbueringer" - "@vincepri" -reviewers: + reviewers: - "@CecileRobertMichon" - "@enxebre" - "@ykakarap" @@ -74,13 +74,13 @@ superseded-by: Refer to the [Cluster API Book Glossary](https://cluster-api.sigs.k8s.io/reference/glossary.html). -- **Cluster API Runtime**: identifies the Cluster API execution model, a set of controllers cooperating in managing the +- **Cluster API Runtime**: identifies the Cluster API execution model, a set of controllers cooperating in managing the workload cluster’s lifecycle. -- **Runtime SDK**: a set of rules, recommendations and fundamental capabilities required to develop Runtime Hooks and +- **Runtime SDK**: a set of rules, recommendations and fundamental capabilities required to develop Runtime Hooks and Runtime Extensions. - **Runtime Hook**: a single, well identified, extension point allowing applications built on top of Cluster API to hook - into specific moments of the workload cluster’s lifecycle, e.g. Cluster.BeforeUpgrade, Machine.BeforeRemediation. -- **Runtime Extension**: an external component which is part of a system/product built on top of Cluster API that can + into specific moments of the workload cluster’s lifecycle, e.g. `BeforeClusterUpgrade`, `BeforeMachineRemediation`. +- **Runtime Extension**: an external component which is part of a system/product built on top of Cluster API that can handle requests for a specific Runtime Hook. ## Summary @@ -105,7 +105,7 @@ Instead, with the growing adoption of Cluster API as a common layer to manage fl now a new category of systems, products and services built on top of Cluster API that require strict interactions with the lifecycle of Clusters, but at the same time they do not want to replace any “low-level” components in Cluster API, because they happily benefit from all the features available in the existing providers (built on top vs -plug-in/swap). +plug-in/swap). A common approach for this problem has been to watch for Cluster API resources; another approach has been to implement API Server admission webhooks to alter CAPI resources, but both approaches are limited by the fact that the system @@ -120,12 +120,12 @@ other lifecycle moments. This proposal aims to solve the above problem in a more structured and generic way, by introducing the Runtime SDK, a set of rules, recommendations and fundamental capabilities required to implement a new extensibility mechanism that will allow systems, products and services built on top of Cluster API to hook in the workload cluster’s -lifecycle. +lifecycle. The key elements of the above extensibility mechanism are Runtime Hooks and Runtime Extensions. Runtime Hooks and Runtime Extensions are designed to be powerful and flexible, and _by opportunity_ it will be also -possible to use this capability for allowing the user to hook into Cluster API reconcile loops at "low level", e.g. +possible to use this capability for allowing the user to hook into Cluster API reconcile loops at "low level", e.g. by allowing a Runtime Extension providing external patches to be executed on every topology reconcile. ### Goals @@ -164,7 +164,7 @@ To define the Runtime SDK and more specifically ### User Stories - As a cluster operator I want to be able to execute a particular action in well-defined moments of the Workload - Cluster’s lifecycle, e.g. + Cluster’s lifecycle, e.g. - As a cluster operator I want to automatically install the external CPI addon Before Upgrading the Cluster. - As a cluster operator I want to automatically check my quota management systems Before Creating a cluster. - As a cluster operator I want to automatically run Kubernetes conformance tests After a Cluster upgrade completes. @@ -221,7 +221,7 @@ Runtime Hooks are inspired by Kubernetes admission webhooks, but there is one ke - Admission webhooks are strictly linked to Kubernetes API Server/etcd **CRUD operations** e.g. Create or Update Cluster in etcd. -- Runtime Hooks can be used to define **arbitrary operations**, e.g. Cluster.BeforeUpgrade, Machine.Remediate etc. +- Runtime Hooks can be used to define **arbitrary operations**, e.g. `BeforeClusterUpgrade`, `BeforeMachineRemediate` etc. In other words, Runtime Hooks are not concerned about “low-level” details of how Kubernetes handles objects in the API Server/etcd; Runtime Hooks instead focus on “high-level” events of a Cluster’s lifecycle. @@ -232,7 +232,7 @@ defined in the following paragraphs. #### Runtime SDK rules -As this proposal is based on RESTful APIs, we are using [OpenAPI Specification v3.0.0](https://swagger.io/specification/) [1] +As this proposal is based on RESTful APIs, we are using [OpenAPI Specification v3.0.0](https://swagger.io/specification/) [1] to document Runtime Hooks supported by Cluster API. Most specifically, a single OpenAPI document providing specification for all the Runtime Hooks supported by a @@ -243,12 +243,12 @@ book as well e.g. ![overview](images/runtime-sdk/swagger-ui.png) Each Runtime Hook will be defined by one (or more) RESTful APIs implemented as a `POST` operation; each operation -is going to receive an input parameter as a request body, and return an output value as response body, both +is going to receive a request parameter as a request body, and return a response value as response body, both `application/json` encoded and with a schema of arbitrary complexity that should be considered an integral part of the Runtime Hook definition. It is also worth noting that more than one version of the same Runtime Hook might be supported at the same time; -e.g. in the example above the `Cluster.BeforeUpgrade` Hook exist in version `v1alpha1` (old version) +e.g. in the example above the `BeforeClusterUpgrade` Hook exist in version `v1alpha1` (old version) and `v1alpha2` (current). Supporting more versions at the same time is a requirement in order to: @@ -279,231 +279,10 @@ mechanism allowing to: ## Runtime Extensions developer guide -As a developer building systems on top of Cluster API, if you want to hook in the Cluster’s lifecycle via -a Runtime Hook, you are required to implement a Runtime Extension handling requests according to the -OpenAPI specification for the Runtime Hook you are interested in. +The following sections have been moved to the book to avoid duplication: -Runtime Extensions by design are very powerful and flexible, however given that with great power comes -great responsibility, a few key consideration should always be kept in mind (more details in the following paragraphs): - -- Runtime Extension are components that should be designed, written and deployed with great caution given that they - can affect the proper functioning of the Cluster API runtime. -- Cluster administrators should carefully vet any Runtime Extension registration, thus preventing malicious components - from being added to the system. - -Please note that following similar practices is already commonly accepted in the Kubernetes ecosystem for -Kubernetes API server admission webhooks, and Runtime Extensions share the same foundation and most of the same -considerations/concerns apply. - -### Implementing Runtime Extensions - -As a developer building systems on top of Cluster API, if you want to hook in the Cluster’s lifecycle via a -Runtime Extension, you are required to implement an HTTP server handling a discovery request and a set of -additional requests according to the OpenAPI specification for the Runtime Hook you are interested in. - -E.g. - -```go -// Note: this is pseudo code, meant to demonstrate that implementing a RuntimeExtension requires only minimal scaffolding; -// the exact details will be defined during implementation, possibly taking advantage of Golang generics. - -var c = catalog.NewCatalog() - -func init() { - v1alpha2.AddToCatalog(c) -} - -func main() { - ctx := context.Background() - - listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", "8082")) - if err != nil { - panic(err) - } - - // Build an HTTP handler for a given operation, calling a strongly typed func at each request - beforeUgradeHandler, err := catalogHTTP.NewHandlerBuilder(). - WithCatalog(c). - AddService(&v1alpha2.DiscoveryHook{}, doDiscovery). - AddService(&v1alpha2.BeforeUgradeHook{}, doBeforeUpgrade). - Build() - if err != nil { - panic(err) - } - - srv := &http.Server{ - Handler: BeforeUgradeHandler, - } - if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { - panic(err) - } -} - -func doDiscovery(in *v1alpha2.DiscoveryInput, out *v1alpha2.DiscoveryOutput) error { - out.Items = append(out.Items, - *v1alpha2.DiscoveryExtension{ - name: "upgradeAddons", - hook: *v1alpha2.DiscoveryHook{ - apiVersion: "hook.runtime.cluster.x-k8s.io/v1alpha1", - name: "BeforeUpgrade", - }, - timeoutSeconds: 5, - failurePolicy: *v1alpha2.FailurePolicyFail, - }) - return nil -} - -func doBeforeUpgrade(in *v1alpha2.BeforeUpgradeInput, out *v1alpha2.BeforeUpgradeOutput) error { - // your actual implementation... - return nil -} -``` - -Please note that each runtime extension server could answer to different hooks calls (in the -example above `Discovery` and `BeforeUpgrade`) each one of them handled at different path, like API server does -for the different API resources. The exact format of those paths will be defined during the implementation -and this document updated accordingly. - -[Discovery](#discovery-hook) is an operation that allows each runtime extension server to inform Cluster API of the -list of Runtime Extension it implements, their version and other features. - -Please note that Cluster API only enforces input and output parameters types as defined by a Runtime Hook version, -and developers are fully responsible for all the other elements of the design of a Runtime Extension implementation, -including: - -- To choose which programming language to use; please note that for sake of this proposal Golang is the language - of choice, and we are not planning to test/provide tooling/libraries for other languages. Nevertheless, given that - we rely on Open API and plain HTTP(s) calls, other languages should just work but support will be provided at - best effort. -- To choose if to have a dedicated HTTP server(s) for Runtime Extensions only or if to use the HTTP server for other - purposes as well (e.g. metric endpoint). - -In case the Runtime Extension is being developed in Golang, the implementer can benefit from importing -`sigs.k8s.io/cluster-api` as show in the example above and: - -- Use Golang types defined under `/runtime` (the types from which the OpenAPI specification has been generated). -- Use the RuntimeExtension catalog object to generate the skeleton of the HTTP handler for a given Runtime extension. - The generated func will take care of scaffolding tasks like Marshal/Unmarshal; the only missing part will be to - implement a strongly typed func that contains the actual extension implementation. - -While writing the actual code of the Runtime Extension a set of important guidelines must be considered: - -#### Timeouts -Runtime Extension processing adds to network request latency, they should run as quickly as possible -(typically in milliseconds); Cluster Administrator will be allowed to configure how long the Cluster API Runtime -should wait for a Runtime Extension to respond before treating the call as a failure (max 10s). - -#### Availability -Runtime Extension failure could result in errors in handling Workload’s Clusters lifecycle, and so the implementation -should be robust, have proper error handling, avoid panics, etc.; It will be allowed to set up -failure policies preventing a Runtime Extension failure to have negative effects on the Cluster API Runtime, but -this option can’t be used in all the use cases. see [Error Management](#error-management) - -#### Blocking Hooks -A Runtime Hook can be defined as "blocking", e.g. the BeforeClusterUpgrade hook allows a Runtime Extension -to prevent the upgrade to start. - -A Runtime Extension registered for the above hook will be allowed to block by retuning a `retryAfterSeconds` value. -Following consideration apply: - -- The system might decide to retry the same Runtime Extension even before the `retryAfterSeconds` period expires, - e.g. due to other changes in the Cluster, so retry after should be considered as an approximate maximum - time before the next reconcile. -- If there is more than one Runtime Extension registered for the same Runtime Hook and more than one returns - `retryAfterSeconds`, the shortest one will be used. -- If there is more than one Runtime Extension registered for the same Runtime Hook and at least one returns - `retryAfterSeconds`, all the Runtime Extension be executed when the operation will be re-tried. - -Detailed description of what "blocking" means for each specific Runtime Hooks will be documented case by case. - -#### Side Effects -It is recommended that Runtime Extensions should avoid side effects if possible, which means to operate only on -the content of the Input/Output sent to them, and not make out-of-band changes. -If side effects are required, rules defined in the following paragraphs apply. - -#### Idempotence -An idempotent Runtime Extension is able to successfully accomplish its task also in case it has already been completed -(the Runtime Extension checks current state and changes it only if necessary). - -A practical example that explains why idempotence is relevant is the fact that extension could be called more than once -for the same lifecycle transition, e.g. - -- Two RuntimeExtension are registered for the BeforeUpgrade hook. -- Before a Cluster upgrades both extensions are called, but one of them temporarily block the operation asking to retry after 30s. -- After 30s the system retries the lifecycle transition, and both the extensions are called again to re-evaluate - if it is now possible to proceed with Cluster upgrade. - -#### Avoid dependencies -Each Runtime Extension should accomplish its task without dependency or relations with other Runtime Extensions. -Introducing dependencies across Runtime Extensions makes the system fragile, and it is probably a consequence of -poor “Separation of Concerns” while designing such components. - -#### Deterministic result -A deterministic Runtime Extension is implemented in such a way that given the same input it will always return -the same output. - -Some Runtime Hook, like e.g. external patches, might explicitly request for corresponding -Runtime Extensions to support this property, but we encourage developers to follow -this pattern more generally given that it fits well with practices like unit testing and -generally makes the entire system more predictable and easier to troubleshoot. - -#### Error Management -In case a Runtime Extension returns an error, the error will be handled according to the corresponding FailurePolicy -defined in the response to the Discovery call. - -If the failure policy is `Ignore` the error is going to be recorded in controller's logs, but the processing will continue; -however we recognize that this failure policy cannot be used in most of the use cases because Runtime Extension -implementers want to ensure that some task is completed before continuing with the cluster's lifecycle. - -If instead the failure policy is `Fail` the system will retry the operation until it passes. -Following general considerations apply: - -- It is responsibility of Cluster API components to surface RuntimeExtension errors using conditions. -- Operations will be retried with an exponential backoff or whenever the state of Cluster changes (we are going to rely - on controller runtime exponential backoff/watches). -- If the operation is defined as "blocking", the error is going to block a lifecycle transition, - e.g. an error on a Runtime Extension for the BeforeClusterUpgrade hook is going to block the Cluster upgrade to start. -- If there is more than one Runtime Extension registered for the same Runtime Hook and at least one of them fails, - all the registered Runtime Extension will be retried. see [Idempotence](#idempotence) - -Additional consideration about errors that apply to a specific Runtime Hooks only will be documented case by case. - -### Deploy Runtime Extensions -Cluster API requires that each Runtime Extension must be deployed using an endpoint accessible from the Cluster API -controllers; additionally, Runtime Extensions must always be executed out of process (not in the same process as -the Cluster API runtime). - -This proposal assumes that the default solution that implementers are going to adopt is to deploy Runtime Extension -in the Management Cluster by: - -- Packing the Runtime Extension in a container image; -- Using a Kubernetes Deployment to run the above container inside the Management Cluster; -- Using a Cluster IP service to make the Runtime Extension instances accessible via a stable DNS name; -- Using a cert-manager generated Certificate to protect the endpoint. - -There are a set of important guidelines that must be considered while choosing the deployment method: - -#### Availability -It is recommended that Runtime Extensions should leverage some form of load-balancing, to provide high availability -and performance benefits; you can run multiple Runtime Extension backends behind a service to leverage the -load-balancing that services support. - -#### Identity and access management -The security model for each Runtime Extensions should be carefully defined, similar to any other application deployed -in the Cluster: the deployment must use a dedicated service account with limited RBAC permission. - -On top of that, the container image for the Runtime Extension should be carefully designed in order to avoid -privilege escalation (e.g using [distroless](https://github.com/GoogleContainerTools/distroless) base images) and -the Pod spec in the Deployment manifest should enforce security best practices (e.g. do not use privileged pods etc.). - -#### Alternative deployments methods -Alternative deployment methods can be used given that the requirement about the HTTP endpoint accessibility -is satisfied, like e.g. - -- deploying the HTTP Server as a part of another component, e.g. a controller; -- deploying the HTTP Server outside the Management Cluster. - -In these cases above recommendations about availability and identity and access management apply as well. +* [Implementing Runtime Extensions](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md) +* [Deploying Runtime Extensions](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md) ### Registering Runtime Extensions @@ -516,7 +295,7 @@ By registering a Runtime Extension the Cluster API Runtime becomes aware of a Ru Runtime Hook, and as a consequence the runtime starts calling the extension at well-defined moments of the workload cluster’s lifecycle. -This process has many similarities with registering dynamic webhooks in Kubernetes, but some specific +This process has many similarities with registering dynamic webhooks in Kubernetes, but some specific behavior is introduced by this proposal: The Cluster administrator is required to register available Runtime Extension server using the following CR: @@ -544,7 +323,7 @@ spec: ``` Once the extension is registered the [discovery hook](#discovery-hook) is called and the above CR is updated with the list -of the Runtime Extensions supported by the server. The ExtensionConfig is Cluster scoped, meaning it has no namespace. The `namespaceSelector` will enable targeting of a subset of Clusters. +of the Runtime Extensions supported by the server. The ExtensionConfig is Cluster scoped, meaning it has no namespace. The `namespaceSelector` will enable targeting of a subset of Clusters. ```yaml @@ -569,7 +348,7 @@ status: As you can notice, each Runtime Extension is given a unique identifier that can be used to reference it from other part of the system, e.g. from ClusterClass. Additionally, it is documented the exact reference to the hook/version -the Runtime Extension is implementing as well as the failurePolicy and the timeout the system should use when +the Runtime Extension is implementing as well as the failurePolicy and the timeout the system should use when calling the extension. If consensus is reached/in a follow-up iteration we consider to eventually add support for defining @@ -584,12 +363,12 @@ objectSelector: ``` -Instead, unless there's a strong and evident need for it, we are not considering adding support for defining -dependencies among Runtime Extensions, being it modeled with something similar to +Instead, unless there's a strong and evident need for it, we are not considering adding support for defining +dependencies among Runtime Extensions, being it modeled with something similar to [systemd unit options](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) or alternative approaches. The main reason behind that is that such type of feature introduces complexity and creates "pet" like relations across -components making the overall system more fragile. This is also consistent with the [avoid dependencies](#avoid-dependencies) +components making the overall system more fragile. This is also consistent with the [avoid dependencies](#avoid-dependencies) recommendation above. ## Runtime Hooks developer guide (CAPI internals) @@ -603,98 +382,117 @@ The process of implementing the new Runtime Hooks is intentionally designed in o used to define API types, thus providing a familiar experience to the maintainers/the people used to look at the Cluster API codebase. Most specifically: -- Runtime Hooks versions must be defined under a `/runtime` folder. -- For each Runtime Hook, there must be one version, each one defined in its own folder e.g. `/v1alpha1`, `/v1alpha2` etc. -- Eventually we can have further grouping by "area" (TBD during implementation). +- Runtime Hooks versions must be defined under the `/exp/runtime/hooks/api` folder. +- There must be one folder per apiVersion, e.g. `/v1alpha1`, `/v1alpha2` etc. ``` -/runtime -└── contract - ├── cluster - │ ├── v1alpha1 - │ └── v1alpha2 - └── controlplane - └── v1alpha3 +/exp/runtime/hooks/api +├── v1alpha1 +└── v1alpha2 ``` Each version folder must - Define a group version -- Provide type definitions for the RuntimeHook and its input/output parameters. +- Provide type definitions for the Runtime Hook and its request and response parameters. ``` -/runtime/contract/cluster/v1alpha1 +/exp/runtime/hooks/api/v1alpha1 ├── groupversion_info.go -└── before_upgrade_types.go +└── lifecyclehooks_types.go ``` -Type definitions are standard golang type definitions with golang json tags and a set of additional k8s/kubebuilder +Type definitions are standard Golang type definitions with Golang JSON tags and a set of additional k8s/kubebuilder markers triggering code generators for: -- DeepCopy func, making input/output parameters types to satisfy the runtime.object interface. -- Conversion func from older releases of the Runtime Hook input/output parameters types to the latest one. -- OpenAPI schema’s definition for each type. +- DeepCopy functions, so that request and response parameter types satisfy the `runtime.Object` interface. +- Conversion functions from older apiVersions of the Runtime Hook request and response parameter types to the latest one. +- OpenAPI schema definitions for each type. ```go +// BeforeClusterUpgradeRequest is the request of the BeforeClusterUpgrade hook. // +k8s:openapi-gen=true +// +kubebuilder:object:generate=true // +kubebuilder:object:root=true -type BeforeUpgradeInput struct { -metav1.TypeMeta `json:",inline"` - ... +type BeforeClusterUpgradeRequest struct { + metav1.TypeMeta `json:",inline"` + ... } + +// BeforeClusterUpgradeResponse is the response of the BeforeClusterUpgrade hook. +// +k8s:openapi-gen=true +// +kubebuilder:object:generate=true +// +kubebuilder:object:root=true +type BeforeClusterUpgradeResponse struct { + metav1.TypeMeta `json:",inline"` + + // CommonRetryResponse contains Status, Message and RetryAfterSeconds fields. + CommonRetryResponse `json:",inline"` +} + +// BeforeClusterUpgrade is the hook that will be called after a Cluster.spec.version is upgraded and +// before the updated version is propagated to the underlying objects. +func BeforeClusterUpgrade(*BeforeClusterUpgradeRequest, *BeforeClusterUpgradeResponse) {} ``` -The code generators are https://github.com/kubernetes-sigs/controller-tools and https://github.com/kubernetes/kube-openapi; -the expected output will be something similar to: +The code generators are https://github.com/kubernetes-sigs/controller-tools and https://github.com/kubernetes/kube-openapi; +the expected output will be similar to: ``` /runtime/contract/cluster/v1alpha1 ├── groupversion_info.go -├── before_upgrade_types.go +├── lifecyclehooks_types.go ├── zz_generated.conversion.go ├── zz_generated.deepcopy.go └── zz_generated.openapi.go ``` Similarly to what happens for API types and api-machinery schema, the type definitions inside every version folder -have to be added to a **catalog**, but with few notable differences: +have to be added to a `Catalog`, but with a few notable differences: -- The Runtime Hooks tracks mapping between a group/version/hook and its own corresponding input/output types - (group/version/input-kind and group/version/output-kind). +- The `Catalog` tracks mapping between a group/version/hook and its own corresponding request/response types + (group/version/request-GVK and group/version/response-GVK). - Type conversions are allowed between objects with the same group/hook (instead of being in a “flat type-space” like in the api-machinery schema). -_Note: this is pseudo code, meant to demonstrate that registering an Runtime Hook is similar to registering an API type; -the exact details will be defined during implementation._ - +`groupversion_info.go`: ```go var ( - // GroupVersion is group version identifying Runtime Hooks defined in this package - // and their request and response types. - GroupVersion = catalog.GroupVersion{Group: "cluster.runtime.cluster.x-k8s.io", Version: "v1alpha1"} - - // catalogBuilder is used to add Runtime Hooks and their input and output types - // to a Catalog. - catalogBuilder = catalog.NewBuilder(GroupVersion) - - // AddToCatalog adds Runtime Hooks defined in this package and their input and - // output types to a catalog. - AddToCatalog = catalogBuilder.AddToCatalog - - // localSchemeBuilder provides access to the SchemeBuilder used for managing Runtime Hooks - // input and output types defined in this package. - // NOTE: this object is required to allow registration of automatically generated - // conversions func. - localSchemeBuilder = catalogBuilder.SchemeBuilder + // GroupVersion is the group version identifying Runtime Hooks defined in this package + // and their request and response types. + GroupVersion = schema.GroupVersion{Group: "hooks.runtime.cluster.x-k8s.io", Version: "v1alpha1"} + + // catalogBuilder is used to add Runtime Hooks and their request and response types + // to a Catalog. + catalogBuilder = &runtimecatalog.Builder{GroupVersion: GroupVersion} + + // AddToCatalog adds Runtime Hooks defined in this package and their request and + // response types to a catalog. + AddToCatalog = catalogBuilder.AddToCatalog + + // localSchemeBuilder provide access to the SchemeBuilder used for managing Runtime Hooks + // and their request and response types defined in this package. + // NOTE: This object is required to allow registration of automatically generated + // conversions func. + localSchemeBuilder = catalogBuilder ) func init() { - // Add Open API definitions for Runtime Hooks input and output types in this package - // NOTE: the GetOpenAPIDefinitions func is automatically generated by openapi-gen. - catalogBuilder.OpenAPIDefinitions(GetOpenAPIDefinitions) + // Add Open API definitions for RuntimeHooks request and response types in this package + // NOTE: the GetOpenAPIDefinitions func is automatically generated by openapi-gen. + catalogBuilder.RegisterOpenAPIDefinitions(GetOpenAPIDefinitions) +} +``` - // Register Runtime Hooks defined in this package and their input and output types. - catalogBuilder.RegisterHook(&BeforeUgradeHook{}, &BeforeUgradeInput{}, &BeforeUgradeOutput{}) +`lifecyclehooks_types.go`: +```go +func init() { + // Register Runtime Hooks defined in this package. + catalogBuilder.RegisterHook(BeforeClusterUpgrade, &runtimecatalog.HookMeta{ + Tags: []string{"Lifecycle Hooks"}, + Summary: "Called before the Cluster is upgraded.", + Description: "This blocking hook is called after the Cluster object has been updated with a new spec.topology.version by the user, and immediately before the new version is propagated to the Control Plane.", + }) } ``` @@ -723,7 +521,7 @@ responsibility of this controller should be to maintain an internal, shared **re at a given time. Please note that the Runtime Extensions registry also provides a single point to centralize a set of common behaviors -supporting interaction with those external components, thus making the adoption of this feature scalable - +supporting interaction with those external components, thus making the adoption of this feature scalable - in the sense of being used for an increasing numbers of use cases in Cluster API - while operating consistently across the board. @@ -732,7 +530,7 @@ in case of errors, thus preventing Cluster API from creating pressure on HTTP Se ongoing operational issues. Another cross-cutting concern is about ensuring that Runtime Extensions, which are external components triggered -in the middle of Cluster API controllers logic, do not block the reconciliation process indefinitely +in the middle of Cluster API controllers logic, do not block the reconciliation process indefinitely (e.g by enforcing a maximum timeout for all the Runtime Extensions calls). ### Calling Runtime Extensions @@ -744,51 +542,75 @@ Cluster API is going to implement calls to registered Runtime Extensions at well The two key elements that make the implementation of runtime extension calls simple and consistent across the codebase are: -- The Runtime Hook catalog, providing the info about all the defined Runtime Hooks, supported version and - corresponding input/output types; -- The Runtime Extensions registry, providing info about the registered Runtime Extensions implementing the - Runtime Hooks defined above. +- The catalog, providing the info about all the defined Runtime Hooks, supported version and + corresponding request/response types; +- The client, implement the call to a Runtime Extension. -Given these two elements, the code for calling Runtime Extensions is: - -_Note: this is pseudo code, meant to demonstrate a set of elements described below the example; the exact details -will be defined during implementation, possibly taking advantage of golang generics._ +Given these two elements, the code for calling a Runtime Extensions is: +`main.go`: ```go -extensions := registry.Get( - registry.Group("cluster.runtime.cluster.x-k8s.io/v1alpha2”), - registry.Hook(&v1alpha2.BeforeUgradeHook{}), +var ( + catalog = runtimecatalog.New() + ... ) -for _, e := range extensions{ - client := catalogHTTP.NewClientBuilder(). - WithCatalog(c). - Host(e.host). - Build() +func init() { + ... + // Register the RuntimeHook types into the catalog. + _ = runtimehooksv1.AddToCatalog(catalog) + ... +} - hook := &v1alpha2.BeforeUgradeHook{} - in := &v1alpha2.BeforeUgradeInput{First: 1, Second: "Hello CAPI Runtime Extensions!"} - out := &v1alpha2.BeforeUgradeOutput{} - if err := client.Extension(hook, catalogHTTP.SpecVersion(r.version)).Invoke(ctx, in, out); err != nil { - panic(err) - } +func setupReconcilers(ctx context.Context, mgr ctrl.Manager) { + ... + // Setup the runtime client. + runtimeClient = runtimeclient.New(runtimeclient.Options{ + Catalog: catalog, + Registry: runtimeregistry.New(), + Client: mgr.GetClient(), + }) + ... + // Pass the runtime client to a reconciler. + if err := (&controllers.ClusterTopologyReconciler{ + Client: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), + RuntimeClient: runtimeClient, + UnstructuredCachingClient: unstructuredCachingClient, + WatchFilterValue: watchFilterValue, + }).SetupWithManager(ctx, mgr, concurrency(clusterTopologyConcurrency)); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterTopology") + os.Exit(1) + } + ... +} +``` - // Do something with the output e.g. proceed with upgrade or block +`cluster_controller.go`: +```go + // Call BeforeClusterCreate Runtime Extensions. + hookRequest := &runtimehooksv1.BeforeClusterCreateRequest{ + Cluster: *s.Current.Cluster, + } + hookResponse := &runtimehooksv1.BeforeClusterCreateResponse{} + if err := r.RuntimeClient.CallAllExtensions(ctx, runtimehooksv1.BeforeClusterCreate, s.Current.Cluster, hookRequest, hookResponse); err != nil { + return ctrl.Result{}, err + } } ``` A couple of elements are worth noting: -- Registered Runtime Extensions are returned by group and hook; this will also include Runtime Extensions - implementing older versions of the same Runtime Hook; -- The call is implemented using the last version of the Runtime Hook/Input/Output types; the Invoke function - will take care of version conversions, if required. +- `CallAllExtensions` will call all registered Runtime Extensions of the corresponding group and hook. + This will also include Runtime Extensions implementing older versions of the same Runtime Hook. +- The call is implemented using the latest version of the Runtime Hook/request/response types; the + `CallAllExtensions` function will take care of version conversions, if required. ## Security Model Following threats were considered: -- Malicious Runtime Extensions being registered +- Malicious Runtime Extensions being registered Mitigation: The same mitigations used for avoiding malicious dynamic webhooks in Kubernetes apply (defining RBAC rules for the ExtensionConfig assigning this responsibility to cluster admin only). @@ -833,10 +655,10 @@ However, rules for evolving Runtime Hook across Cluster API versions are introdu ### Test Plan -While in alpha phase it is expected that the Runtime SDK will have unit tests covering all the main components: +While in alpha phase it is expected that the Runtime SDK will have unit tests covering all the main components: catalog, discovery controller, tooling. -With the increasing adoption of this feature, we expect more unit tests, integration tests and E2E tests +With the increasing adoption of this feature, we expect more unit tests, integration tests and E2E tests to be added covering specific Runtime Hooks. ### Graduation Criteria @@ -852,28 +674,28 @@ See upgrade strategy. ### Runtime SDK rules -**Rule #1: Runtime Hooks and input/output parameter elements may only be removed by incrementing the version of the +**Rule #1: Runtime Hooks and request/response parameter elements may only be removed by incrementing the version of the Runtime Hook.** -Once a Runtime Hook or a Runtime Hook input/output parameter element has been added to a particular version, +Once a Runtime Hook or a Runtime Hook response/response parameter element has been added to a particular version, it can not be removed from that version or have its behavior significantly changed. -**Rule #2 Runtime Hook’s input parameters must be down-convertible, output parameters must be up-convertible. +**Rule #2 Runtime Hook’s request parameters must be down-convertible, response parameters must be up-convertible. Most specifically** -- input parameters must be able to be down-converted from the latest version to previous versions of the same +- request parameters must be able to be down-converted from the latest version to previous versions of the same Runtime Hook; this might imply information loss, but the behavior of the previous version of the Runtime Hook must not be affected by this. -- output parameters must be able to be up-converted from previous versions to current versions of the same +- response parameters must be able to be up-converted from previous versions to current versions of the same Runtime Hook; this means that new information should be nullable or have defaults. -For example assume that we have a Cluster.BeforeUpgrade Runtime Hook with version `v1alpha1` and `v1alpha2`; -In order to avoid duplicating code, Cluster API internally will always work at the latest version, `v1alpha2` +For example assume that we have a `BeforeClusterUpgrade` Runtime Hook with version `v1alpha1` and `v1alpha2`; +In order to avoid duplicating code, Cluster API internally will always work at the latest version, `v1alpha2` in the example, but there could be still a deployed Runtime Extension on `v1alpha1`. This rule makes it possible to call the Runtime Extensions still using the `v1alpha1` by ensuring it is possible -to down-converting the input parameter for the `v1alpha2` call implemented in CAPI, make the call, and then -up-converting the `v1alpha1` output parameter to the v1alpha2 version `CAPI` expects. +to down-converting the request parameter for the `v1alpha2` call implemented in CAPI, make the call, and then +up-converting the `v1alpha1` response parameter to the v1alpha2 version `CAPI` expects. **Rule #3: A Runtime Hook version in a given track may not be deprecated until a new version at least as stable is released.** @@ -910,7 +732,7 @@ items: # Info about implemented runtime extensions ``` Please note that the above struct supports defining more than one Runtime Extension for the same hook, e.g. -defining more than one "generatePatches" extensions. +defining more than one "generatePatches" extensions. ## Implementation History diff --git a/docs/proposals/20220330-topology-mutation-hook.md b/docs/proposals/20220330-topology-mutation-hook.md index d9abf0049e8f..85dad4b792ea 100644 --- a/docs/proposals/20220330-topology-mutation-hook.md +++ b/docs/proposals/20220330-topology-mutation-hook.md @@ -169,115 +169,22 @@ This section provides guidance for developers on the implementation of an Extern #### Cluster topology reconciliation -This section documents when the Topology Mutation Hook is going to be called during each Cluster topology reconciliation. More specifically we are going to call two different hooks for each reconciliation: - -* **GeneratePatches**: GeneratePatches is responsible for generating patches for the entire Cluster topology. -* **[optional] ValidateTopology**: ValidateTopology is called after all patches have been applied and thus allows the External Patch Extension developer to validate the resulting objects. - - **Note**: ValidateTopology is optional, i.e. it will be only called if an External Patch Extension implements it (and returns it during discovery). +This section documents when the Topology Mutation Hook is going to be called during each Cluster topology reconciliation. ![Cluster topology reconciliation](./images/topology-mutation-hook/topology-reconciliation.png) -#### GeneratePatches Hook - -A GeneratePatches call generates patches for the entire Cluster topology. Accordingly the request contains all templates, the global variables and the template-specific variables. The response contains generated patches. - -Example request: -* Generating patches for a Cluster topology is done via a single call to allow External Patch Extensions a holistic view of the entire Cluster topology. Additionally this allows us to reduce the number of round-trips. -* Each item in the request will contain the template as a raw object. Additionally information about where the template is used is provided via `holderReference`. -```yaml -variables: -- name: - value: - ... -items: -- uid: 7091de79-e26c-4af5-8be3-071bc4b102c9 - holderReference: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: MachineDeployment - namespace: default - name: cluster-md1-xyz - fieldPath: spec.template.spec.infrastructureRef - object: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: AWSMachineTemplate - spec: - ... - variables: - - name: - value: - ... -``` - -Example response: -* The response contains patches instead of full objects to reduce the payload. -* Templates in the request and patches in the response will be correlated via UIDs. -* Like for inline patches external patches are only allowed to change `spec.template.spec`. -```yaml -status: Success # or Failure -message: "error message if status == Failure" -items: -- uid: 7091de79-e26c-4af5-8be3-071bc4b102c9 - patchType: JSONPatch - patch: -``` - -The full OpenAPI specification (draft) of the GeneratePatches hook can be seen via the [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml). - -During implementation we will consider introducing a library to facilitate development of External Patch Extensions. It will provide capabilities like: -* Access builtin variables -* Extract certain templates from a GeneratePatches request (e.g. all bootstrap templates) +The remainder of this section has been moved to the [book](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md#introduction) +to avoid duplication. -#### ValidateTopology Hook - -A ValidateTopology call validates the topology after all patches have been applied. The request contains all templates of the Cluster topology, the global variables and the template-specific variables. The response contains the result of the validation. - -Example request: -* The request is the same as the GeneratePatches request except the `uid` fields. We don't - need them as we don't have to correlate anything in the response. -```yaml -variables: -- name: - value: - ... -items: -- holderReference: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: MachineDeployment - namespace: default - name: cluster-md1-xyz - fieldPath: spec.template.spec.infrastructureRef - object: - apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 - kind: AWSMachineTemplate - spec: - ... - variables: - - name: - value: - ... -``` - -Example response: -```yaml -status: Success # or Failure -message: "error message if status == Failure" -``` - -The full OpenAPI specification (draft) of the ValidateTopology hook can be seen via the [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/topology-mutation-hook/runtime-sdk-openapi.yaml). +#### Definitions +This section has been moved to the [book](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md#definitions) +to avoid duplication. #### Guidelines -For general Runtime Extension developer guidelines please refer to the [developer guide in the Runtime SDK proposal](https://github.com/kubernetes-sigs/cluster-api/blob/75b39db545ae439f4f6203b5e07496d3b0a6aa75/docs/proposals/20220221-runtime-SDK.md#runtime-extensions-developer-guide). This section outlines guidelines specific to External Patch Extensions: - -* **Input validation**: An External Patch Extension must always validate its input, i.e. it must validate that all variables exist and have the right type and it must validate the kind and apiVersion of the templates which should be patched. -* **Timeouts**: As External Patch Extensions are called during each Cluster topology reconciliation, they must respond as fast as possible (<=200ms) to avoid delaying individual reconciles and congestion. -* **Availability**: An External Patch Extension must be always available, otherwise Cluster topologies won’t be reconciled anymore. -* **Side Effects**: An External Patch Extension must not make out-of-band changes. If necessary external data can be retrieved, but be aware of performance impact. -* **Deterministic results**: For a given request (a set of templates and variables) an External Patch Extension must always return the same response (a set of patches). Otherwise the Cluster topology will never reach a stable state. -* **Idempotence**: An External Patch Extension must only return patches if changes to the templates are required, i.e. unnecessary patches when the template is already in the desired state must be avoided. -* **Avoid Dependencies**: An External Patch Extension must be independent of other External Patch Extensions. However if dependencies cannot be avoided, it is possible to control the order in which patches are executed via the ClusterClass. +This section has been moved to the [book](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md#guidelines) +to avoid duplication. #### clusterctl alpha topology plan diff --git a/docs/proposals/20220414-runtime-hooks.md b/docs/proposals/20220414-runtime-hooks.md index 3ba27cc48007..bc2e037891f5 100644 --- a/docs/proposals/20220414-runtime-hooks.md +++ b/docs/proposals/20220414-runtime-hooks.md @@ -126,227 +126,13 @@ Below is a description for the Runtime Hooks introduced by this proposal. ![runtime-hooks](images/runtime-hooks/runtime-hooks.png) - -#### Before Cluster Create - -This hook is called after the Cluster object has been created by the user, immediately before all the objects which are part of a Cluster topology(*) are going to be created. Runtime Extension implementers can use this hook to determine/prepare add-ons for the Cluster and block the creation of those objects until everything is ready. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterCreateRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterCreateResponse -status: Success -message: "error message if status == Failure" -retryAfterSeconds: 10 -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - -(*) The objects which are part of a Cluster topology are the infrastructure Cluster, the control plane, the MachineDeployments and the templates derived from the ClusterClass. - - -#### After Control Plane Initialized - -This hook is called after the ControlPlane for the Cluster is marked as available for the first time. Runtime Extension implementers can use this hook to execute tasks, for example component installation on workload clusters, that are only possible once the Control Plane is available. This hook does not block any further changes to the Cluster. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterControlPlaneInitializedRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterControlPlaneInitializedResponse -status: Success -message: "error message if status == Failure" -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - - -#### Before Cluster Upgrade - -This hook is called after the Cluster object has been updated with a new spec.topology.version by the user, and immediately before the new version is going to be propagated to the control plane (*). Runtime Extension implementers can use this hook to execute pre-upgrade add-on tasks and block upgrades of the ControlPlane and Workers. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterUpgradeRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -fromKubernetesVersion: "v1.21.2" -toKubernetesVersion: "v1.22.0" -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterUpgradeResponse -status: Success -message: "error message if status == Failure" -retryAfterSeconds: 10 -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - -* Under normal circumstances spec.topology.version gets propagated to the control plane immediately; however if previous upgrades or worker machine rollouts are still in progress, the system waits for those operations to complete before starting the new upgrade. - -#### After Control Plane Upgrade - -This hook is called after the control plane has been upgraded to the version specified in spec.topology.version, and immediately before the new version is going to be propagated to the MachineDeployments existing in the Cluster. Runtime Extension implementers can use this hook to execute post-upgrade add-on tasks and block upgrades to workers until everything is ready. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterControlPlaneUpgradeRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -kubernetesVersion: "v1.22.0" -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterControlPlaneUpgradeResponse -status: Success -message: "error message if status == Failure" -retryAfterSeconds: 10 -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - - -#### After Cluster Upgrade - -This hook is called after the Cluster, control plane and workers have been upgraded to the version specified in spec.topology.version. Runtime Extensions implementers can use this hook to execute post-upgrade add-on tasks. This hook does not block any further changes or upgrades to the Cluster. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterClusterUpgradeRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -kubernetesVersion: "v1.22.0" -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: AfterClusterUpgradeResponse -status: Success -message: "error message if status == Failure" -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - -#### Before Cluster Delete - -This hook is called after the Cluster has been deleted by the user, and immediately before objects existing in the Cluster are going to be deleted. Runtime Extension implementers can use this hook to execute cleanup tasks for the add-ons and block deletion of the Cluster and descendant objects until everything is ready. - -##### Example Request: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterDeleteRequest -cluster: - apiVersion: cluster.x-k8s.io/v1beta1 - kind: Cluster - metadata: - name: test-cluster - namespace: test-ns - spec: - ... - status: - ... -``` - -##### Example Response: - -```yaml -apiVersion: hooks.runtime.cluster.x-k8s.io/v1alpha1 -kind: BeforeClusterDeleteResponse -status: Success -message: "error message if status == Failure" -retryAfterSeconds: 10 -``` - -For additional details, refer to the [Draft OpenAPI spec](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubernetes-sigs/cluster-api/main/docs/proposals/images/runtime-hooks/runtime-hooks-openapi.yaml). - +The remainder of this section has been moved to the [book](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md#definitions) +to avoid duplication. ### Runtime Extensions developer guide -All guidelines defined in the [Runtime SDK](https://github.com/kubernetes-sigs/cluster-api/blob/b48a6ed07ac2bd353f99000270510369f4baa1a5/docs/proposals/20220221-runtime-SDK.md) apply to the implementation of Runtime Extensions of the hooks defined in this proposal. - -TL;DR; Runtime Extensions are components that should be designed, written and deployed with great caution given that they can affect the proper functioning of the Cluster API runtime. A poorly implemented Runtime Extension could potentially block lifecycle transitions from happening. - -Following recommendations are especially relevant: - -* [Blocking and non Blocking](https://github.com/kubernetes-sigs/cluster-api/blob/b48a6ed07ac2bd353f99000270510369f4baa1a5/docs/proposals/20220221-runtime-SDK.md#blocking-hooks) -* [Error management](https://github.com/kubernetes-sigs/cluster-api/blob/b48a6ed07ac2bd353f99000270510369f4baa1a5/docs/proposals/20220221-runtime-SDK.md#error-management) -* [Avoid dependencies](https://github.com/kubernetes-sigs/cluster-api/blob/b48a6ed07ac2bd353f99000270510369f4baa1a5/docs/proposals/20220221-runtime-SDK.md#avoid-dependencies) - +This section has been moved to the [book](../../docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md#guidelines) +to avoid duplication. ### Security Model diff --git a/test/extension/main.go b/test/extension/main.go index 8acd24bfa38b..e60b9d98eef9 100644 --- a/test/extension/main.go +++ b/test/extension/main.go @@ -45,7 +45,7 @@ var ( setupLog = ctrl.Log.WithName("setup") - // flags. + // Flags. profilerAddress string webhookPort int webhookCertDir string @@ -197,7 +197,7 @@ func main() { os.Exit(1) } - setupLog.Info("starting RuntimeExtension", "version", version.Get().String()) + setupLog.Info("Starting Runtime Extension server", "version", version.Get().String()) if err := webhookServer.Start(ctx); err != nil { setupLog.Error(err, "error running webhook server") os.Exit(1)