Skip to content

Commit 54243cb

Browse files
authored
docs: Add dev doc for preflight checks (#1251)
**What problem does this PR solve?**: Explain how to implement `Check`s and `Checker`s. **Which issue(s) this PR fixes**: Fixes # **How Has This Been Tested?**: <!-- Please describe the tests that you ran to verify your changes. Provide output from the tests and any manual steps needed to replicate the tests. --> **Special notes for your reviewer**: <!-- Use this to provide any additional information to the reviewers. This may include: - Best way to review the PR. - Where the author wants the most review attention on. - etc. -->
1 parent c01edd6 commit 54243cb

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

pkg/webhook/preflight/README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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

Comments
 (0)