• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / hyperconverged-cluster-operator / 30752919445

02 Aug 2026 02:48PM UTC coverage: 85.724% (+0.1%) from 85.618%
30752919445

push

github

web-flow
CNV-94341: validating wh: don't mask errors with warnings (#4471)

In some cases, when warnings are returned from the validating webhook,
they mask real errors, allowing wrong setting applied to the
HyperConverged CR.

Signed-off-by: Nahshon Unna Tsameret <nunnatsa@redhat.com>

121 of 126 new or added lines in 2 files covered. (96.03%)

4 existing lines in 3 files now uncovered.

12082 of 14094 relevant lines covered (85.72%)

2.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.72
/pkg/webhooks/validator/validator.go
1
package validator
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "net/http"
8
        "reflect"
9
        "strings"
10
        "time"
11

12
        "github.com/go-logr/logr"
13
        openshiftconfigv1 "github.com/openshift/api/config/v1"
14
        "github.com/samber/lo"
15
        xsync "golang.org/x/sync/errgroup"
16
        admissionv1 "k8s.io/api/admission/v1"
17
        corev1 "k8s.io/api/core/v1"
18
        apierrors "k8s.io/apimachinery/pkg/api/errors"
19
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
        "k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
21
        "k8s.io/utils/ptr"
22
        "sigs.k8s.io/controller-runtime/pkg/client"
23
        "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
24

25
        networkaddonsv1 "github.com/kubevirt/cluster-network-addons-operator/pkg/apis/networkaddonsoperator/v1"
26
        kubevirtcorev1 "kubevirt.io/api/core/v1"
27
        cdiv1beta1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
28
        sspv1beta3 "kubevirt.io/ssp-operator/api/v1beta3"
29

30
        hcov1 "github.com/kubevirt/hyperconverged-cluster-operator/api/v1"
31
        hcov1fg "github.com/kubevirt/hyperconverged-cluster-operator/api/v1/featuregates"
32
        hcov1beta1 "github.com/kubevirt/hyperconverged-cluster-operator/api/v1beta1"
33
        "github.com/kubevirt/hyperconverged-cluster-operator/controllers/handlers"
34
        "github.com/kubevirt/hyperconverged-cluster-operator/pkg/featuregatedetails"
35
        "github.com/kubevirt/hyperconverged-cluster-operator/pkg/featuregates"
36
        "github.com/kubevirt/hyperconverged-cluster-operator/pkg/tlssecprofile"
37
        hcoutil "github.com/kubevirt/hyperconverged-cluster-operator/pkg/util"
38
)
39

40
const (
41
        updateDryRunTimeOut = time.Second * 3
42

43
        validatorV1Name = "hyperConverged v1 validator"
44
)
45

46
type WebhookHandler struct {
47
        logger      logr.Logger
48
        cli         client.Client
49
        namespace   string
50
        isOpenshift bool
51
        decoder     admission.Decoder
52
}
53

54
func NewWebhookHandler(logger logr.Logger, cli client.Client, decoder admission.Decoder, namespace string, isOpenshift bool) *WebhookHandler {
2✔
55
        return &WebhookHandler{
2✔
56
                logger:      logger.WithName(validatorV1Name),
2✔
57
                cli:         cli,
2✔
58
                namespace:   namespace,
2✔
59
                isOpenshift: isOpenshift,
2✔
60
                decoder:     decoder,
2✔
61
        }
2✔
62
}
2✔
63

64
func (wh *WebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
1✔
65
        ctx = admission.NewContextWithRequest(ctx, req)
1✔
66
        logger, err := logr.FromContext(ctx)
1✔
67
        if err != nil {
2✔
68
                logger = wh.logger
1✔
69
        } else {
1✔
70
                logger = logger.WithName(validatorV1Name)
×
71
        }
×
72

73
        // Get the object in the request
74
        obj := &hcov1.HyperConverged{}
1✔
75

1✔
76
        dryRun := req.DryRun != nil && *req.DryRun
1✔
77

1✔
78
        switch req.Operation {
1✔
79
        case admissionv1.Create:
1✔
80
                if err = wh.decoder.Decode(req, obj); err != nil {
2✔
81
                        return admission.Errored(http.StatusBadRequest, err)
1✔
82
                }
1✔
83

84
                return wh.validateCreate(logger, dryRun, obj)
1✔
85

86
        case admissionv1.Update:
1✔
87
                if err = wh.decoder.DecodeRaw(req.Object, obj); err != nil {
2✔
88
                        return admission.Errored(http.StatusBadRequest, err)
1✔
89
                }
1✔
90

91
                oldObj := &hcov1.HyperConverged{}
1✔
92
                if err = wh.decoder.DecodeRaw(req.OldObject, oldObj); err != nil {
2✔
93
                        return admission.Errored(http.StatusBadRequest, err)
1✔
94
                }
1✔
95

96
                return wh.validateUpdate(ctx, logger, dryRun, obj, oldObj)
1✔
97

98
        case admissionv1.Delete:
1✔
99
                if err = wh.decoder.DecodeRaw(req.OldObject, obj); err != nil {
2✔
100
                        return admission.Errored(http.StatusBadRequest, err)
1✔
101
                }
1✔
102

103
                return wh.validateDelete(ctx, logger, dryRun, obj)
1✔
104

105
        default:
1✔
106
                return admission.Errored(http.StatusBadRequest, fmt.Errorf("unknown operation request %q", req.Operation))
1✔
107
        }
108
}
109

110
func (wh *WebhookHandler) validateCreate(logger logr.Logger, dryrun bool, hc *hcov1.HyperConverged) admission.Response {
1✔
111
        logger.Info("Validating create", "name", hc.Name, "namespace:", hc.Namespace)
1✔
112

1✔
113
        warnings, err := wh.validateCreateHyperConverged(hc)
1✔
114
        if err != nil {
2✔
115
                return errToResponse(err, warnings)
1✔
116
        }
1✔
117

118
        err = wh.validateCreateComponents(hc)
1✔
119
        if err != nil {
2✔
120
                return errToResponse(err, warnings)
1✔
121
        }
1✔
122

123
        if !dryrun {
2✔
124
                tlssecprofile.SetHyperConvergedTLSSecurityProfile(hc.Spec.Security.TLSSecurityProfile)
1✔
125
        }
1✔
126

127
        return errToResponse(nil, warnings)
1✔
128
}
129

130
func (wh *WebhookHandler) validateUpdate(ctx context.Context, logger logr.Logger, dryrun bool, requested *hcov1.HyperConverged, exists *hcov1.HyperConverged) admission.Response {
1✔
131
        logger.Info("Validating update", "name", requested.Name)
1✔
132

1✔
133
        // If no change is detected in the spec nor the annotations - nothing to validate
1✔
134
        if reflect.DeepEqual(exists.Spec, requested.Spec) &&
1✔
135
                reflect.DeepEqual(exists.Annotations, requested.Annotations) {
2✔
136
                return admission.Allowed("")
1✔
137
        }
1✔
138

139
        warnings, err := wh.validateUpdateHyperConverged(requested, exists)
1✔
140
        if err != nil {
2✔
141
                return errToResponse(err, warnings)
1✔
142
        }
1✔
143

144
        if err = checkOperands(ctx, wh.cli, logger, requested, wh.isOpenshift); err != nil {
2✔
145
                return errToResponse(err, warnings)
1✔
146
        }
1✔
147

148
        if !dryrun {
2✔
149
                tlssecprofile.SetHyperConvergedTLSSecurityProfile(requested.Spec.Security.TLSSecurityProfile)
1✔
150
        }
1✔
151

152
        return errToResponse(nil, warnings)
1✔
153
}
154

155
func (wh *WebhookHandler) validateDelete(ctx context.Context, logger logr.Logger, dryrun bool, hc *hcov1.HyperConverged) admission.Response {
1✔
156
        logger.Info("Validating delete", "name", hc.Name, "namespace", hc.Namespace)
1✔
157

1✔
158
        var err error
1✔
159
        for _, obj := range []client.Object{
1✔
160
                handlers.NewKubeVirtWithNameOnly(),
1✔
161
                handlers.NewCDIWithNameOnly(),
1✔
162
        } {
2✔
163
                _, err = hcoutil.EnsureDeleted(ctx, wh.cli, obj, hc.Name, logger, true, false, true)
1✔
164
                if err != nil {
2✔
165
                        logger.Error(err, "Delete validation failed", "GVK", obj.GetObjectKind().GroupVersionKind())
1✔
166
                        break
1✔
167
                }
168
        }
169

170
        if err == nil && !dryrun {
2✔
171
                tlssecprofile.SetHyperConvergedTLSSecurityProfile(nil)
1✔
172
        }
1✔
173

174
        return errToResponse(err, nil)
1✔
175
}
176

177
func (wh *WebhookHandler) validateHyperConverged(hc *hcov1.HyperConverged) ([]string, error) {
1✔
178
        var warnings []string
1✔
179
        if err := wh.validateCertConfig(hc); err != nil {
2✔
180
                return nil, err
1✔
181
        }
1✔
182

183
        if err := wh.validateDataImportCronTemplates(hc); err != nil {
2✔
184
                return nil, err
1✔
185
        }
1✔
186

187
        if err := wh.validateTLSSecurityProfiles(hc); err != nil {
2✔
188
                return nil, err
1✔
189
        }
1✔
190

191
        if err := wh.validateAffinity(hc); err != nil {
2✔
192
                return nil, err
1✔
193
        }
1✔
194

195
        if warn := wh.validateTuningPolicy(hc); len(warn) > 0 {
2✔
196
                warnings = append(warnings, warn...)
1✔
197
        }
1✔
198

199
        return warnings, nil
1✔
200
}
201

202
func (wh *WebhookHandler) validateCreateHyperConverged(hc *hcov1.HyperConverged) ([]string, error) {
1✔
203
        var warnings []string
1✔
204

1✔
205
        warn, err := wh.validateHyperConverged(hc)
1✔
206
        if len(warn) > 0 {
2✔
207
                warnings = append(warnings, warn...)
1✔
208
        }
1✔
209

210
        if warn = wh.validateFeatureGatesOnCreate(hc); len(warn) > 0 {
2✔
211
                warnings = append(warnings, warn...)
1✔
212
        }
1✔
213

214
        return warnings, err
1✔
215
}
216

217
func (wh *WebhookHandler) validateUpdateHyperConverged(hc, oldHC *hcov1.HyperConverged) ([]string, error) {
1✔
218
        var warnings []string
1✔
219

1✔
220
        warn, err := wh.validateHyperConverged(hc)
1✔
221
        if len(warn) > 0 {
2✔
222
                warnings = append(warnings, warn...)
1✔
223
        }
1✔
224

225
        if warn = wh.validateFeatureGatesOnUpdate(hc, oldHC); len(warn) > 0 {
1✔
NEW
226
                warnings = append(warnings, warn...)
×
UNCOV
227
        }
×
228

229
        return warnings, err
1✔
230
}
231

232
func (wh *WebhookHandler) validateCreateComponents(hc *hcov1.HyperConverged) error {
1✔
233
        if _, err := handlers.NewKubeVirt(hc); err != nil {
2✔
234
                return err
1✔
235
        }
1✔
236

237
        if _, err := handlers.NewCDI(hc); err != nil {
2✔
238
                return err
1✔
239
        }
1✔
240

241
        if _, err := handlers.NewNetworkAddons(hc); err != nil {
2✔
242
                return err
1✔
243
        }
1✔
244

245
        if _, _, err := handlers.NewSSP(hc, true); err != nil {
2✔
246
                return err
1✔
247
        }
1✔
248

249
        return nil
1✔
250
}
251

252
func (wh *WebhookHandler) validateCertConfig(hc *hcov1.HyperConverged) error {
1✔
253
        minimalDuration := metav1.Duration{Duration: 10 * time.Minute}
1✔
254

1✔
255
        ccValues := make(map[string]time.Duration)
1✔
256
        ccValues["spec.certConfig.ca.duration"] = hc.Spec.Security.CertConfig.CA.Duration.Duration
1✔
257
        ccValues["spec.certConfig.ca.renewBefore"] = hc.Spec.Security.CertConfig.CA.RenewBefore.Duration
1✔
258
        ccValues["spec.certConfig.server.duration"] = hc.Spec.Security.CertConfig.Server.Duration.Duration
1✔
259
        ccValues["spec.certConfig.server.renewBefore"] = hc.Spec.Security.CertConfig.Server.RenewBefore.Duration
1✔
260

1✔
261
        for key, value := range ccValues {
2✔
262
                if value < minimalDuration.Duration {
2✔
263
                        return fmt.Errorf("%v: value is too small", key)
1✔
264
                }
1✔
265
        }
266

267
        if hc.Spec.Security.CertConfig.CA.Duration.Duration < hc.Spec.Security.CertConfig.CA.RenewBefore.Duration {
2✔
268
                return errors.New("spec.certConfig.ca: duration is smaller than renewBefore")
1✔
269
        }
1✔
270

271
        if hc.Spec.Security.CertConfig.Server.Duration.Duration < hc.Spec.Security.CertConfig.Server.RenewBefore.Duration {
2✔
272
                return errors.New("spec.certConfig.server: duration is smaller than renewBefore")
1✔
273
        }
1✔
274

275
        if hc.Spec.Security.CertConfig.CA.Duration.Duration < hc.Spec.Security.CertConfig.Server.Duration.Duration {
2✔
276
                return errors.New("spec.certConfig: ca.duration is smaller than server.duration")
1✔
277
        }
1✔
278

279
        return nil
1✔
280
}
281

282
func (wh *WebhookHandler) validateDataImportCronTemplates(hc *hcov1.HyperConverged) error {
1✔
283

1✔
284
        for _, dict := range hc.Spec.WorkloadSources.DataImportCronTemplates {
2✔
285
                val, ok := dict.Annotations[hcoutil.DataImportCronEnabledAnnotation]
1✔
286
                val = strings.ToLower(val)
1✔
287
                if ok && val != "false" && val != "true" {
2✔
288
                        return fmt.Errorf(`the %s annotation of a dataImportCronTemplate must be either "true" or "false"`, hcoutil.DataImportCronEnabledAnnotation)
1✔
289
                }
1✔
290

291
                enabled := !ok || val == "true"
1✔
292

1✔
293
                if enabled && dict.Spec == nil {
2✔
294
                        return fmt.Errorf("dataImportCronTemplate spec is empty for an enabled DataImportCronTemplate")
1✔
295
                }
1✔
296
        }
297

298
        return nil
1✔
299
}
300

301
func (wh *WebhookHandler) validateTLSSecurityProfiles(hc *hcov1.HyperConverged) error {
1✔
302
        tlsSP := hc.Spec.Security.TLSSecurityProfile
1✔
303

1✔
304
        if tlsSP == nil {
2✔
305
                return nil
1✔
306
        }
1✔
307

308
        if tlsSP.Custom == nil {
2✔
309
                if tlsSP.Type == openshiftconfigv1.TLSProfileCustomType {
2✔
310
                        return fmt.Errorf("missing required field spec.tlsSecurityProfile.custom when type is Custom")
1✔
311
                }
1✔
312
                return nil
1✔
313
        }
314

315
        if !isValidTLSProtocolVersion(tlsSP.Custom.MinTLSVersion) {
2✔
316
                return fmt.Errorf("invalid value for spec.tlsSecurityProfile.custom.minTLSVersion: %q", tlsSP.Custom.MinTLSVersion)
1✔
317
        }
1✔
318

319
        if tlsSP.Custom.MinTLSVersion < openshiftconfigv1.VersionTLS13 && !hasRequiredHTTP2Ciphers(tlsSP.Custom.Ciphers) {
2✔
320
                return fmt.Errorf("http2: TLSConfig.CipherSuites is missing an HTTP/2-required AES_128_GCM_SHA256 cipher (need at least one of ECDHE-RSA-AES128-GCM-SHA256 or ECDHE-ECDSA-AES128-GCM-SHA256)")
1✔
321
        } else if tlsSP.Custom.MinTLSVersion == openshiftconfigv1.VersionTLS13 && len(tlsSP.Custom.Ciphers) > 0 {
3✔
322
                return fmt.Errorf("custom ciphers cannot be selected when minTLSVersion is VersionTLS13")
1✔
323
        }
1✔
324

325
        return nil
1✔
326
}
327

328
func (wh *WebhookHandler) validateTuningPolicy(hc *hcov1.HyperConverged) []string {
1✔
329
        if hc.Spec.Virtualization.TuningPolicy == hcov1beta1.HyperConvergedHighBurstProfile { //nolint SA1019
2✔
330
                return []string{"spec.virtualization.tuningPolicy: the highBurst profile is not supported and ignored"}
1✔
331
        }
1✔
332

333
        return nil
1✔
334
}
335

336
func (wh *WebhookHandler) validateFeatureGatesOnCreate(hc *hcov1.HyperConverged) []string {
1✔
337
        fgMap := v1FGsToMap(hc.Spec.FeatureGates)
1✔
338

1✔
339
        return wh.validateDeprecatedFeatureGates(fgMap, nil)
1✔
340
}
1✔
341

342
func (wh *WebhookHandler) validateFeatureGatesOnUpdate(requested, exists *hcov1.HyperConverged) []string {
1✔
343
        reqFGMap := v1FGsToMap(requested.Spec.FeatureGates)
1✔
344
        oldFGMap := v1FGsToMap(exists.Spec.FeatureGates)
1✔
345

1✔
346
        return wh.validateDeprecatedFeatureGates(reqFGMap, oldFGMap)
1✔
347
}
1✔
348

349
func (wh *WebhookHandler) validateAffinity(hc *hcov1.HyperConverged) error {
1✔
350
        if hc.Spec.Deployment.NodePlacements == nil {
2✔
351
                return nil
1✔
352
        }
1✔
353

354
        nodePlacements := hc.Spec.Deployment.NodePlacements
1✔
355

1✔
356
        if nodePlacements.Workload != nil {
2✔
357
                if err := validateAffinity(nodePlacements.Workload.Affinity); err != nil {
2✔
358
                        return fmt.Errorf("invalid workloads node placement affinity: %v", err.Error())
1✔
359
                }
1✔
360
        }
361

362
        if nodePlacements.Infra != nil {
2✔
363
                if err := validateAffinity(nodePlacements.Infra.Affinity); err != nil {
2✔
364
                        return fmt.Errorf("invalid infra node placement affinity: %v", err.Error())
1✔
365
                }
1✔
366
        }
367

368
        return nil
1✔
369
}
370

371
const (
372
        fgv1Unknown            = "the %s featureGate is unknown and ignored."
373
        fgv1DeprecationWarning = "the %s featureGate is deprecated and will be removed in a future release."
374
)
375

376
func (wh *WebhookHandler) validateDeprecatedFeatureGates(fgMap, oldFgMap map[string]bool) []string {
1✔
377
        var warnings []string
1✔
378

1✔
379
        for fgName, enabled := range fgMap {
2✔
380
                phase, exists := featuregatedetails.GetFeatureGatePhase(fgName)
1✔
381
                if !exists {
2✔
382
                        warnings = append(warnings, fmt.Sprintf(fgv1Unknown, fgName))
1✔
383
                        continue
1✔
384
                }
385

386
                if phase != featuregates.PhaseDeprecated {
1✔
387
                        continue
×
388
                }
389

390
                if oldEnabled, oldExists := oldFgMap[fgName]; !oldExists || enabled != oldEnabled {
2✔
391
                        warnings = append(warnings, fmt.Sprintf(fgv1DeprecationWarning, fgName))
1✔
392
                }
1✔
393
        }
394

395
        return warnings
1✔
396
}
397

398
func hasRequiredHTTP2Ciphers(ciphers []string) bool {
1✔
399
        var requiredHTTP2Ciphers = []string{
1✔
400
                "ECDHE-RSA-AES128-GCM-SHA256",
1✔
401
                "ECDHE-ECDSA-AES128-GCM-SHA256",
1✔
402
        }
1✔
403

1✔
404
        // lo.Some returns true if at least 1 element of a subset is contained into a collection
1✔
405
        return lo.Some[string](requiredHTTP2Ciphers, ciphers)
1✔
406
}
1✔
407

408
// validationResponseFromStatus returns a response for admitting a request with provided Status object.
409
func validationResponseFromStatus(status metav1.Status, warnings []string) admission.Response {
1✔
410
        resp := admission.Response{
1✔
411
                AdmissionResponse: admissionv1.AdmissionResponse{
1✔
412
                        Allowed: false,
1✔
413
                        Result:  &status,
1✔
414
                },
1✔
415
        }
1✔
416

1✔
417
        if len(warnings) > 0 {
1✔
NEW
418
                resp = resp.WithWarnings(warnings...)
×
NEW
419
        }
×
420

421
        return resp
1✔
422
}
423

424
func isValidTLSProtocolVersion(pv openshiftconfigv1.TLSProtocolVersion) bool {
1✔
425
        switch pv {
1✔
426
        case
427
                openshiftconfigv1.VersionTLS10,
428
                openshiftconfigv1.VersionTLS11,
429
                openshiftconfigv1.VersionTLS12,
430
                openshiftconfigv1.VersionTLS13:
1✔
431
                return true
1✔
432
        }
433
        return false
1✔
434
}
435

436
func validateAffinity(affinity *corev1.Affinity) error {
1✔
437
        if affinity == nil || affinity.NodeAffinity == nil || affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil {
2✔
438
                return nil
1✔
439
        }
1✔
440

441
        _, err := nodeaffinity.NewNodeSelector(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution)
1✔
442

1✔
443
        return err
1✔
444
}
445

446
func errToResponse(err error, warnings []string) admission.Response {
1✔
447
        if err == nil {
2✔
448
                return withWarnings(admission.Allowed(""), warnings)
1✔
449
        }
1✔
450

451
        if apiStatus, ok := errors.AsType[*apierrors.StatusError](err); ok {
2✔
452
                return validationResponseFromStatus(apiStatus.Status(), warnings)
1✔
453
        }
1✔
454

455
        return withWarnings(admission.Denied(err.Error()), warnings)
1✔
456
}
457

458
func withWarnings(resp admission.Response, warnings []string) admission.Response {
1✔
459
        if len(warnings) > 0 {
2✔
460
                return resp.WithWarnings(warnings...)
1✔
461
        }
1✔
462

463
        return resp
1✔
464
}
465

466
func v1FGsToMap(fgs hcov1fg.HyperConvergedFeatureGates) map[string]bool {
1✔
467
        m := map[string]bool{}
1✔
468
        for _, fg := range fgs {
2✔
469
                m[fg.Name] = ptr.Deref(fg.State, hcov1fg.Enabled) == hcov1fg.Enabled
1✔
470
        }
1✔
471

472
        return m
1✔
473
}
474

475
func checkOperands(ctx context.Context, cli client.Client, logger logr.Logger, requested *hcov1.HyperConverged, isOpenshift bool) error {
1✔
476
        if requested.DeletionTimestamp != nil { // do not check other components when removing HCO
2✔
477
                return nil
1✔
478
        }
1✔
479

480
        resources, err := getOperands(ctx, cli, isOpenshift)
1✔
481
        if err != nil {
2✔
482
                return err
1✔
483
        }
1✔
484

485
        toCtx, cancel := context.WithTimeout(ctx, updateDryRunTimeOut)
1✔
486
        defer cancel()
1✔
487

1✔
488
        eg, egCtx := xsync.WithContext(toCtx)
1✔
489
        opts := &client.UpdateOptions{DryRun: []string{metav1.DryRunAll}}
1✔
490

1✔
491
        for _, obj := range resources {
2✔
492
                func(o client.Object) {
2✔
493
                        eg.Go(func() error {
2✔
494
                                return updateOperatorCr(egCtx, cli, logger, requested, o, opts)
1✔
495
                        })
1✔
496
                }(obj)
497
        }
498

499
        return eg.Wait()
1✔
500
}
501

502
func getOperands(ctx context.Context, cli client.Client, isOpenshift bool) ([]client.Object, error) {
1✔
503
        kv := handlers.NewKubeVirtWithNameOnly()
1✔
504
        err := cli.Get(ctx, client.ObjectKeyFromObject(kv), kv)
1✔
505
        if err != nil {
2✔
506
                return nil, err
1✔
507
        }
1✔
508

509
        cdi := handlers.NewCDIWithNameOnly()
1✔
510
        err = cli.Get(ctx, client.ObjectKeyFromObject(cdi), cdi)
1✔
511
        if err != nil {
2✔
512
                return nil, err
1✔
513
        }
1✔
514

515
        cna := handlers.NewNetworkAddonsWithNameOnly()
1✔
516
        err = cli.Get(ctx, client.ObjectKeyFromObject(cna), cna)
1✔
517
        if err != nil {
2✔
518
                return nil, err
1✔
519
        }
1✔
520

521
        resources := make([]client.Object, 0, 4)
1✔
522
        resources = append(resources, kv, cdi, cna)
1✔
523

1✔
524
        if isOpenshift {
2✔
525
                ssp := handlers.NewSSPWithNameOnly()
1✔
526
                err = cli.Get(ctx, client.ObjectKeyFromObject(ssp), ssp)
1✔
527
                if err != nil {
2✔
528
                        return nil, err
1✔
529
                }
1✔
530

531
                resources = append(resources, ssp)
1✔
532
        }
533

534
        return resources, nil
1✔
535
}
536

537
const dryRunMaxRetries = 3
538

539
func updateOperatorCr(ctx context.Context, cli client.Client, logger logr.Logger, hc *hcov1.HyperConverged, exists client.Object, opts *client.UpdateOptions) error {
1✔
540
        for attempt := range dryRunMaxRetries {
2✔
541
                if attempt > 0 {
1✔
542
                        if err := cli.Get(ctx, client.ObjectKeyFromObject(exists), exists); err != nil {
×
543
                                logger.Error(err, "failed to re-fetch object for dry-run retry", "kind", exists.GetObjectKind())
×
544
                                return err
×
545
                        }
×
546
                }
547

548
                if err := applyDesiredSpec(hc, exists); err != nil {
2✔
549
                        return err
1✔
550
                }
1✔
551

552
                err := cli.Update(ctx, exists, opts)
1✔
553
                if err == nil {
2✔
554
                        logger.Info("dry-run update the object passed", "kind", exists.GetObjectKind())
1✔
555
                        return nil
1✔
556
                }
1✔
557

558
                if !apierrors.IsConflict(err) {
2✔
559
                        logger.Error(err, "failed to dry-run update the object", "kind", exists.GetObjectKind())
1✔
560
                        return err
1✔
561
                }
1✔
562

563
                logger.Info("dry-run update conflict, retrying", "kind", exists.GetObjectKind(), "attempt", attempt+1)
×
564
        }
565

566
        return fmt.Errorf("failed to dry-run update %v after %d retries due to persistent conflicts", exists.GetObjectKind(), dryRunMaxRetries)
×
567
}
568

569
func applyDesiredSpec(hc *hcov1.HyperConverged, exists client.Object) error {
1✔
570
        switch existing := exists.(type) {
1✔
571
        case *kubevirtcorev1.KubeVirt:
1✔
572
                required, err := handlers.NewKubeVirt(hc)
1✔
573
                if err != nil {
2✔
574
                        return err
1✔
575
                }
1✔
576
                required.Spec.DeepCopyInto(&existing.Spec)
1✔
577

578
        case *cdiv1beta1.CDI:
1✔
579
                required, err := handlers.NewCDI(hc)
1✔
580
                if err != nil {
2✔
581
                        return err
1✔
582
                }
1✔
583
                required.Spec.DeepCopyInto(&existing.Spec)
1✔
584

585
        case *networkaddonsv1.NetworkAddonsConfig:
1✔
586
                required, err := handlers.NewNetworkAddons(hc)
1✔
587
                if err != nil {
2✔
588
                        return err
1✔
589
                }
1✔
590
                required.Spec.DeepCopyInto(&existing.Spec)
1✔
591

592
        case *sspv1beta3.SSP:
1✔
593
                required, _, err := handlers.NewSSP(hc, true)
1✔
594
                if err != nil {
2✔
595
                        return err
1✔
596
                }
1✔
597
                required.Spec.DeepCopyInto(&existing.Spec)
1✔
598
        }
599

600
        return nil
1✔
601
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc