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

openshift-kni / cluster-group-upgrades-operator / #3

20 Nov 2025 11:55AM UTC coverage: 61.674% (+17.2%) from 44.472%
#3

push

web-flow
chore(deps): update topology-aware-lifecycle-manager-precache-4-21 to e280b86 (#5031)

Image created from 'https://github.com/openshift-kni/cluster-group-upgrades-operator?rev=e0953ccb4'

Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com>
Co-authored-by: red-hat-konflux[bot] <126015336+red-hat-konflux[bot]@users.noreply.github.com>

4039 of 6549 relevant lines covered (61.67%)

0.69 hits per line

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

76.62
/controllers/precache.go
1
package controllers
2

3
import (
4
        "context"
5
        "crypto/tls"
6
        "encoding/json"
7
        "fmt"
8
        "io"
9
        "math"
10
        "net/http"
11
        "os"
12
        "strconv"
13

14
        "strings"
15

16
        "github.com/docker/go-units"
17

18
        "github.com/openshift-kni/cluster-group-upgrades-operator/controllers/utils"
19
        ranv1alpha1 "github.com/openshift-kni/cluster-group-upgrades-operator/pkg/api/clustergroupupgrades/v1alpha1"
20

21
        "k8s.io/apimachinery/pkg/api/meta"
22
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23
        "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
24
)
25

26
// reconcilePrecaching provides the main precaching entry point
27
// returns:                         error
28
func (r *ClusterGroupUpgradeReconciler) reconcilePrecaching(
29
        ctx context.Context,
30
        clusterGroupUpgrade *ranv1alpha1.ClusterGroupUpgrade, clusters []string, policies []*unstructured.Unstructured) error {
1✔
31

1✔
32
        if clusterGroupUpgrade.Spec.PreCaching && len(clusters) > 0 {
2✔
33
                // Pre-caching is required
1✔
34
                if clusterGroupUpgrade.Status.Precaching == nil {
2✔
35
                        clusterGroupUpgrade.Status.Precaching = &ranv1alpha1.PrecachingStatus{
1✔
36
                                Spec: &ranv1alpha1.PrecachingSpec{
1✔
37
                                        PlatformImage:                "",
1✔
38
                                        OperatorsIndexes:             []string{},
1✔
39
                                        OperatorsPackagesAndChannels: []string{},
1✔
40
                                },
1✔
41
                                Status:   make(map[string]string),
1✔
42
                                Clusters: []string{},
1✔
43
                        }
1✔
44
                } else if clusterGroupUpgrade.Status.Precaching.Status == nil {
3✔
45
                        clusterGroupUpgrade.Status.Precaching.Status = make(map[string]string)
1✔
46
                }
1✔
47

48
                precachingCondition := meta.FindStatusCondition(
1✔
49
                        clusterGroupUpgrade.Status.Conditions, string(utils.ConditionTypes.PrecachingSuceeded))
1✔
50
                r.Log.Info("[reconcilePrecaching]",
1✔
51
                        "FindStatusCondition", precachingCondition)
1✔
52
                if precachingCondition != nil && precachingCondition.Status == metav1.ConditionTrue {
1✔
53
                        // Precaching is done
×
54
                        return nil
×
55
                }
×
56
                // Precaching is required and not marked as done
57
                return r.precachingFsm(ctx, clusterGroupUpgrade, clusters, policies)
1✔
58
        }
59
        // No precaching required
60
        return nil
1✔
61
}
62

63
// getImageForVersionFromUpdateGraph gets the image for the given version
64
// by traversing the update graph.
65
// Connecting to the upstream URL with the channel passed as a parameter
66
// the update graph is returned as JSON. This function then traverses
67
// the nodes list from that JSON to find the version and if found
68
// then returns the image
69
func (r *ClusterGroupUpgradeReconciler) getImageForVersionFromUpdateGraph(
70
        upstream string, channel string, version string) (string, error) {
1✔
71
        updateGraphURL := upstream + "?channel=" + channel
1✔
72

1✔
73
        insecureSkipVerify := os.Getenv("INSECURE_GRAPH_CALL") == "true"
1✔
74
        tr := &http.Transport{
1✔
75
                TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
1✔
76
                Proxy:           http.ProxyFromEnvironment,
1✔
77
        }
1✔
78
        client := &http.Client{Transport: tr}
1✔
79
        req, _ := http.NewRequest("GET", updateGraphURL, nil)
1✔
80
        req.Header.Add("Accept", "application/json")
1✔
81
        res, err := client.Do(req)
1✔
82

1✔
83
        if err != nil {
1✔
84
                return "", fmt.Errorf("unable to request update graph on url %s: %w", updateGraphURL, err)
×
85
        }
×
86
        if res.StatusCode != http.StatusOK {
1✔
87
                return "", fmt.Errorf("error response from update graph url %s: %d", updateGraphURL, res.StatusCode)
×
88
        }
×
89

90
        defer res.Body.Close()
1✔
91

1✔
92
        body, err := io.ReadAll(res.Body)
1✔
93
        if err != nil && len(body) > 0 {
1✔
94
                return "", fmt.Errorf("unable to read body from response: %w", err)
×
95
        }
×
96

97
        var graph map[string]interface{}
1✔
98
        err = json.Unmarshal(body, &graph)
1✔
99
        if err != nil {
1✔
100
                return "", fmt.Errorf("unable to unmarshal body: %w", err)
×
101
        }
×
102

103
        if nodes, ok := graph["nodes"]; ok {
2✔
104
                for _, n := range nodes.([]interface{}) {
2✔
105
                        node := n.(map[string]interface{})
1✔
106
                        if node["version"] == version && node["payload"] != "" {
2✔
107
                                return node["payload"].(string), nil
1✔
108
                        }
1✔
109
                }
110
        }
111
        return "", fmt.Errorf("unable to find version %s on update graph on url %s", version, updateGraphURL)
×
112
}
113

114
// extractPrecachingSpecFromPolicies extracts the software spec to be pre-cached
115
//
116
//                        from policies.
117
//                        There are three object types to look at in the policies:
118
//             - ClusterVersion: release image must be specified to be pre-cached
119
//             - Subscription: provides the list of operator packages and channels
120
//             - CatalogSource: must be explicitly configured to be precached.
121
//               All the clusters in the CGU must have same catalog source(s)
122
//
123
// returns: precachingSpec, error
124
func (r *ClusterGroupUpgradeReconciler) extractPrecachingSpecFromPolicies(
125
        policies []*unstructured.Unstructured) (ranv1alpha1.PrecachingSpec, error) {
1✔
126

1✔
127
        var spec ranv1alpha1.PrecachingSpec
1✔
128
        for _, policy := range policies {
2✔
129
                objects, err := stripPolicy(policy.Object)
1✔
130
                if err != nil {
1✔
131
                        return spec, err
×
132
                }
×
133
                for _, object := range objects {
2✔
134
                        kind := object["kind"]
1✔
135
                        switch kind {
1✔
136
                        case utils.SubscriptionGroupVersionKind().Kind:
1✔
137
                                packChan := fmt.Sprintf("%s:%s", object["spec"].(map[string]interface{})["name"],
1✔
138
                                        object["spec"].(map[string]interface{})["channel"])
1✔
139
                                spec.OperatorsPackagesAndChannels = append(spec.OperatorsPackagesAndChannels, packChan)
1✔
140
                                r.Log.Info("[extractPrecachingSpecFromPolicies]", "Operator package:channel", packChan)
1✔
141
                                continue
1✔
142
                        case utils.PolicyTypeCatalogSource:
1✔
143
                                index := fmt.Sprintf("%s", object["spec"].(map[string]interface{})["image"])
1✔
144
                                spec.OperatorsIndexes = append(spec.OperatorsIndexes, index)
1✔
145
                                r.Log.Info("[extractPrecachingSpecFromPolicies]", "CatalogSource", index)
1✔
146
                                continue
1✔
147
                        default:
1✔
148
                                continue
1✔
149
                        }
150
                }
151
        }
152

153
        // Get the platform image spec from the policies
154
        image, err := r.extractOCPImageFromPolicies(policies)
1✔
155
        if err != nil {
1✔
156
                return ranv1alpha1.PrecachingSpec{}, err
×
157
        }
×
158
        spec.PlatformImage = image
1✔
159
        r.Log.Info("[extractPrecachingSpecFromPolicies]", "ClusterVersion image", spec.PlatformImage)
1✔
160

1✔
161
        return spec, nil
1✔
162
}
163

164
// stripPolicy strips policy information and returns the underlying objects
165
// filters objects with mustnothave compliance type
166
// returns: []interface{} - list of the underlying objects in the policy
167
//
168
//        error
169
func stripPolicy(
170
        policyObject map[string]interface{}) ([]map[string]interface{}, error) {
1✔
171

1✔
172
        var objects []map[string]interface{}
1✔
173
        policyTemplates, exists, err := unstructured.NestedFieldCopy(
1✔
174
                policyObject, "spec", "policy-templates")
1✔
175
        if err != nil {
1✔
176
                return nil, err
×
177
        }
×
178
        if !exists {
1✔
179
                return nil, fmt.Errorf("[stripPolicy] spec -> policy-templates not found")
×
180
        }
×
181

182
        for _, policyTemplate := range policyTemplates.([]interface{}) {
2✔
183

1✔
184
                plcTmplDefSpec := policyTemplate.(map[string]interface {
1✔
185
                })["objectDefinition"].(map[string]interface {
1✔
186
                })["spec"].(map[string]interface{})
1✔
187

1✔
188
                // One and only one of [object-templates, object-templates-raw] should be defined
1✔
189
                objectTemplatePresent := plcTmplDefSpec[utils.ObjectTemplates] != nil
1✔
190
                objectTemplateRawPresent := plcTmplDefSpec[utils.ObjectTemplatesRaw] != nil
1✔
191

1✔
192
                var objTemplates interface{}
1✔
193

1✔
194
                switch {
1✔
195
                case objectTemplatePresent && objectTemplateRawPresent:
×
196
                        return nil, fmt.Errorf("[stripPolicy] found both %s and %s in policyTemplate", utils.ObjectTemplates, utils.ObjectTemplatesRaw)
×
197
                case !objectTemplatePresent && !objectTemplateRawPresent:
×
198
                        return nil, fmt.Errorf("[stripPolicy] can't find %s or %s in policyTemplate", utils.ObjectTemplates, utils.ObjectTemplatesRaw)
×
199
                case objectTemplatePresent:
1✔
200
                        objTemplates = plcTmplDefSpec[utils.ObjectTemplates]
1✔
201
                case objectTemplateRawPresent:
×
202
                        stringTemplate := utils.StripObjectTemplatesRaw(plcTmplDefSpec[utils.ObjectTemplatesRaw].(string))
×
203

×
204
                        var err error
×
205
                        objTemplates, err = utils.StringToYaml(stringTemplate)
×
206
                        if err != nil {
×
207
                                return nil, fmt.Errorf("%s", utils.ConfigPlcFailRawMarshal)
×
208
                        }
×
209
                default:
×
210
                        return nil, fmt.Errorf("[stripPolicy] can't find %s or %s in policyTemplate", utils.ObjectTemplates, utils.ObjectTemplatesRaw)
×
211
                }
212

213
                for _, objTemplate := range objTemplates.([]interface{}) {
2✔
214
                        complianceType := objTemplate.(map[string]interface{})["complianceType"]
1✔
215
                        if complianceType == "mustnothave" {
1✔
216
                                continue
×
217
                        }
218
                        spec := objTemplate.(map[string]interface{})["objectDefinition"]
1✔
219
                        if spec == nil {
1✔
220
                                return nil, fmt.Errorf("[stripPolicy] can't find any objectDefinition")
×
221
                        }
×
222
                        objects = append(objects, spec.(map[string]interface{}))
1✔
223
                }
224
        }
225
        return objects, nil
1✔
226
}
227

228
// deployDependencies deploys precaching workload dependencies
229
// returns: ok (bool)
230
//
231
//        error
232
func (r *ClusterGroupUpgradeReconciler) deployDependencies(
233
        ctx context.Context,
234
        clusterGroupUpgrade *ranv1alpha1.ClusterGroupUpgrade,
235
        cluster string) (bool, error) {
1✔
236

1✔
237
        spec := r.getPrecacheSpecTemplateData(clusterGroupUpgrade)
1✔
238
        spec.Cluster = cluster
1✔
239
        msg := fmt.Sprintf("%v", spec)
1✔
240
        r.Log.Info("[deployDependencies]", "getPrecacheSpecTemplateData",
1✔
241
                cluster, "status", "success", "content", msg)
1✔
242

1✔
243
        err := r.createResourcesFromTemplates(ctx, spec, precacheDependenciesCreateTemplates)
1✔
244
        if err != nil {
1✔
245
                return false, err
×
246
        }
×
247
        spec.ViewUpdateIntervalSec = utils.GetMCVUpdateInterval(len(clusterGroupUpgrade.Status.Precaching.Status))
1✔
248
        err = r.createResourcesFromTemplates(ctx, spec, precacheDependenciesViewTemplates)
1✔
249
        if err != nil {
1✔
250
                return false, err
×
251
        }
×
252
        return true, nil
1✔
253
}
254

255
// getPrecacheImagePullSpec gets the precaching workload image pull spec.
256
// returns: image - pull spec string
257
//
258
//        error
259
func (r *ClusterGroupUpgradeReconciler) getPrecacheImagePullSpec(
260
        ctx context.Context,
261
        clusterGroupUpgrade *ranv1alpha1.ClusterGroupUpgrade) (
262
        string, error) {
1✔
263

1✔
264
        preCachingConfigSpec, err := r.getPreCachingConfigSpec(ctx, clusterGroupUpgrade)
1✔
265
        if err != nil {
1✔
266
                r.Log.Error(err, "getPreCachingConfigSpec failed ")
×
267
                return "", err
×
268
        }
×
269

270
        preCacheImage := preCachingConfigSpec.Overrides.PreCacheImage
1✔
271
        if preCacheImage == "" {
2✔
272
                overrides, err := r.getOverrides(ctx, clusterGroupUpgrade)
1✔
273
                if err != nil {
1✔
274
                        r.Log.Error(err, "getOverrides failed ")
×
275
                        return "", err
×
276
                }
×
277
                preCacheImage = overrides["precache.image"]
1✔
278
                if preCacheImage == "" {
2✔
279
                        preCacheImage = os.Getenv("PRECACHE_IMG")
1✔
280
                        r.Log.Info("[getPrecacheImagePullSpec]", "workload image", preCacheImage)
1✔
281
                        if preCacheImage == "" {
1✔
282
                                return "", fmt.Errorf(
×
283
                                        "can't find pre-caching image pull spec in environment or overrides")
×
284
                        }
×
285
                } else {
×
286
                        r.Log.Info(getDeprecationMessage("precache.image"))
×
287
                }
×
288
        }
289
        return preCacheImage, nil
1✔
290
}
291

292
// parseSpaceRequired parses the spaceRequired string value (can be a floating-point value) and converts it to
293
// an integer string value representing the space required on the managed cluster in Gibibytes.
294
// Note that the parsed value is rounded up to the nearest integer via the math.Ceil function.
295
func parseSpaceRequired(spaceRequired string) (string, error) {
1✔
296
        var result int64
1✔
297
        var err error
1✔
298
        // Check if the spaceRequired is specified in base-2 (KiB, MiB, GiB, TiB, PiB) or base-10 (KB, MB, GB, TB, PB)
1✔
299
        if strings.Contains(spaceRequired, "i") {
2✔
300
                result, err = units.RAMInBytes(spaceRequired)
1✔
301
        } else {
1✔
302
                result, err = units.FromHumanSize(spaceRequired)
×
303
        }
×
304

305
        // Verify that no parsing errors occurred
306
        if err != nil {
1✔
307
                return "", err
×
308
        }
×
309
        if result < 0 {
1✔
310
                return "", fmt.Errorf("invalid value for spaceRequired, must be a number greater than 0")
×
311
        }
×
312

313
        // Convert to base-2 format in Gibibytes (result is rounded-up to the next integer value)
314
        resultGiB := int(math.Ceil(float64(result) / math.Pow(1024, 3)))
1✔
315
        return strconv.Itoa(resultGiB), nil
1✔
316
}
317

318
// getPrecacheSpecTemplateData: Converts precaching payload spec to template data
319
// returns: precacheTemplateData (softwareSpec)
320
//
321
//        error
322
func (r *ClusterGroupUpgradeReconciler) getPrecacheSpecTemplateData(
323
        clusterGroupUpgrade *ranv1alpha1.ClusterGroupUpgrade) *templateData {
1✔
324

1✔
325
        rv := new(templateData)
1✔
326
        spec := clusterGroupUpgrade.Status.Precaching.Spec
1✔
327
        rv.PlatformImage = spec.PlatformImage
1✔
328
        rv.Operators.Indexes = spec.OperatorsIndexes
1✔
329
        rv.Operators.PackagesAndChannels = spec.OperatorsPackagesAndChannels
1✔
330
        rv.ExcludePrecachePatterns = spec.ExcludePrecachePatterns
1✔
331
        rv.AdditionalImages = spec.AdditionalImages
1✔
332
        rv.SpaceRequired = spec.SpaceRequired
1✔
333
        return rv
1✔
334
}
1✔
335

336
// includePreCachingConfigs retrieves the PreCachingConfigCR associated to the CGU
337
// returns: *ranv1alpha1.PrecachingSpec, error
338
func (r *ClusterGroupUpgradeReconciler) includePreCachingConfigs(
339
        ctx context.Context,
340
        clusterGroupUpgrade *ranv1alpha1.ClusterGroupUpgrade, spec *ranv1alpha1.PrecachingSpec) (
341
        ranv1alpha1.PrecachingSpec, error) {
1✔
342

1✔
343
        rv := new(ranv1alpha1.PrecachingSpec)
1✔
344

1✔
345
        preCachingConfigSpec, err := r.getPreCachingConfigSpec(ctx, clusterGroupUpgrade)
1✔
346
        if err != nil {
2✔
347
                return *rv, err
1✔
348
        }
1✔
349

350
        // Support specifying overrides via ConfigMap (if PreCachingConfig fields are not specified)
351
        overrides, err := r.getOverrides(ctx, clusterGroupUpgrade)
1✔
352
        if err != nil {
1✔
353
                return *rv, err
×
354
        }
×
355

356
        // Check the OpenShift platform image
357
        platformImage := preCachingConfigSpec.Overrides.PlatformImage
1✔
358
        if platformImage == "" {
2✔
359
                overrideField := "platform.image"
1✔
360
                platformImage = overrides[overrideField]
1✔
361
                if platformImage == "" {
2✔
362
                        platformImage = spec.PlatformImage
1✔
363
                } else {
1✔
364
                        r.Log.Info(getDeprecationMessage(overrideField))
×
365
                }
×
366
        }
367
        rv.PlatformImage = platformImage
1✔
368

1✔
369
        // Define re-usable function to extract pre-caching config from the following sources (in order of precedence):
1✔
370
        // 1) PreCachingConfig CR,
1✔
371
        // 2) cluster-group-upgrade-overrides ConfigMap (log deprecation message if this source is used),
1✔
372
        // 3) spec.<field> (value derived by TALM)
1✔
373
        extractConfig := func(preCachingConfigCRValue []string, overrideField string, talmDerivedValue []string) []string {
2✔
374
                if len(preCachingConfigCRValue) == 0 {
2✔
375
                        extractedOverrides := strings.Split(overrides[overrideField], "\n")
1✔
376
                        if overrides[overrideField] == "" {
2✔
377
                                return talmDerivedValue
1✔
378
                        }
1✔
379
                        r.Log.Info(getDeprecationMessage(overrideField))
1✔
380
                        // Remove empty strings as a consequence of strings.Split
1✔
381
                        var filteredResult []string
1✔
382
                        for _, value := range extractedOverrides {
2✔
383
                                if value != "" {
2✔
384
                                        filteredResult = append(filteredResult, strings.TrimSpace(value))
1✔
385
                                }
1✔
386
                        }
387
                        return filteredResult
1✔
388
                }
389
                return preCachingConfigCRValue
×
390
        }
391

392
        // Extract the operator indexes
393
        rv.OperatorsIndexes = extractConfig(preCachingConfigSpec.Overrides.OperatorsIndexes,
1✔
394
                "operators.indexes", spec.OperatorsIndexes)
1✔
395

1✔
396
        // Extract the operator packages and channels
1✔
397
        rv.OperatorsPackagesAndChannels = extractConfig(preCachingConfigSpec.Overrides.OperatorsPackagesAndChannels,
1✔
398
                "operators.packagesAndChannels", spec.OperatorsPackagesAndChannels)
1✔
399

1✔
400
        // Extract the pre-cache exclusion patterns
1✔
401
        rv.ExcludePrecachePatterns = extractConfig(preCachingConfigSpec.ExcludePrecachePatterns,
1✔
402
                "excludePrecachePatterns", []string{})
1✔
403

1✔
404
        // Retrieve additional user images
1✔
405
        rv.AdditionalImages = preCachingConfigSpec.AdditionalImages
1✔
406

1✔
407
        // Extract the space required for pre-caching
1✔
408
        spaceRequired := preCachingConfigSpec.SpaceRequired
1✔
409
        if spaceRequired == "" {
2✔
410
                overrideField := "precache.spaceRequired"
1✔
411
                spaceRequired = overrides[overrideField]
1✔
412
                if spaceRequired == "" {
2✔
413
                        spaceRequired = utils.SpaceRequiredForPrecache
1✔
414
                } else {
1✔
415
                        r.Log.Info(getDeprecationMessage(overrideField))
×
416
                }
×
417
        }
418
        spaceRequired, err = parseSpaceRequired(spaceRequired)
1✔
419
        if err != nil {
1✔
420
                return *rv, err
×
421
        }
×
422
        rv.SpaceRequired = spaceRequired
1✔
423

1✔
424
        return *rv, nil
1✔
425
}
426

427
// checkPreCacheSpecConsistency checks software spec can be precached
428
// returns: consistent (bool), message (string)
429
func (r *ClusterGroupUpgradeReconciler) checkPreCacheSpecConsistency(
430
        spec ranv1alpha1.PrecachingSpec) (consistent bool, message string) {
1✔
431

1✔
432
        var operatorsRequested, platformRequested bool = true, true
1✔
433
        if len(spec.OperatorsIndexes) == 0 {
1✔
434
                operatorsRequested = false
×
435
        }
×
436
        if spec.PlatformImage == "" {
2✔
437
                platformRequested = false
1✔
438
        }
1✔
439
        if operatorsRequested && len(spec.OperatorsPackagesAndChannels) == 0 {
2✔
440
                return false, "inconsistent precaching configuration: olm index provided, but no packages"
1✔
441
        }
1✔
442
        if !operatorsRequested && !platformRequested {
1✔
443
                return false, "inconsistent precaching configuration: no software spec provided"
×
444
        }
×
445
        return true, ""
1✔
446
}
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

© 2026 Coveralls, Inc