-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Add preflight checks framework #1129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7511f49
a40fd92
24c9d45
858e6fd
2c69dd0
7e687a2
306a852
b3da7d4
690a89e
47c6ad8
3211e7a
5eb6d26
4a518b3
438495b
b377301
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright 2025 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package preflight | ||
|
||
// +kubebuilder:webhook:path=/preflight-v1beta1-cluster,mutating=false,failurePolicy=fail,groups="cluster.x-k8s.io",resources=clusters,verbs=create,versions=*,name=preflight.cluster.caren.nutanix.com,admissionReviewVersions=v1,sideEffects=None,timeoutSeconds=30 |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,153 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Copyright 2025 Nutanix. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
package preflight | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
"context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
"net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
"sync" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
admissionv1 "k8s.io/api/admission/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
type Cause struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Message string | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Field string | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
type CheckResult struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Name string | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
Allowed bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Error bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
Causes []Cause | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Warnings []string | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
type Check = func(ctx context.Context) CheckResult | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
type Checker interface { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Init decides which of its checks should run for the cluster. It then initializes the checks | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// with common dependencies, such as an infrastructure client. Finally, it returns the initialized checks, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// ready to be run. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Init can store the client and cluster, but not the context, because each check will accept its own context. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Init(ctx context.Context, client ctrlclient.Client, cluster *clusterv1.Cluster) []Check | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
type WebhookHandler struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
client ctrlclient.Client | ||||||||||||||||||||||||||||||||||||||||||||||||||||
decoder admission.Decoder | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checkers []Checker | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
func New(client ctrlclient.Client, decoder admission.Decoder, checkers ...Checker) *WebhookHandler { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
h := &WebhookHandler{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
client: client, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
decoder: decoder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checkers: checkers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return h | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
func (h *WebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if req.Operation == admissionv1.Delete { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return admission.Allowed("") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
cluster := &clusterv1.Cluster{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
err := h.decoder.Decode(req, cluster) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return admission.Errored(http.StatusBadRequest, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Checks run only for ClusterClass-based clusters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if cluster.Spec.Topology == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return admission.Allowed("") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
resp := admission.Response{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
AdmissionResponse: admissionv1.AdmissionResponse{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Allowed: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Result: &metav1.Status{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Details: &metav1.StatusDetails{}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Collect all checks in parallel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checkerWG := &sync.WaitGroup{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resultCh := make(chan CheckResult) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
for _, checker := range h.checkers { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checkerWG.Add(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
go func(ctx context.Context, checker Checker, resultCh chan CheckResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Initialize the checker. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checks := checker.Init(ctx, h.client, cluster) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Run its checks in parallel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checksWG := &sync.WaitGroup{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
for _, check := range checks { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checksWG.Add(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
go func(ctx context.Context, check Check, resultCh chan CheckResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
result := check(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resultCh <- result | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checksWG.Done() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}(ctx, check, resultCh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
checksWG.Wait() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
checkerWG.Done() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}(ctx, checker, resultCh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Close the channel when all checkers are done. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
go func(wg *sync.WaitGroup, resultCh chan CheckResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
wg.Wait() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
close(resultCh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}(checkerWG, resultCh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+111
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can the function return before this go routing closes the channel? what happens with slow checks? (EDIT) goroutines will run even after the function returns |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Collect all check results from the channel. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
internalError := false | ||||||||||||||||||||||||||||||||||||||||||||||||||||
for result := range resultCh { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very clean, on any Error to run a Check or when a check returns a (just mainly thinking out loud) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right. We want to keep track of whether we had a rejection, or an error, but collect all the check results, regardless. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
if result.Error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
internalError = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if !result.Allowed { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Allowed = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
for _, cause := range result.Causes { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Details.Causes = append(resp.Result.Details.Causes, metav1.StatusCause{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Type: metav1.CauseType(fmt.Sprintf("FailedPreflight%s", result.Name)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any concern around unbounded cardinality here? |
||||||||||||||||||||||||||||||||||||||||||||||||||||
Message: cause.Message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Field: cause.Field, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Warnings = append(resp.Warnings, result.Warnings...) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if len(resp.Result.Details.Causes) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return resp | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
// Because we have some causes, we construct the response message and code. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Message = "preflight checks failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Code = http.StatusForbidden | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Reason = metav1.StatusReasonForbidden | ||||||||||||||||||||||||||||||||||||||||||||||||||||
if internalError { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// Internal errors take precedence over check failures. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Code = http.StatusInternalServerError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
resp.Result.Reason = metav1.StatusReasonInternalError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+138
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't checking if causes is empty is enough here, does it handle the error response correctly? How about a switch instead to cover error and allowed state? Also think it should use
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
return resp | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the side effects of using channels like this is that results would be non-deterministically ordered, as well as check results from different checkers being interleaved. I would prefer deterministic ordering of results.
How about gathering all checks prior to running them (i.e. run
checker.Init
for each checker in a loop to get all checks in order) and then run each check asynchronously sending check index back to channel along with result, gathering results in a heap/priority queue using the check index as the priority?This would enable using a buffered channel for results (single loop with known number of checks to run) so non-blocking and return results in deterministic order, as well as keeping checks originating from a single checker together (non-interleaved).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! +1 to deterministically ordered results.
Since
checker.Init
may call out to an external API, I wanted to do initialization concurrently.Sounds good.