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

openshift-kni / cluster-group-upgrades-operator / 6e522fdb177f6d512bdef6390d8450964150d583

24 Nov 2025 07:10PM UTC coverage: 5.68% (-23.1%) from 28.767%
6e522fdb177f6d512bdef6390d8450964150d583

push

web-flow
Replace oc wait with sleep (#5164)

Signed-off-by: Saeid Askari <saskari@redhat.com>

372 of 6549 relevant lines covered (5.68%)

0.06 hits per line

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

0.0
/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 {
×
31

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

48
                precachingCondition := meta.FindStatusCondition(
×
49
                        clusterGroupUpgrade.Status.Conditions, string(utils.ConditionTypes.PrecachingSuceeded))
×
50
                r.Log.Info("[reconcilePrecaching]",
×
51
                        "FindStatusCondition", precachingCondition)
×
52
                if precachingCondition != nil && precachingCondition.Status == metav1.ConditionTrue {
×
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)
×
58
        }
59
        // No precaching required
60
        return nil
×
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) {
×
71
        updateGraphURL := upstream + "?channel=" + channel
×
72

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

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

90
        defer res.Body.Close()
×
91

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

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

103
        if nodes, ok := graph["nodes"]; ok {
×
104
                for _, n := range nodes.([]interface{}) {
×
105
                        node := n.(map[string]interface{})
×
106
                        if node["version"] == version && node["payload"] != "" {
×
107
                                return node["payload"].(string), nil
×
108
                        }
×
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) {
×
126

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

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

×
161
        return spec, nil
×
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) {
×
171

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

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

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

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

×
192
                var objTemplates interface{}
×
193

×
194
                switch {
×
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:
×
200
                        objTemplates = plcTmplDefSpec[utils.ObjectTemplates]
×
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{}) {
×
214
                        complianceType := objTemplate.(map[string]interface{})["complianceType"]
×
215
                        if complianceType == "mustnothave" {
×
216
                                continue
×
217
                        }
218
                        spec := objTemplate.(map[string]interface{})["objectDefinition"]
×
219
                        if spec == nil {
×
220
                                return nil, fmt.Errorf("[stripPolicy] can't find any objectDefinition")
×
221
                        }
×
222
                        objects = append(objects, spec.(map[string]interface{}))
×
223
                }
224
        }
225
        return objects, nil
×
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) {
×
236

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

×
243
        err := r.createResourcesFromTemplates(ctx, spec, precacheDependenciesCreateTemplates)
×
244
        if err != nil {
×
245
                return false, err
×
246
        }
×
247
        spec.ViewUpdateIntervalSec = utils.GetMCVUpdateInterval(len(clusterGroupUpgrade.Status.Precaching.Status))
×
248
        err = r.createResourcesFromTemplates(ctx, spec, precacheDependenciesViewTemplates)
×
249
        if err != nil {
×
250
                return false, err
×
251
        }
×
252
        return true, nil
×
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) {
×
263

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

270
        preCacheImage := preCachingConfigSpec.Overrides.PreCacheImage
×
271
        if preCacheImage == "" {
×
272
                overrides, err := r.getOverrides(ctx, clusterGroupUpgrade)
×
273
                if err != nil {
×
274
                        r.Log.Error(err, "getOverrides failed ")
×
275
                        return "", err
×
276
                }
×
277
                preCacheImage = overrides["precache.image"]
×
278
                if preCacheImage == "" {
×
279
                        preCacheImage = os.Getenv("PRECACHE_IMG")
×
280
                        r.Log.Info("[getPrecacheImagePullSpec]", "workload image", preCacheImage)
×
281
                        if preCacheImage == "" {
×
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
×
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) {
×
296
        var result int64
×
297
        var err error
×
298
        // Check if the spaceRequired is specified in base-2 (KiB, MiB, GiB, TiB, PiB) or base-10 (KB, MB, GB, TB, PB)
×
299
        if strings.Contains(spaceRequired, "i") {
×
300
                result, err = units.RAMInBytes(spaceRequired)
×
301
        } else {
×
302
                result, err = units.FromHumanSize(spaceRequired)
×
303
        }
×
304

305
        // Verify that no parsing errors occurred
306
        if err != nil {
×
307
                return "", err
×
308
        }
×
309
        if result < 0 {
×
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)))
×
315
        return strconv.Itoa(resultGiB), nil
×
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 {
×
324

×
325
        rv := new(templateData)
×
326
        spec := clusterGroupUpgrade.Status.Precaching.Spec
×
327
        rv.PlatformImage = spec.PlatformImage
×
328
        rv.Operators.Indexes = spec.OperatorsIndexes
×
329
        rv.Operators.PackagesAndChannels = spec.OperatorsPackagesAndChannels
×
330
        rv.ExcludePrecachePatterns = spec.ExcludePrecachePatterns
×
331
        rv.AdditionalImages = spec.AdditionalImages
×
332
        rv.SpaceRequired = spec.SpaceRequired
×
333
        return rv
×
334
}
×
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) {
×
342

×
343
        rv := new(ranv1alpha1.PrecachingSpec)
×
344

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

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

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

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

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

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

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

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

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

×
424
        return *rv, nil
×
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) {
×
431

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