Skip to content

Commit 33486e6

Browse files
committed
feat: adds a generic checker package with registry checks
1 parent 3ef53dc commit 33486e6

File tree

5 files changed

+248
-0
lines changed

5 files changed

+248
-0
lines changed

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
4343
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/cluster"
4444
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
45+
preflightgeneric "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/generic"
4546
preflightnutanix "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/nutanix"
4647
)
4748

@@ -241,6 +242,7 @@ func main() {
241242
Handler: preflight.New(mgr.GetClient(), admission.NewDecoder(mgr.GetScheme()),
242243
[]preflight.Checker{
243244
// Add your preflight checkers here.
245+
preflightgeneric.Checker,
244246
preflightnutanix.Checker,
245247
}...,
246248
),

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
github.com/onsi/ginkgo/v2 v2.23.4
2929
github.com/onsi/gomega v1.37.0
3030
github.com/pkg/errors v0.9.1
31+
github.com/regclient/regclient v0.8.3
3132
github.com/samber/lo v1.51.0
3233
github.com/spf13/pflag v1.0.6
3334
github.com/stretchr/testify v1.10.0
@@ -73,6 +74,7 @@ require (
7374
github.com/docker/docker v28.0.2+incompatible // indirect
7475
github.com/docker/go-connections v0.5.0 // indirect
7576
github.com/docker/go-units v0.5.0 // indirect
77+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
7678
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
7779
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
7880
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
@@ -111,6 +113,7 @@ require (
111113
github.com/jmespath/go-jmespath v0.4.0 // indirect
112114
github.com/josharian/intern v1.0.0 // indirect
113115
github.com/json-iterator/go v1.1.12 // indirect
116+
github.com/klauspost/compress v1.18.0 // indirect
114117
github.com/mailru/easyjson v0.7.7 // indirect
115118
github.com/mattn/go-colorable v0.1.13 // indirect
116119
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -145,6 +148,7 @@ require (
145148
github.com/spf13/viper v1.20.0 // indirect
146149
github.com/stoewer/go-strcase v1.3.0 // indirect
147150
github.com/subosito/gotenv v1.6.0 // indirect
151+
github.com/ulikunitz/xz v0.5.12 // indirect
148152
github.com/valyala/fastjson v1.6.4 // indirect
149153
github.com/x448/float16 v0.8.4 // indirect
150154
go.mongodb.org/mongo-driver v1.14.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
7373
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
7474
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
7575
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
76+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
77+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
7678
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
7779
github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
7880
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
@@ -192,6 +194,8 @@ github.com/keploy/go-sdk v0.9.0 h1:kpSNcCTDdELsa1gWyhoD9oV57SgSMbG/wq6Cjp4y7cY=
192194
github.com/keploy/go-sdk v0.9.0/go.mod h1:vNKXoFd2MaK+Gly/K6XeP1Hs9dP834C74szH+vtBPwg=
193195
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
194196
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
197+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
198+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
195199
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
196200
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
197201
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -241,6 +245,8 @@ github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1 h1
241245
github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1/go.mod h1:Z+RKLwsHYxAcFbZPy2ft3QAK9kBPt9bQdqXSp7eYWkY=
242246
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
243247
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
248+
github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI=
249+
github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y=
244250
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
245251
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
246252
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
@@ -270,6 +276,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
270276
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
271277
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
272278
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
279+
github.com/regclient/regclient v0.8.3 h1:AFAPu/vmOYGyY22AIgzdBUKbzH+83lEpRioRYJ/reCs=
280+
github.com/regclient/regclient v0.8.3/go.mod h1:gjQh5uBVZoo/CngchghtQh9Hx81HOMKRRDd5WPcPkbk=
273281
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
274282
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
275283
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -310,6 +318,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
310318
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
311319
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
312320
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
321+
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
322+
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
313323
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
314324
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
315325
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package generic
4+
5+
import (
6+
"context"
7+
8+
"github.com/go-logr/logr"
9+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
10+
ctrl "sigs.k8s.io/controller-runtime"
11+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
12+
13+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
14+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
15+
)
16+
17+
var Checker = &genericChecker{
18+
registryCheckFactory: newRegistryCheck,
19+
}
20+
21+
type genericChecker struct {
22+
registryCheckFactory func(
23+
cd *checkDependencies,
24+
) []preflight.Check
25+
}
26+
27+
type checkDependencies struct {
28+
kclient ctrlclient.Client
29+
cluster *clusterv1.Cluster
30+
genericClusterConfigSpec *carenv1.GenericClusterConfigSpec
31+
log logr.Logger
32+
}
33+
34+
func (g *genericChecker) Init(
35+
ctx context.Context,
36+
kclient ctrlclient.Client,
37+
cluster *clusterv1.Cluster,
38+
) []preflight.Check {
39+
cd := &checkDependencies{
40+
kclient: kclient,
41+
cluster: cluster,
42+
log: ctrl.LoggerFrom(ctx).WithName("preflight/generic"),
43+
}
44+
checks := []preflight.Check{}
45+
checks = append(checks, g.registryCheckFactory(cd)...)
46+
return checks
47+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package generic
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"regexp"
10+
11+
"github.com/regclient/regclient"
12+
"github.com/regclient/regclient/config"
13+
"github.com/regclient/regclient/types/ref"
14+
corev1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/types"
16+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
17+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
18+
19+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
20+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
21+
)
22+
23+
var (
24+
registryMirrorVarPath = "cluster.spec.topology.variables[.name=clusterConfig].value.globalImageRegistryMirror"
25+
mirrorURLValidationRegex = regexp.MustCompile(
26+
`^https?://`,
27+
) // in order to use regclient we need to pass just a hostname
28+
// this regex allows us to strip it so we can verify connectivity for this test.
29+
)
30+
31+
type registyCheck struct {
32+
registryMirror *carenv1.GlobalImageRegistryMirror
33+
imageRegistry *carenv1.ImageRegistry
34+
field string
35+
kclient ctrlclient.Client
36+
cluster *clusterv1.Cluster
37+
}
38+
39+
func (r *registyCheck) Name() string {
40+
return "RegistryCredentials"
41+
}
42+
43+
func (r *registyCheck) Run(ctx context.Context) preflight.CheckResult {
44+
if r.registryMirror != nil {
45+
return r.checkRegistry(
46+
ctx,
47+
mirrorURLValidationRegex.ReplaceAllString(r.registryMirror.URL, ""),
48+
r.registryMirror.Credentials,
49+
)
50+
}
51+
if r.imageRegistry != nil {
52+
return r.checkRegistry(
53+
ctx,
54+
mirrorURLValidationRegex.ReplaceAllString(r.imageRegistry.URL, ""),
55+
r.imageRegistry.Credentials,
56+
)
57+
}
58+
return preflight.CheckResult{
59+
Allowed: true,
60+
}
61+
}
62+
63+
func (r *registyCheck) checkRegistry(
64+
ctx context.Context,
65+
registryURL string,
66+
credentials *carenv1.RegistryCredentials,
67+
) preflight.CheckResult {
68+
result := preflight.CheckResult{
69+
Allowed: false,
70+
}
71+
mirrorHost := config.Host{
72+
Name: registryURL,
73+
}
74+
if credentials != nil && credentials.SecretRef != nil {
75+
mirrorCredentialsSecret := &corev1.Secret{}
76+
err := r.kclient.Get(
77+
ctx,
78+
types.NamespacedName{
79+
Namespace: r.cluster.Namespace,
80+
Name: r.registryMirror.Credentials.SecretRef.Name,
81+
},
82+
mirrorCredentialsSecret,
83+
)
84+
if err != nil {
85+
result.Allowed = false
86+
result.Error = true
87+
result.Causes = append(result.Causes,
88+
preflight.Cause{
89+
Message: fmt.Sprintf("failed to get Registry credentials Secret: %s", err),
90+
Field: fmt.Sprintf("%s.credentials.secretRef", registryMirrorVarPath),
91+
},
92+
)
93+
return result
94+
}
95+
username, ok := mirrorCredentialsSecret.Data["username"]
96+
if !ok {
97+
result.Allowed = false
98+
result.Error = true
99+
result.Causes = append(result.Causes,
100+
preflight.Cause{
101+
Message: "failed to get username from Registry credentials Secret. secret must have field username.",
102+
Field: fmt.Sprintf("%s.credentials.secretRef", registryMirrorVarPath),
103+
},
104+
)
105+
return result
106+
}
107+
password, ok := mirrorCredentialsSecret.Data["password"]
108+
if !ok {
109+
result.Allowed = false
110+
result.Error = true
111+
result.Causes = append(result.Causes,
112+
preflight.Cause{
113+
Message: "failed to get username from Registry credentials Secret. secret must have field password.",
114+
Field: fmt.Sprintf("%s.credentials.secretRef", registryMirrorVarPath),
115+
},
116+
)
117+
return result
118+
}
119+
mirrorHost.User = string(username)
120+
mirrorHost.Pass = string(password)
121+
if caCert, ok := mirrorCredentialsSecret.Data["ca.crt"]; ok {
122+
mirrorHost.RegCert = string(caCert)
123+
}
124+
}
125+
rc := regclient.New(
126+
regclient.WithConfigHost(mirrorHost),
127+
regclient.WithUserAgent("regclient/example"),
128+
)
129+
mirrorRef, err := ref.NewHost(mirrorHost.Hostname)
130+
if err != nil {
131+
result.Allowed = false
132+
result.Error = true
133+
result.Causes = append(result.Causes,
134+
preflight.Cause{
135+
Message: "failed to create a client to verify registry configuration",
136+
Field: registryMirrorVarPath,
137+
},
138+
)
139+
return result
140+
}
141+
_, err = rc.Ping(ctx, mirrorRef) // ping will return an error for anything that's not 200
142+
if err != nil {
143+
result.Allowed = false
144+
result.Error = true
145+
result.Causes = append(result.Causes,
146+
preflight.Cause{
147+
Message: fmt.Sprintf("failed to ping registry %s with err: %s", mirrorHost.Hostname, err.Error()),
148+
Field: registryMirrorVarPath,
149+
},
150+
)
151+
return result
152+
}
153+
result.Allowed = true
154+
result.Error = false
155+
return result
156+
}
157+
158+
func newRegistryCheck(
159+
cd *checkDependencies,
160+
) []preflight.Check {
161+
checks := []preflight.Check{}
162+
if cd.genericClusterConfigSpec != nil &&
163+
cd.genericClusterConfigSpec.GlobalImageRegistryMirror != nil {
164+
checks = append(checks, &registyCheck{
165+
field: "cluster.spec.topology.variables[.name=clusterConfig].value.globalImageRegistryMirror",
166+
kclient: cd.kclient,
167+
registryMirror: cd.genericClusterConfigSpec.GlobalImageRegistryMirror,
168+
cluster: cd.cluster,
169+
})
170+
}
171+
if cd.genericClusterConfigSpec != nil && len(cd.genericClusterConfigSpec.ImageRegistries) > 0 {
172+
for i, registry := range cd.genericClusterConfigSpec.ImageRegistries {
173+
checks = append(checks, &registyCheck{
174+
field: fmt.Sprintf(
175+
"cluster.spec.topology.variables[.name=clusterConfig].value.imageRegistries[%d]",
176+
i,
177+
),
178+
kclient: cd.kclient,
179+
imageRegistry: &registry,
180+
cluster: cd.cluster,
181+
})
182+
}
183+
}
184+
return checks
185+
}

0 commit comments

Comments
 (0)