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

kubevirt / containerized-data-importer / #4794

14 Jul 2024 06:12PM UTC coverage: 58.983% (+0.01%) from 58.972%
#4794

push

travis-ci

web-flow
update to k8s 1.30 libs and controller-runtime 0.18.4 (#3336)

* make deps-update

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* ReourceRequirements -> VolumeResourceRequirements

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* fix calls to controller.Watch()

controller-runtime changed the API!

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* Fix errors with actual openshift/library-go lib

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* make all works now and everything compiles

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* fix "make update-codegen" because generate_groups.sh deprecated

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* run "make generate"

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

* fix transfer unittest because of change to controller-runtime

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

---------

Signed-off-by: Michael Henriksen <mhenriks@redhat.com>

6 of 238 new or added lines in 24 files covered. (2.52%)

10 existing lines in 4 files now uncovered.

16454 of 27896 relevant lines covered (58.98%)

0.65 hits per line

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

55.91
/pkg/controller/config-controller.go
1
package controller
2

3
import (
4
        "context"
5
        "crypto/x509"
6
        "encoding/pem"
7
        "fmt"
8
        "reflect"
9
        "regexp"
10
        "slices"
11
        "strings"
12
        "time"
13

14
        "github.com/go-logr/logr"
15
        ocpconfigv1 "github.com/openshift/api/config/v1"
16
        routev1 "github.com/openshift/api/route/v1"
17
        "github.com/pkg/errors"
18

19
        corev1 "k8s.io/api/core/v1"
20
        v1 "k8s.io/api/core/v1"
21
        networkingv1 "k8s.io/api/networking/v1"
22
        storagev1 "k8s.io/api/storage/v1"
23
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
24
        "k8s.io/apimachinery/pkg/api/meta"
25
        "k8s.io/apimachinery/pkg/api/resource"
26
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27
        "k8s.io/apimachinery/pkg/runtime"
28
        "k8s.io/apimachinery/pkg/types"
29
        "k8s.io/client-go/tools/record"
30

31
        "sigs.k8s.io/controller-runtime/pkg/client"
32
        "sigs.k8s.io/controller-runtime/pkg/controller"
33
        "sigs.k8s.io/controller-runtime/pkg/event"
34
        "sigs.k8s.io/controller-runtime/pkg/handler"
35
        "sigs.k8s.io/controller-runtime/pkg/manager"
36
        "sigs.k8s.io/controller-runtime/pkg/predicate"
37
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
38
        "sigs.k8s.io/controller-runtime/pkg/source"
39

40
        cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
41
        "kubevirt.io/containerized-data-importer/pkg/common"
42
        cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
43
        "kubevirt.io/containerized-data-importer/pkg/operator"
44
        "kubevirt.io/containerized-data-importer/pkg/util"
45
        "kubevirt.io/containerized-data-importer/pkg/util/cert"
46
)
47

48
// AnnConfigAuthority is the annotation specifying a resource as the CDIConfig authority
49
const (
50
        AnnConfigAuthority = "cdi.kubevirt.io/configAuthority"
51

52
        errResourceDoesntExist     = "ErrResourceDoesntExist"
53
        messageResourceDoesntExist = "Resource managed by %q doesn't exist"
54

55
        defaultCPULimit   = "750m"
56
        defaultMemLimit   = "600M"
57
        defaultCPURequest = "100m"
58
        defaultMemRequest = "60M"
59

60
        rootCertificateConfigMap = "kube-root-ca.crt"
61
)
62

63
// CDIConfigReconciler members
64
type CDIConfigReconciler struct {
65
        client client.Client
66
        // use this for getting any resources not in the install namespace or cluster scope
67
        uncachedClient         client.Client
68
        recorder               record.EventRecorder
69
        scheme                 *runtime.Scheme
70
        log                    logr.Logger
71
        uploadProxyServiceName string
72
        configName             string
73
        cdiNamespace           string
74
        installerLabels        map[string]string
75
}
76

77
// Reconcile the reconcile loop for the CDIConfig object.
78
func (r *CDIConfigReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
1✔
79
        log := r.log.WithValues("CDIConfig", req.NamespacedName)
1✔
80
        log.Info("reconciling CDIConfig")
1✔
81

1✔
82
        config, err := r.createCDIConfig()
1✔
83
        if err != nil {
1✔
84
                log.Error(err, "Unable to create CDIConfig")
×
85
                return reconcile.Result{}, err
×
86
        }
×
87
        // Keep a copy of the original for comparison later.
88
        currentConfigCopy := config.DeepCopyObject()
1✔
89

1✔
90
        config.Status.Preallocation = config.Spec.Preallocation != nil && *config.Spec.Preallocation
1✔
91

1✔
92
        // ignore whatever is in config spec and set to operator view
1✔
93
        if err := r.setOperatorParams(config); err != nil {
1✔
94
                return reconcile.Result{}, err
×
95
        }
×
96

97
        if err := r.reconcileUploadProxy(config); err != nil {
1✔
98
                return reconcile.Result{}, err
×
99
        }
×
100

101
        if err := r.reconcileStorageClass(config); err != nil {
1✔
102
                return reconcile.Result{}, err
×
103
        }
×
104

105
        if err := r.reconcileDefaultPodResourceRequirements(config); err != nil {
1✔
106
                return reconcile.Result{}, err
×
107
        }
×
108

109
        if err := r.reconcileImagePullSecrets(config); err != nil {
1✔
110
                return reconcile.Result{}, err
×
111
        }
×
112

113
        if err := r.reconcileFilesystemOverhead(config); err != nil {
1✔
114
                return reconcile.Result{}, err
×
115
        }
×
116

117
        if err := r.reconcileImportProxy(config); err != nil {
2✔
118
                return reconcile.Result{}, err
1✔
119
        }
1✔
120

121
        if !reflect.DeepEqual(currentConfigCopy, config) {
2✔
122
                // Updates have happened, update CDIConfig.
1✔
123
                log.Info("Updating CDIConfig", "CDIConfig.Name", config.Name, "config", config)
1✔
124
                if err := r.client.Update(context.TODO(), config); err != nil {
1✔
125
                        return reconcile.Result{}, err
×
126
                }
×
127
        }
128

129
        return reconcile.Result{}, nil
1✔
130
}
131

132
func (r *CDIConfigReconciler) setOperatorParams(config *cdiv1.CDIConfig) error {
1✔
133
        util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")
1✔
134

1✔
135
        cdiCR, err := cc.GetActiveCDI(context.TODO(), r.client)
1✔
136
        if err != nil {
1✔
137
                return err
×
138
        }
×
139

140
        if cdiCR == nil {
1✔
141
                return nil
×
142
        }
×
143

144
        if _, ok := cdiCR.Annotations[AnnConfigAuthority]; !ok {
2✔
145
                return nil
1✔
146
        }
1✔
147

148
        if cdiCR.Spec.Config == nil {
2✔
149
                config.Spec = cdiv1.CDIConfigSpec{}
1✔
150
        } else {
2✔
151
                config.Spec = *cdiCR.Spec.Config
1✔
152
        }
1✔
153

154
        return nil
1✔
155
}
156

157
func (r *CDIConfigReconciler) reconcileUploadProxy(config *cdiv1.CDIConfig) error {
1✔
158
        log := r.log.WithName("CDIconfig").WithName("UploadProxyReconcile")
1✔
159
        config.Status.UploadProxyURL = config.Spec.UploadProxyURLOverride
1✔
160
        // No override, try Ingress
1✔
161
        if config.Status.UploadProxyURL == nil {
2✔
162
                ingress, err := r.reconcileIngress(config)
1✔
163
                if err != nil {
1✔
164
                        log.Error(err, "Unable to reconcile Ingress")
×
165
                        return err
×
166
                }
×
167

168
                if ingress != nil {
2✔
169
                        if err := r.reconcileUploadProxyIngressCA(config, *ingress); err != nil {
1✔
170
                                log.Error(err, "Unable to reconcile Ingress CA")
×
171
                                return fmt.Errorf("unable to reconcile Ingress CA: %w", err)
×
172
                        }
×
173
                }
174
        }
175
        // No override or Ingress, try Route
176
        if config.Status.UploadProxyURL == nil {
2✔
177
                if err := r.reconcileRoute(config); err != nil {
1✔
178
                        log.Error(err, "Unable to reconcile Routes")
×
179
                        return err
×
180
                }
×
181

182
                if err := r.reconcileUploadProxyRouteCA(config); err != nil {
1✔
183
                        log.Error(err, "Unable to reconcile Route CA")
×
184
                        return fmt.Errorf("unable to reconcile Route CA: %w", err)
×
185
                }
×
186
        }
187
        return nil
1✔
188
}
189

190
func (r *CDIConfigReconciler) reconcileUploadProxyIngressCA(config *cdiv1.CDIConfig, ingress networkingv1.Ingress) error {
1✔
191
        log := r.log.WithName("CDIconfig").WithName("UploadProxyIngressCAReconcile")
1✔
192

1✔
193
        url := config.Status.UploadProxyURL
1✔
194
        if url == nil || *url == "" {
1✔
195
                return nil
×
196
        }
×
197

198
        var secretName string
1✔
199
        i := slices.IndexFunc(ingress.Spec.TLS, func(tls networkingv1.IngressTLS) bool { return tls.SecretName != "" })
2✔
200
        if i == -1 {
2✔
201
                log.Info("Secret name not found in Ingress")
1✔
202
                config.Status.UploadProxyCA = nil
1✔
203
                return nil
1✔
204
        }
1✔
205
        secretName = ingress.Spec.TLS[i].SecretName
1✔
206

1✔
207
        var secret corev1.Secret
1✔
208
        err := r.client.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.cdiNamespace}, &secret)
1✔
209
        if err != nil {
1✔
210
                return fmt.Errorf("unable to get secret %q: %v", secretName, err)
×
211
        }
×
212

213
        certBytes, ok := secret.Data["tls.crt"]
1✔
214
        if !ok {
1✔
215
                log.Info(fmt.Sprintf("Secret %q does not contain %q", secretName, "tls.crt"))
×
216
                config.Status.UploadProxyCA = nil
×
217
                return nil
×
218
        }
×
219

220
        certs, err := cert.ParseCertsPEM(certBytes)
1✔
221
        if err != nil {
1✔
222
                return fmt.Errorf("unable to parse tls.crt: %v", err)
×
223
        }
×
224

225
        s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
1✔
226
        if err != nil {
1✔
227
                return err
×
228
        } else if s == "" {
1✔
229
                log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
×
230
                config.Status.UploadProxyCA = nil
×
231
                return nil
×
232
        }
×
233

234
        log.Info("Setting upload proxy CA", "UploadProxyCA", s)
1✔
235
        config.Status.UploadProxyCA = &s
1✔
236
        return nil
1✔
237
}
238

239
func (r *CDIConfigReconciler) reconcileUploadProxyRouteCA(config *cdiv1.CDIConfig) error {
1✔
240
        log := r.log.WithName("CDIconfig").WithName("UploadProxyRouteCAReconcile")
1✔
241

1✔
242
        if config.Status.UploadProxyURL == nil || *config.Status.UploadProxyURL == "" {
2✔
243
                log.Info("No upload proxy URL found, setting upload proxy CA to blank")
1✔
244
                config.Status.UploadProxyCA = nil
1✔
245
                return nil
1✔
246
        }
1✔
247

248
        var cm corev1.ConfigMap
1✔
249
        err := r.client.Get(context.TODO(), types.NamespacedName{Name: rootCertificateConfigMap, Namespace: r.cdiNamespace}, &cm)
1✔
250
        if err != nil {
1✔
251
                log.Info(fmt.Sprintf("Could not get certificates: %v", err))
×
252
                config.Status.UploadProxyCA = nil
×
253
                return nil
×
254
        }
×
255

256
        rawCert, ok := cm.Data["ca.crt"]
1✔
257
        if !ok {
1✔
258
                log.Info(fmt.Sprintf("Config map %q does not contain %q", rootCertificateConfigMap, "ca.crt"))
×
259
                config.Status.UploadProxyCA = nil
×
260
                return nil
×
261
        }
×
262

263
        certs, err := cert.ParseCertsPEM([]byte(rawCert))
1✔
264
        if err != nil {
1✔
265
                return fmt.Errorf("unable to parse ca.crt: %v", err)
×
266
        }
×
267

268
        s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
1✔
269
        if err != nil {
1✔
270
                return err
×
271
        } else if s == "" {
1✔
272
                log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
×
273
                config.Status.UploadProxyCA = nil
×
274
                return nil
×
275
        }
×
276

277
        log.Info("Setting upload proxy CA", "UploadProxyCA", s)
1✔
278
        config.Status.UploadProxyCA = &s
1✔
279
        return nil
1✔
280
}
281

282
func findCertByHostName(hostName string, certs []*x509.Certificate) (string, error) {
1✔
283
        now := time.Now()
1✔
284
        var latestValidCert *x509.Certificate
1✔
285
        for _, cert := range certs {
2✔
286
                // Check validity
1✔
287
                if now.After(cert.NotAfter) {
1✔
288
                        continue
×
289
                }
290
                if now.Before(cert.NotBefore) {
1✔
291
                        continue
×
292
                }
293
                if err := cert.VerifyHostname(hostName); err != nil {
2✔
294
                        continue
1✔
295
                }
296

297
                // Check if this is the cert with the latest expiration date
298
                if latestValidCert == nil {
2✔
299
                        latestValidCert = cert
1✔
300
                        continue
1✔
301
                }
302
                if latestValidCert.NotAfter.After(cert.NotAfter) {
×
303
                        continue
×
304
                }
305
                latestValidCert = cert
×
306
        }
307

308
        if latestValidCert != nil {
2✔
309
                return buildPemFromCert(latestValidCert, certs)
1✔
310
        }
1✔
311

312
        if len(certs) > 0 {
2✔
313
                return buildPemFromAllCerts(certs)
1✔
314
        }
1✔
315

316
        return "", nil
×
317
}
318

319
func buildPemFromCert(matchingCert *x509.Certificate, allCerts []*x509.Certificate) (string, error) {
1✔
320
        pemOut := strings.Builder{}
1✔
321

1✔
322
        if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: matchingCert.Raw}); err != nil {
1✔
323
                return "", fmt.Errorf("could not encode certificate: %w", err)
×
324
        }
×
325

326
        if matchingCert.Issuer.CommonName != matchingCert.Subject.CommonName && !matchingCert.IsCA {
2✔
327
                //lookup issuer recursively, if not found a blank is returned.
1✔
328
                chain, err := findCertByHostName(matchingCert.Issuer.CommonName, allCerts)
1✔
329
                if err != nil {
1✔
330
                        return "", err
×
331
                }
×
332

333
                if _, err := pemOut.WriteString(chain); err != nil {
1✔
334
                        return "", fmt.Errorf("could not write issuer certificate: %w", err)
×
335
                }
×
336
        }
337

338
        return strings.TrimSpace(pemOut.String()), nil
1✔
339
}
340

341
func buildPemFromAllCerts(allCerts []*x509.Certificate) (string, error) {
1✔
342
        now := time.Now()
1✔
343
        pemOut := strings.Builder{}
1✔
344
        for _, cert := range allCerts {
2✔
345
                if now.After(cert.NotAfter) {
1✔
346
                        continue
×
347
                }
348

349
                if now.Before(cert.NotBefore) {
1✔
350
                        continue
×
351
                }
352

353
                if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
1✔
354
                        return "", fmt.Errorf("could not encode certificate: %w", err)
×
355
                }
×
356
        }
357

358
        return strings.TrimSpace(pemOut.String()), nil
1✔
359
}
360

361
func (r *CDIConfigReconciler) reconcileIngress(config *cdiv1.CDIConfig) (*networkingv1.Ingress, error) {
1✔
362
        log := r.log.WithName("CDIconfig").WithName("IngressReconcile")
1✔
363
        ingressList := &networkingv1.IngressList{}
1✔
364
        if err := r.client.List(context.TODO(), ingressList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
1✔
365
                return nil, err
×
366
        }
×
367
        for _, ingress := range ingressList.Items {
2✔
368
                ingressURL := getURLFromIngress(&ingress, r.uploadProxyServiceName)
1✔
369
                if ingressURL != "" {
2✔
370
                        log.Info("Setting upload proxy url", "IngressURL", ingressURL)
1✔
371
                        config.Status.UploadProxyURL = &ingressURL
1✔
372
                        return &ingress, nil
1✔
373
                }
1✔
374
        }
375
        log.Info("No ingress found, setting to blank", "IngressURL", "")
1✔
376
        config.Status.UploadProxyURL = nil
1✔
377
        return nil, nil
1✔
378
}
379

380
func (r *CDIConfigReconciler) reconcileRoute(config *cdiv1.CDIConfig) error {
1✔
381
        log := r.log.WithName("CDIconfig").WithName("RouteReconcile")
1✔
382
        routeList := &routev1.RouteList{}
1✔
383
        if err := r.client.List(context.TODO(), routeList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
1✔
384
                return err
×
385
        }
×
386
        for _, route := range routeList.Items {
2✔
387
                routeURL := getURLFromRoute(&route, r.uploadProxyServiceName)
1✔
388
                if routeURL != "" {
2✔
389
                        log.Info("Setting upload proxy url", "RouteURL", routeURL)
1✔
390
                        config.Status.UploadProxyURL = &routeURL
1✔
391
                        return nil
1✔
392
                }
1✔
393
        }
394
        log.Info("No route found, setting to blank", "RouteURL", "")
1✔
395
        config.Status.UploadProxyURL = nil
1✔
396
        return nil
1✔
397
}
398

399
func (r *CDIConfigReconciler) reconcileStorageClass(config *cdiv1.CDIConfig) error {
1✔
400
        log := r.log.WithName("CDIconfig").WithName("StorageClassReconcile")
1✔
401
        storageClassList := &storagev1.StorageClassList{}
1✔
402
        if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
1✔
403
                return err
×
404
        }
×
405

406
        // Check config for scratch space class
407
        if config.Spec.ScratchSpaceStorageClass != nil {
2✔
408
                for _, storageClass := range storageClassList.Items {
2✔
409
                        if storageClass.Name == *config.Spec.ScratchSpaceStorageClass {
2✔
410
                                log.Info("Setting scratch space to override", "storageClass.Name", storageClass.Name)
1✔
411
                                config.Status.ScratchSpaceStorageClass = storageClass.Name
1✔
412
                                return nil
1✔
413
                        }
1✔
414
                }
415
        }
416
        // Check for default storage class.
417
        for _, storageClass := range storageClassList.Items {
2✔
418
                if defaultClassValue, ok := storageClass.Annotations[cc.AnnDefaultStorageClass]; ok {
2✔
419
                        if defaultClassValue == "true" {
2✔
420
                                log.Info("Setting scratch space to default", "storageClass.Name", storageClass.Name)
1✔
421
                                config.Status.ScratchSpaceStorageClass = storageClass.Name
1✔
422
                                return nil
1✔
423
                        }
1✔
424
                }
425
        }
426
        log.Info("No default storage class found, setting scratch space to blank")
1✔
427
        // No storage class found, blank it out.
1✔
428
        config.Status.ScratchSpaceStorageClass = ""
1✔
429
        return nil
1✔
430
}
431

432
func (r *CDIConfigReconciler) reconcileImagePullSecrets(config *cdiv1.CDIConfig) error {
1✔
433
        config.Status.ImagePullSecrets = config.Spec.ImagePullSecrets
1✔
434
        return nil
1✔
435
}
1✔
436

437
func (r *CDIConfigReconciler) reconcileDefaultPodResourceRequirements(config *cdiv1.CDIConfig) error {
1✔
438
        cpuLimit, _ := resource.ParseQuantity(defaultCPULimit)
1✔
439
        memLimit, _ := resource.ParseQuantity(defaultMemLimit)
1✔
440
        cpuRequest, _ := resource.ParseQuantity(defaultCPURequest)
1✔
441
        memRequest, _ := resource.ParseQuantity(defaultMemRequest)
1✔
442
        config.Status.DefaultPodResourceRequirements = &v1.ResourceRequirements{
1✔
443
                Limits: map[v1.ResourceName]resource.Quantity{
1✔
444
                        v1.ResourceCPU:    cpuLimit,
1✔
445
                        v1.ResourceMemory: memLimit,
1✔
446
                },
1✔
447
                Requests: map[v1.ResourceName]resource.Quantity{
1✔
448
                        v1.ResourceCPU:    cpuRequest,
1✔
449
                        v1.ResourceMemory: memRequest,
1✔
450
                },
1✔
451
        }
1✔
452

1✔
453
        if config.Spec.PodResourceRequirements != nil {
2✔
454
                if config.Spec.PodResourceRequirements.Limits != nil {
2✔
455
                        if cpu, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceCPU]; exist {
2✔
456
                                config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceCPU] = cpu
1✔
457
                        }
1✔
458

459
                        if memory, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceMemory]; exist {
2✔
460
                                config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceMemory] = memory
1✔
461
                        }
1✔
462
                }
463

464
                if config.Spec.PodResourceRequirements.Requests != nil {
2✔
465
                        if cpu, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceCPU]; exist {
2✔
466
                                config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceCPU] = cpu
1✔
467
                        }
1✔
468

469
                        if memory, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceMemory]; exist {
2✔
470
                                config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceMemory] = memory
1✔
471
                        }
1✔
472
                }
473
        }
474

475
        return nil
1✔
476
}
477

478
func (r *CDIConfigReconciler) reconcileFilesystemOverhead(config *cdiv1.CDIConfig) error {
1✔
479
        var globalOverhead cdiv1.Percent = common.DefaultGlobalOverhead
1✔
480
        var perStorageConfig = make(map[string]cdiv1.Percent)
1✔
481

1✔
482
        log := r.log.WithName("CDIconfig").WithName("FilesystemOverhead")
1✔
483

1✔
484
        // Avoid nil maps and segfaults for the initial case, where filesystemOverhead
1✔
485
        // is nil for both the spec and the status.
1✔
486
        if config.Status.FilesystemOverhead == nil {
2✔
487
                log.Info("No filesystem overhead found in status, initializing to defaults")
1✔
488
                config.Status.FilesystemOverhead = &cdiv1.FilesystemOverhead{
1✔
489
                        Global:       globalOverhead,
1✔
490
                        StorageClass: make(map[string]cdiv1.Percent),
1✔
491
                }
1✔
492
        }
1✔
493

494
        if config.Spec.FilesystemOverhead != nil {
1✔
495
                if valid, _ := validOverhead(config.Spec.FilesystemOverhead.Global); valid {
×
496
                        globalOverhead = config.Spec.FilesystemOverhead.Global
×
497
                }
×
498
                if config.Spec.FilesystemOverhead.StorageClass != nil {
×
499
                        perStorageConfig = config.Spec.FilesystemOverhead.StorageClass
×
500
                }
×
501
        }
502

503
        // Set status global overhead
504
        config.Status.FilesystemOverhead.Global = globalOverhead
1✔
505

1✔
506
        // Set status per-storageClass overhead
1✔
507
        storageClassList := &storagev1.StorageClassList{}
1✔
508
        if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
1✔
509
                return err
×
510
        }
×
511
        config.Status.FilesystemOverhead.StorageClass = make(map[string]cdiv1.Percent)
1✔
512
        for _, storageClass := range storageClassList.Items {
1✔
513
                storageClassName := storageClass.GetName()
×
514
                storageClassNameOverhead, found := perStorageConfig[storageClassName]
×
515

×
516
                if found {
×
517
                        valid, err := validOverhead(storageClassNameOverhead)
×
518
                        if !valid {
×
519
                                return err
×
520
                        }
×
521
                        config.Status.FilesystemOverhead.StorageClass[storageClassName] = storageClassNameOverhead
×
522
                } else {
×
523
                        config.Status.FilesystemOverhead.StorageClass[storageClassName] = globalOverhead
×
524
                }
×
525
        }
526

527
        return nil
1✔
528
}
529

530
func validOverhead(overhead cdiv1.Percent) (bool, error) {
×
531
        return regexp.MatchString(`^(0(?:\.\d{1,3})?|1)$`, string(overhead))
×
532
}
×
533

534
// createCDIConfig creates a new instance of the CDIConfig object if it doesn't exist already, and returns the existing one if found.
535
// It also sets the operator to be the owner of the CDIConfig object.
536
func (r *CDIConfigReconciler) createCDIConfig() (*cdiv1.CDIConfig, error) {
1✔
537
        config := &cdiv1.CDIConfig{}
1✔
538
        if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err != nil {
2✔
539
                if k8serrors.IsNotFound(err) {
2✔
540
                        config = cc.MakeEmptyCDIConfigSpec(r.configName)
1✔
541
                        if err := operator.SetOwnerRuntime(r.uncachedClient, config); err != nil {
1✔
542
                                return nil, err
×
543
                        }
×
544
                        util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")
1✔
545
                        if err := r.client.Create(context.TODO(), config); err != nil {
1✔
546
                                if k8serrors.IsAlreadyExists(err) {
×
547
                                        config := &cdiv1.CDIConfig{}
×
548
                                        if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err == nil {
×
549
                                                return config, nil
×
550
                                        }
×
551
                                        return nil, err
×
552
                                }
553
                                return nil, err
×
554
                        }
555
                } else {
×
556
                        return nil, err
×
557
                }
×
558
        }
559
        return config, nil
1✔
560
}
561

562
func (r *CDIConfigReconciler) reconcileImportProxy(config *cdiv1.CDIConfig) error {
1✔
563
        config.Status.ImportProxy = config.Spec.ImportProxy
1✔
564

1✔
565
        // Avoid nil pointers and segfaults for the initial case, where ImportProxy is nil for both the spec and the status.
1✔
566
        if config.Status.ImportProxy == nil {
2✔
567
                config.Status.ImportProxy = &cdiv1.ImportProxy{
1✔
568
                        HTTPProxy:      new(string),
1✔
569
                        HTTPSProxy:     new(string),
1✔
570
                        NoProxy:        new(string),
1✔
571
                        TrustedCAProxy: new(string),
1✔
572
                }
1✔
573

1✔
574
                // Try Openshift cluster wide proxy only if the CDIConfig default config is empty
1✔
575
                clusterWideProxy, err := getClusterWideProxy(r.client)
1✔
576
                if err != nil {
1✔
577
                        return err
×
578
                }
×
579
                config.Status.ImportProxy.HTTPProxy = &clusterWideProxy.Status.HTTPProxy
1✔
580
                config.Status.ImportProxy.HTTPSProxy = &clusterWideProxy.Status.HTTPSProxy
1✔
581
                config.Status.ImportProxy.NoProxy = &clusterWideProxy.Status.NoProxy
1✔
582
                if err := r.reconcileImportProxyCAConfigMap(config, clusterWideProxy); err != nil {
2✔
583
                        return err
1✔
584
                }
1✔
585
                config.Status.ImportProxy.TrustedCAProxy = &clusterWideProxy.Spec.TrustedCA.Name
1✔
586
        }
587
        return nil
1✔
588
}
589

590
// Create/Update a configmap with the CA certificates in the controllor context with the cluster-wide proxy CA certificates to be used by the importer pod
591
func (r *CDIConfigReconciler) reconcileImportProxyCAConfigMap(config *cdiv1.CDIConfig, clusterWideProxy *ocpconfigv1.Proxy) error {
1✔
592
        cmOldName := config.Status.ImportProxy.TrustedCAProxy
1✔
593
        cmName := clusterWideProxy.Spec.TrustedCA.Name
1✔
594
        client := r.uncachedClient
1✔
595

1✔
596
        // Delete old ConfigMap if name changed
1✔
597
        if cmOldName != nil && *cmOldName != "" && *cmOldName != cmName {
1✔
598
                if err := client.Delete(context.TODO(), r.createProxyConfigMap(*cmOldName, "")); err != nil && !k8serrors.IsNotFound(err) {
×
599
                        return err
×
600
                }
×
601
        }
602
        if cmName == "" {
2✔
603
                return nil
1✔
604
        }
1✔
605

606
        clusterWideProxyConfigMap := &v1.ConfigMap{}
1✔
607
        if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: ClusterWideProxyConfigMapNameSpace}, clusterWideProxyConfigMap); err != nil {
2✔
608
                if k8serrors.IsNotFound(err) {
2✔
609
                        msg := fmt.Sprintf(messageResourceDoesntExist, cmName)
1✔
610
                        r.recorder.Event(clusterWideProxy, v1.EventTypeWarning, errResourceDoesntExist, msg)
1✔
611
                }
1✔
612
                return err
1✔
613
        }
614
        // Copy the cluster-wide proxy CA certificates to the importer pod proxy CA certificates configMap
615
        certBytes, ok := clusterWideProxyConfigMap.Data[ClusterWideProxyConfigMapKey]
1✔
616
        if !ok {
1✔
617
                return fmt.Errorf("no cluster-wide proxy CA certificate")
×
618
        }
×
619
        configMap := &v1.ConfigMap{}
1✔
620
        if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: r.cdiNamespace}, configMap); err != nil {
2✔
621
                if !k8serrors.IsNotFound(err) {
1✔
622
                        return err
×
623
                }
×
624
                proxyConfigMap := r.createProxyConfigMap(cmName, certBytes)
1✔
625
                util.SetRecommendedLabels(proxyConfigMap, r.installerLabels, "cdi-controller")
1✔
626
                if err := client.Create(context.TODO(), proxyConfigMap); err != nil {
1✔
627
                        return err
×
628
                }
×
629
                return nil
1✔
630
        }
631
        configMap.Data[common.ImportProxyConfigMapKey] = certBytes
×
632
        util.SetRecommendedLabels(configMap, r.installerLabels, "cdi-controller")
×
633
        if err := client.Update(context.TODO(), configMap); err != nil {
×
634
                return err
×
635
        }
×
636
        return nil
×
637
}
638

639
func (r *CDIConfigReconciler) createProxyConfigMap(cmName, cert string) *v1.ConfigMap {
1✔
640
        return &v1.ConfigMap{
1✔
641
                ObjectMeta: metav1.ObjectMeta{
1✔
642
                        Name:      cmName,
1✔
643
                        Namespace: r.cdiNamespace},
1✔
644
                Data: map[string]string{common.ImportProxyConfigMapKey: cert},
1✔
645
        }
1✔
646
}
1✔
647

648
// Init initializes a CDIConfig object.
649
func (r *CDIConfigReconciler) Init() error {
×
650
        _, err := r.createCDIConfig()
×
651
        return err
×
652
}
×
653

654
// NewConfigController creates a new instance of the config controller.
655
func NewConfigController(mgr manager.Manager, log logr.Logger, uploadProxyServiceName, configName string, installerLabels map[string]string) (controller.Controller, error) {
×
656
        uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
×
657
                Scheme: mgr.GetScheme(),
×
658
                Mapper: mgr.GetRESTMapper(),
×
659
        })
×
660
        if err != nil {
×
661
                return nil, err
×
662
        }
×
663
        reconciler := &CDIConfigReconciler{
×
664
                client:                 mgr.GetClient(),
×
665
                uncachedClient:         uncachedClient,
×
666
                recorder:               mgr.GetEventRecorderFor("config-controller"),
×
667
                scheme:                 mgr.GetScheme(),
×
668
                log:                    log.WithName("config-controller"),
×
669
                uploadProxyServiceName: uploadProxyServiceName,
×
670
                configName:             configName,
×
671
                cdiNamespace:           util.GetNamespace(),
×
672
                installerLabels:        installerLabels,
×
673
        }
×
674

×
675
        configController, err := controller.New("config-controller", mgr, controller.Options{
×
676
                MaxConcurrentReconciles: 3,
×
677
                Reconciler:              reconciler,
×
678
        })
×
679
        if err != nil {
×
680
                return nil, err
×
681
        }
×
682
        if err := addConfigControllerWatches(mgr, configController, reconciler.cdiNamespace, configName, uploadProxyServiceName, log); err != nil {
×
683
                return nil, err
×
684
        }
×
685
        if err := reconciler.Init(); err != nil {
×
686
                log.Error(err, "Unable to initialize CDIConfig")
×
687
        }
×
688
        log.Info("Initialized CDI Config object")
×
689
        return configController, nil
×
690
}
691

692
// addConfigControllerWatches sets up the watches used by the config controller.
693
func addConfigControllerWatches(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string, log logr.Logger) error {
×
694
        // Setup watches
×
695
        if err := watchCDIConfig(mgr, configController, configName); err != nil {
×
696
                return err
×
697
        }
×
698
        if err := watchStorageClass(mgr, configController, configName); err != nil {
×
699
                return err
×
700
        }
×
701
        if err := watchIngress(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
×
702
                return err
×
703
        }
×
704
        if err := watchRoutes(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
×
705
                return err
×
706
        }
×
707
        if err := watchClusterProxy(mgr, configController, configName); err != nil {
×
708
                return err
×
709
        }
×
710
        if err := watchUploadProxyCA(mgr, configController, configName); err != nil {
×
711
                return err
×
712
        }
×
713

714
        return nil
×
715
}
716

717
func watchCDIConfig(mgr manager.Manager, configController controller.Controller, configName string) error {
×
NEW
718
        if err := configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDIConfig{}, &handler.TypedEnqueueRequestForObject[*cdiv1.CDIConfig]{})); err != nil {
×
719
                return err
×
720
        }
×
NEW
721
        return configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDI{}, handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.CDI](
×
NEW
722
                func(_ context.Context, _ *cdiv1.CDI) []reconcile.Request {
×
723
                        return []reconcile.Request{{
×
724
                                NamespacedName: types.NamespacedName{Name: configName},
×
725
                        }}
×
726
                },
×
727
        )))
728
}
729

730
func watchStorageClass(mgr manager.Manager, configController controller.Controller, configName string) error {
×
NEW
731
        return configController.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{}, handler.TypedEnqueueRequestsFromMapFunc[*storagev1.StorageClass](
×
NEW
732
                func(_ context.Context, _ *storagev1.StorageClass) []reconcile.Request {
×
733
                        return []reconcile.Request{{
×
734
                                NamespacedName: types.NamespacedName{Name: configName},
×
735
                        }}
×
736
                },
×
737
        )))
738
}
739

740
func watchIngress(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
×
NEW
741
        err := configController.Watch(source.Kind(mgr.GetCache(), &networkingv1.Ingress{}, handler.TypedEnqueueRequestsFromMapFunc[*networkingv1.Ingress](
×
NEW
742
                func(_ context.Context, _ *networkingv1.Ingress) []reconcile.Request {
×
743
                        return []reconcile.Request{{
×
744
                                NamespacedName: types.NamespacedName{Name: configName},
×
745
                        }}
×
746
                }),
×
747
                predicate.TypedFuncs[*networkingv1.Ingress]{
NEW
748
                        CreateFunc: func(e event.TypedCreateEvent[*networkingv1.Ingress]) bool {
×
NEW
749
                                return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
×
NEW
750
                                        e.Object.GetNamespace() == cdiNamespace
×
751
                        },
×
NEW
752
                        UpdateFunc: func(e event.TypedUpdateEvent[*networkingv1.Ingress]) bool {
×
NEW
753
                                return "" != getURLFromIngress(e.ObjectNew, uploadProxyServiceName) &&
×
NEW
754
                                        e.ObjectNew.GetNamespace() == cdiNamespace
×
755
                        },
×
NEW
756
                        DeleteFunc: func(e event.TypedDeleteEvent[*networkingv1.Ingress]) bool {
×
NEW
757
                                return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
×
NEW
758
                                        e.Object.GetNamespace() == cdiNamespace
×
UNCOV
759
                        },
×
760
                }))
761
        return err
×
762
}
763

764
// we only watch the route obj if they exist, i.e., if it is an OpenShift cluster
765
func watchRoutes(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
×
766
        err := mgr.GetClient().List(context.TODO(), &routev1.RouteList{}, &client.ListOptions{Namespace: cdiNamespace})
×
767
        if !meta.IsNoMatchError(err) {
×
768
                if err == nil || cc.IsErrCacheNotStarted(err) {
×
NEW
769
                        err := configController.Watch(source.Kind(mgr.GetCache(), &routev1.Route{}, handler.TypedEnqueueRequestsFromMapFunc[*routev1.Route](
×
NEW
770
                                func(_ context.Context, _ *routev1.Route) []reconcile.Request {
×
771
                                        return []reconcile.Request{{
×
772
                                                NamespacedName: types.NamespacedName{Name: configName},
×
773
                                        }}
×
774
                                }),
×
775
                                predicate.TypedFuncs[*routev1.Route]{
NEW
776
                                        CreateFunc: func(e event.TypedCreateEvent[*routev1.Route]) bool {
×
NEW
777
                                                return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
×
NEW
778
                                                        e.Object.GetNamespace() == cdiNamespace
×
779
                                        },
×
NEW
780
                                        UpdateFunc: func(e event.TypedUpdateEvent[*routev1.Route]) bool {
×
NEW
781
                                                return "" != getURLFromRoute(e.ObjectNew, uploadProxyServiceName) &&
×
NEW
782
                                                        e.ObjectNew.GetNamespace() == cdiNamespace
×
783
                                        },
×
NEW
784
                                        DeleteFunc: func(e event.TypedDeleteEvent[*routev1.Route]) bool {
×
NEW
785
                                                return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
×
NEW
786
                                                        e.Object.GetNamespace() == cdiNamespace
×
UNCOV
787
                                        },
×
788
                                }))
789
                        return err
×
790
                }
791
                return err
×
792
        }
793
        return nil
×
794
}
795

796
// we only watch the cluster-wide proxy obj if they exist, i.e., if it is an OpenShift cluster
797
func watchClusterProxy(mgr manager.Manager, configController controller.Controller, configName string) error {
×
798
        err := mgr.GetClient().List(context.TODO(), &ocpconfigv1.ProxyList{})
×
799
        if !meta.IsNoMatchError(err) {
×
800
                if err == nil || cc.IsErrCacheNotStarted(err) {
×
NEW
801
                        return configController.Watch(source.Kind(mgr.GetCache(), &ocpconfigv1.Proxy{}, handler.TypedEnqueueRequestsFromMapFunc[*ocpconfigv1.Proxy](
×
NEW
802
                                func(_ context.Context, _ *ocpconfigv1.Proxy) []reconcile.Request {
×
803
                                        return []reconcile.Request{{
×
804
                                                NamespacedName: types.NamespacedName{Name: configName},
×
805
                                        }}
×
806
                                },
×
807
                        )))
808
                }
809
                return err
×
810
        }
811
        return nil
×
812
}
813

814
// watchUploadProxyCA watches the kube-root-ca.crt ConfigMap for changes
815
// to the CA certificate used by the upload proxy.
816
//
817
// A change in the UploadProxyURL may invalidate the CA certificate, but
818
// watchCDIConfig will handle that.
819
func watchUploadProxyCA(mgr manager.Manager, configcontroller controller.Controller, configName string) error {
×
NEW
820
        handler := handler.TypedEnqueueRequestsFromMapFunc[*v1.ConfigMap](func(context.Context, *v1.ConfigMap) []reconcile.Request {
×
821
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: configName}}}
×
822
        })
×
823

NEW
824
        predicate := predicate.NewTypedPredicateFuncs[*v1.ConfigMap](func(o *v1.ConfigMap) bool {
×
NEW
825
                return o.Name == rootCertificateConfigMap
×
UNCOV
826
        })
×
827

NEW
828
        if err := configcontroller.Watch(source.Kind(mgr.GetCache(), &v1.ConfigMap{}, handler, predicate)); err != nil {
×
829
                return fmt.Errorf("could not watch UploadProxyCA ConfigMap: %w", err)
×
830
        }
×
831
        return nil
×
832
}
833

834
func getURLFromIngress(ing *networkingv1.Ingress, uploadProxyServiceName string) string {
1✔
835
        if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil {
2✔
836
                if ing.Spec.DefaultBackend.Service.Name != uploadProxyServiceName {
2✔
837
                        return ""
1✔
838
                }
1✔
839
                return ing.Spec.Rules[0].Host
1✔
840
        }
841
        for _, rule := range ing.Spec.Rules {
2✔
842
                if rule.HTTP == nil {
2✔
843
                        continue
1✔
844
                }
845
                for _, path := range rule.HTTP.Paths {
2✔
846
                        if path.Backend.Service != nil && path.Backend.Service.Name == uploadProxyServiceName {
2✔
847
                                if rule.Host != "" {
2✔
848
                                        return rule.Host
1✔
849
                                }
1✔
850
                        }
851
                }
852
        }
853
        return ""
1✔
854
}
855

856
func getURLFromRoute(route *routev1.Route, uploadProxyServiceName string) string {
1✔
857
        if route.Spec.To.Name == uploadProxyServiceName {
2✔
858
                if len(route.Status.Ingress) > 0 {
2✔
859
                        return route.Status.Ingress[0].Host
1✔
860
                }
1✔
861
        }
862
        return ""
1✔
863
}
864

865
// getClusterWideProxy returns the OpenShift cluster wide proxy object
866
func getClusterWideProxy(r client.Client) (*ocpconfigv1.Proxy, error) {
1✔
867
        clusterWideProxy := &ocpconfigv1.Proxy{}
1✔
868
        // Ignore both no CRD found (IgnoreIsNoMatch) and the object itself not existing IsNotFound because we want to skip if not
1✔
869
        // in Open Shift.
1✔
870
        if err := r.Get(context.TODO(), types.NamespacedName{Name: ClusterWideProxyName}, clusterWideProxy); cc.IgnoreIsNoMatchError(err) != nil && !k8serrors.IsNotFound(err) {
1✔
871
                return nil, err
×
872
        }
×
873
        return clusterWideProxy, nil
1✔
874
}
875

876
// GetImportProxyConfig attempts to import proxy URLs if configured in the CDIConfig.
877
func GetImportProxyConfig(config *cdiv1.CDIConfig, field string) (string, error) {
1✔
878
        if config == nil {
1✔
879
                return "", errors.New("failed to get field, the CDIConfig is nil")
×
880
        }
×
881
        if config.Status.ImportProxy == nil {
2✔
882
                return "", errors.New("failed to get field, the CDIConfig ImportProxy is nil")
1✔
883
        }
1✔
884

885
        switch field {
1✔
886
        case common.ImportProxyHTTP:
1✔
887
                if config.Status.ImportProxy.HTTPProxy != nil {
2✔
888
                        return *config.Status.ImportProxy.HTTPProxy, nil
1✔
889
                }
1✔
890
        case common.ImportProxyHTTPS:
1✔
891
                if config.Status.ImportProxy.HTTPSProxy != nil {
2✔
892
                        return *config.Status.ImportProxy.HTTPSProxy, nil
1✔
893
                }
1✔
894
        case common.ImportProxyNoProxy:
1✔
895
                if config.Status.ImportProxy.NoProxy != nil {
2✔
896
                        return *config.Status.ImportProxy.NoProxy, nil
1✔
897
                }
1✔
898
        case common.ImportProxyConfigMapName:
1✔
899
                if config.Status.ImportProxy.TrustedCAProxy != nil {
2✔
900
                        return *config.Status.ImportProxy.TrustedCAProxy, nil
1✔
901
                }
1✔
902
        default:
1✔
903
                return "", errors.Errorf("CDIConfig ImportProxy does not have the field: %s", field)
1✔
904
        }
905

906
        // If everything fails, return blank
907
        return "", nil
×
908
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc