Skip to content

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

Merged
merged 21 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7511f49
fix: Use helm variables for all webhook configurations
dlipovetsky May 19, 2025
a40fd92
feat: Add preflight checks framework
dlipovetsky May 20, 2025
24c9d45
fixup! feat: Add preflight checks framework
dlipovetsky May 20, 2025
858e6fd
fixup! feat: Add preflight checks framework
dlipovetsky May 20, 2025
2c69dd0
fixup! feat: Add preflight checks framework
dlipovetsky May 20, 2025
7e687a2
fixup! feat: Add preflight checks framework
dlipovetsky May 20, 2025
306a852
fixup! feat: Add preflight checks framework
dlipovetsky May 20, 2025
b3da7d4
fixup! feat: Add preflight checks framework
dlipovetsky May 21, 2025
690a89e
fixup! feat: Add preflight checks framework
dlipovetsky May 22, 2025
47c6ad8
fixup! feat: Add preflight checks framework
dlipovetsky May 21, 2025
3211e7a
fixup! feat: Add preflight checks framework
dlipovetsky May 22, 2025
5eb6d26
fixup! feat: Add preflight checks framework
dlipovetsky May 22, 2025
4a518b3
fixup! feat: Add preflight checks framework
dlipovetsky May 23, 2025
438495b
fixup! feat: Add preflight checks framework
dlipovetsky May 23, 2025
b377301
fixup! feat: Add preflight checks framework
dlipovetsky May 27, 2025
ec20c1e
fixup! feat: Add preflight checks framework
dlipovetsky May 30, 2025
d766f3a
fixup! feat: Add preflight checks framework
dlipovetsky May 30, 2025
14ea63d
fixup! feat: Add preflight checks framework
dlipovetsky Jun 3, 2025
a4078ad
fixup! feat: Add preflight checks framework
dlipovetsky Jun 12, 2025
10f132a
fixup! feat: Add preflight checks framework
dlipovetsky Jun 13, 2025
a067941
fixup! feat: Add preflight checks framework
dlipovetsky Jun 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,24 @@ webhooks:
resources:
- clusters
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: '{{ include "chart.name" . }}-admission'
namespace: '{{ .Release.Namespace }}'
path: /preflight-v1beta1
failurePolicy: Fail
name: preflight.caren.nutanix.com
rules:
- apiGroups:
- cluster.x-k8s.io
apiVersions:
- '*'
operations:
- CREATE
- UPDATE
resources:
- clusters
sideEffects: None
timeoutSeconds: 30
8 changes: 8 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/nutanix"
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/cluster"
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
)

func main() {
Expand Down Expand Up @@ -219,6 +220,13 @@ func main() {
Handler: cluster.NewValidator(mgr.GetClient(), admission.NewDecoder(mgr.GetScheme())),
})

mgr.GetWebhookServer().Register("/preflight-v1beta1", &webhook.Admission{
Handler: preflight.New(mgr.GetClient(), admission.NewDecoder(mgr.GetScheme()),
[]preflight.Checker{
// Add your preflight checkers here.
}...,
),
})
if err := mgr.Start(signalCtx); err != nil {
setupLog.Error(err, "unable to start controller manager")
os.Exit(1)
Expand Down
2 changes: 1 addition & 1 deletion hack/update-webhook-configurations.yq
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ with(.metadata;
.name = "{{ include \"chart.name\" . }}-" + .name,
.annotations["cert-manager.io/inject-ca-from"] = "{{ .Release.Namespace}}/{{ template \"chart.name\" . }}-admission-tls"
),
with(.webhooks[0].clientConfig.service;
with(.webhooks[].clientConfig.service;
.name = "{{ include \"chart.name\" . }}-admission",
.namespace = "{{ .Release.Namespace }}"
)
5 changes: 5 additions & 0 deletions pkg/webhook/preflight/doc.go
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,mutating=false,failurePolicy=fail,groups="cluster.x-k8s.io",resources=clusters,verbs=create;update,versions=*,name=preflight.caren.nutanix.com,admissionReviewVersions=v1,sideEffects=None,timeoutSeconds=30
156 changes: 156 additions & 0 deletions pkg/webhook/preflight/preflight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2025 Nutanix. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package preflight

import (
"context"
"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 CheckResult struct {
Allowed bool
Field string
Message string
Warning string
Error bool
}

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 should not store the context `ctx`, because each check will accept its own context.
// Checks may use both the client and the cluster.
Init(ctx context.Context, client ctrlclient.Client, cluster *clusterv1.Cluster) ([]Check, error)
}

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{},
},
},
}

// Initialize checkers in parallel.
type ChecksResult struct {
checks []Check
err error
}
checksResultCh := make(chan ChecksResult, len(h.checkers))

wg := &sync.WaitGroup{}
for _, checker := range h.checkers {
wg.Add(1)
result := ChecksResult{}
result.checks, result.err = checker.Init(ctx, h.client, cluster)
checksResultCh <- result
wg.Done()
}
wg.Wait()
close(checksResultCh)

// Collect all checks.
checks := make([]Check, 0)
for checksResult := range checksResultCh {
if checksResult.err != nil {
resp.Allowed = false
resp.Result.Code = http.StatusInternalServerError
resp.Result.Message = "failed to initialize preflight checks"
resp.Result.Details.Causes = append(resp.Result.Details.Causes, metav1.StatusCause{
Type: metav1.CauseTypeInternal,
Message: checksResult.err.Error(),
Field: "", // This concerns the whole cluster.
})
continue
}
checks = append(checks, checksResult.checks...)
}

// Run all checks in parallel.
resultCh := make(chan CheckResult, len(checks))
for _, check := range checks {
wg.Add(1)
go func(ctx context.Context, check Check) {
result := check(ctx)
resultCh <- result
wg.Done()
}(ctx, check)
}
wg.Wait()
close(resultCh)

// Collect check results.
for result := range resultCh {
if result.Error {
resp.Allowed = false
resp.Result.Code = http.StatusForbidden
resp.Result.Message = "preflight checks failed"
resp.Result.Details.Causes = append(resp.Result.Details.Causes, metav1.StatusCause{
Type: metav1.CauseTypeInternal,
Field: result.Field,
Message: result.Message,
})
continue
}

if !result.Allowed {
resp.Allowed = false
resp.Result.Code = http.StatusForbidden
resp.Result.Message = "preflight checks failed"
resp.Result.Details.Causes = append(resp.Result.Details.Causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Field: result.Field,
Message: result.Message,
})
}

if result.Warning != "" {
resp.Warnings = append(resp.Warnings, result.Warning)
}
}

return resp
}
Loading
Loading