|
| 1 | +<!-- |
| 2 | + Copyright 2024 Nutanix. All rights reserved. |
| 3 | + SPDX-License-Identifier: Apache-2.0 |
| 4 | + --> |
| 5 | + |
| 6 | +# Preflight Checks Framework |
| 7 | + |
| 8 | +The preflight checks framework is a validating admission webhook that runs a series of checks before a `Cluster` resource is created. It helps ensure that a cluster's configuration is valid, and that the underlying infrastructure is ready, preventing common issues before they occur. |
| 9 | + |
| 10 | +The framework is designed to be extensible, allowing different sets of checks to be grouped into logical units called `Checker`s. |
| 11 | + |
| 12 | +## Core Concepts |
| 13 | + |
| 14 | +The framework is built around a few key interfaces and structs: |
| 15 | + |
| 16 | +- **`preflight.WebhookHandler`**: The main entry point for the webhook. It receives admission requests, decodes the `Cluster` object, and orchestrates the execution of all registered `Checker`s. |
| 17 | + |
| 18 | +- **`preflight.Checker`**: A collection of checks, logically related to some external API, and sharing dependencies, such as a client for the external API. Each `Checker` is responsible for initializing and returning a slice of `Check`s to be executed. At the time of this writing, we have two checkers: |
| 19 | + - `generic.Checker`: For checks that are not specific to any infrastructure provider. |
| 20 | + - `nutanix.Checker`: For checks specific to the Nutanix infrastructure. All the checks share a Prism Central API client. |
| 21 | + |
| 22 | +- **`preflight.Check`**: Represents a single, atomic validation. Each check must implement two methods: |
| 23 | + - `Name() string`: Returns a unique name for the check. This name is used for identification, and for skipping checks. |
| 24 | + - `Run(ctx context.Context) CheckResult`: Executes the validation logic. |
| 25 | + |
| 26 | +- **`preflight.CheckResult`**: The outcome of a `Check`. It indicates if the check was `Allowed`, if an `InternalError` occurred, and provides a list of `Causes` for failure and any `Warnings`. |
| 27 | + |
| 28 | +### Create a Checker |
| 29 | + |
| 30 | +#### Implement a new Go package |
| 31 | + |
| 32 | +Create a new package for your checker under the preflight directory. For example, `pkg/webhook/preflight/myprovider/`. |
| 33 | + |
| 34 | +Create a checker.go file to define your `Checker`. This checker will initialize all the checks for your provider. A common pattern is to have a configuration pseudo-check that runs first, parses provider-specific configuration, initializes an API client, and then initialize checks with the configuration and client. |
| 35 | + |
| 36 | +````go |
| 37 | +package myprovider |
| 38 | + |
| 39 | +import ( |
| 40 | + "context" |
| 41 | + |
| 42 | + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" |
| 43 | + ctrl "sigs.k8s.io/controller-runtime" |
| 44 | + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" |
| 45 | + |
| 46 | + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight" |
| 47 | +) |
| 48 | + |
| 49 | +// Expose the checker as a package variable. |
| 50 | +var Checker = &myChecker{ |
| 51 | + // Use factories to create checks. |
| 52 | +} |
| 53 | + |
| 54 | +type myChecker struct { |
| 55 | + // factories for creating checks |
| 56 | +} |
| 57 | + |
| 58 | +// checkDependencies holds shared data for all checks. |
| 59 | +type checkDependencies struct { |
| 60 | + // provider-specific config, clients, etc. |
| 61 | +} |
| 62 | + |
| 63 | +func (m *myChecker) Init( |
| 64 | + ctx context.Context, |
| 65 | + client ctrlclient.Client, |
| 66 | + cluster *clusterv1.Cluster, |
| 67 | +) []preflight.Check { |
| 68 | + log := ctrl.LoggerFrom(ctx).WithName("preflight/myprovider") |
| 69 | + |
| 70 | + cd := &checkDependencies{ |
| 71 | + // initialize dependencies |
| 72 | + } |
| 73 | + |
| 74 | + checks := []preflight.Check{ |
| 75 | + // It's good practice to have a configuration check run first. |
| 76 | + newConfigurationCheck(cd), |
| 77 | + } |
| 78 | + |
| 79 | + // Add other checks |
| 80 | + checks = append(checks, &myCheck{}) |
| 81 | + |
| 82 | + return checks |
| 83 | +} |
| 84 | +```` |
| 85 | + |
| 86 | +The `generic.Checker` and `nutanix.Checker` serve as excellent reference implementations. The `nutanix.Checker` demonstrates a more complex setup with multiple dependent checks. |
| 87 | + |
| 88 | +#### Register the Checker |
| 89 | + |
| 90 | +Finally, add your new `Checker` to the list of checkers in main.go. |
| 91 | + |
| 92 | +````go |
| 93 | +// ...existing code... |
| 94 | +import ( |
| 95 | + preflightgeneric "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/generic" |
| 96 | + preflightnutanix "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/nutanix" |
| 97 | + preflightmyprovider "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/myprovider" |
| 98 | +) |
| 99 | +// ...existing code... |
| 100 | + if err := mgr.Add(preflight.New( |
| 101 | + mgr.GetClient(), |
| 102 | + mgr.GetWebhookServer(). |
| 103 | + GetAdmissionDecoder(), |
| 104 | + []preflight.Checker{ |
| 105 | + // Add your preflight checkers here. |
| 106 | + preflightgeneric.Checker, |
| 107 | + preflightnutanix.Checker, |
| 108 | + preflightmyprovider.Checker, |
| 109 | + }..., |
| 110 | + )); err != nil { |
| 111 | +// ...existing code... |
| 112 | +```` |
| 113 | + |
| 114 | +## Create a Check |
| 115 | + |
| 116 | +Create a struct that implements the `preflight.Check` interface. |
| 117 | + |
| 118 | +The `Name` method should return a concise, unique name that is a combination of the Checker and Check name, e.g. `NutanixVMImage`. Checks in the `generic` Checker only use the Check name, e.g. `Registry. The name is used to identify,and skip checks. |
| 119 | +
|
| 120 | +The `Run` method should return a `CheckResult` that indicates whether the check passed (`Allowed`), whether an internal error occurred (`InternalError`), and one or more `Causes`es, each including a `Message` that explains why the check failed, and a `Field` that points the user to the configuration that should be examined and possibly changed. |
| 121 | +
|
| 122 | +If a check runs to completion, then `InternalError` should be false. It should be `true` only in case of an _unexpected_ error, such as a malformed response from some API. |
| 123 | +
|
| 124 | +If the check passes, then `Allowed` should be `true`. |
| 125 | +
|
| 126 | +The `Message` should include context to help the user understand the problem, and the most common ways to help them resolve the problem. Even so, the message should be concise, as it will be displayed in the CLI and UI clients. |
| 127 | +
|
| 128 | +The `Field` should be a valid JSONPath expression that identifies the most relevant part of the Cluster configuration. Look at existing checkers for examples. |
| 129 | +
|
| 130 | +````go |
| 131 | +package myprovider |
| 132 | +
|
| 133 | +import ( |
| 134 | + "context" |
| 135 | + "fmt" |
| 136 | +
|
| 137 | + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight" |
| 138 | +) |
| 139 | +
|
| 140 | +type myCheck struct { |
| 141 | + // any dependencies the check needs |
| 142 | +} |
| 143 | +
|
| 144 | +func (c *myCheck) Name() string { |
| 145 | + return "MyProviderCheck" |
| 146 | +} |
| 147 | +
|
| 148 | +func (c *myCheck) Run(ctx context.Context) preflight.CheckResult { |
| 149 | + // Your validation logic here. |
| 150 | + // For example, check a specific condition. |
| 151 | + isValid := true // replace with real logic |
| 152 | + if !isValid { |
| 153 | + return preflight.CheckResult{ |
| 154 | + Allowed: false, |
| 155 | + Causes: []preflight.Cause{ |
| 156 | + { |
| 157 | + Message: "My custom check failed because of a specific reason.", |
| 158 | + Field: "$.spec.topology.variables[[email protected]=='myProviderConfig']", |
| 159 | + }, |
| 160 | + }, |
| 161 | + } |
| 162 | + } |
| 163 | +
|
| 164 | + return preflight.CheckResult{Allowed: true} |
| 165 | +} |
| 166 | +```` |
0 commit comments