Skip to content

Commit b76922e

Browse files
jan-lawopenshift-merge-bot[bot]
authored andcommitted
Handle CSV approvals with unconventional package names
ref: https://issues.redhat.com/browse/ACM-20500 Remove the assumption that dependency CSV names always match the format of <package name>.v<version>. When approving multiple CSVs in an install plan, inspect the bundles for required package names before recursively gathering dependency CSVs for approval. Signed-off-by: Janelle Law <[email protected]>
1 parent f544d11 commit b76922e

File tree

3 files changed

+205
-39
lines changed

3 files changed

+205
-39
lines changed

controllers/operatorpolicy_controller.go

Lines changed: 75 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,49 +2247,36 @@ func getApprovedCSVs(
22472247
return nil
22482248
}
22492249

2250-
packageDependencies := getBundleDependencies(installPlan, subscriptionCSV, "")
2251-
2252-
approvedCSVs := sets.Set[string]{}
2253-
approvedCSVs.Insert(subscriptionCSV)
2254-
2255-
// Approve all CSVs that are dependencies of the subscription CSV.
2256-
for _, packageDependency := range packageDependencies.UnsortedList() {
2257-
for _, csvName := range installPlan.Spec.ClusterServiceVersionNames {
2258-
if strings.HasPrefix(csvName, packageDependency+".v") {
2259-
approvedCSVs.Insert(csvName)
2260-
}
2261-
}
2262-
}
2250+
approvedCSVs := getBundleDependencies(installPlan, subscriptionCSV)
22632251

22642252
return approvedCSVs
22652253
}
22662254

2267-
// getBundleDependencies recursively gets the dependencies from a target CSV or package name. If the package name is
2268-
// provided instead of the target CSV, then it assumes the CSVs are in the format of `<package>.v<version>`. The
2269-
// returned set is of package names and not the CSV (i.e. no version in it).
2255+
// getBundleDependencies reads the install plan for required package dependencies.
2256+
// The package names are mapped to their CSV identifiers before recursively evaluating the dependency chain.
2257+
// The returned set contains the target CSV and all of its dependency CSVs.
22702258
func getBundleDependencies(
2271-
installPlan *operatorv1alpha1.InstallPlan, targetCSV string, packageName string,
2259+
installPlan *operatorv1alpha1.InstallPlan, targetCSV string,
22722260
) sets.Set[string] {
2273-
packageDependencies := sets.Set[string]{}
2261+
approvedCSVs := sets.Set[string]{}
22742262

2275-
if targetCSV == "" && packageName == "" {
2276-
return packageDependencies
2263+
if targetCSV == "" {
2264+
return approvedCSVs
22772265
}
22782266

2279-
for i, bundle := range installPlan.Status.BundleLookups {
2280-
if targetCSV != "" && bundle.Identifier != targetCSV {
2281-
continue
2282-
}
2267+
approvedCSVs.Insert(targetCSV)
22832268

2284-
if packageName != "" && !strings.HasPrefix(bundle.Identifier, packageName+".v") {
2285-
continue
2286-
}
2269+
// stores the mapping of package names to unapproved CSV identifiers
2270+
packageToCSV := make(map[string]string)
2271+
// stores the required package names for each unapproved CSV
2272+
packageDependencies := make(map[string]sets.Set[string])
22872273

2274+
for i, bundle := range installPlan.Status.BundleLookups {
22882275
if bundle.Properties == "" {
2289-
break
2276+
continue
22902277
}
22912278

2292-
props := map[string]interface{}{}
2279+
props := map[string]any{}
22932280

22942281
err := json.Unmarshal([]byte(bundle.Properties), &props)
22952282
if err != nil {
@@ -2304,37 +2291,86 @@ func getBundleDependencies(
23042291
break
23052292
}
23062293

2307-
propsList, ok := props["properties"].([]interface{})
2294+
propsList, ok := props["properties"].([]any)
23082295
if !ok {
2309-
break
2296+
continue
23102297
}
23112298

23122299
for _, prop := range propsList {
2313-
propMap, ok := prop.(map[string]interface{})
2300+
propMap, ok := prop.(map[string]any)
2301+
if !ok {
2302+
continue
2303+
}
2304+
2305+
propType, ok := propMap["type"].(string)
23142306
if !ok {
23152307
continue
23162308
}
23172309

2318-
if propMap["type"] != "olm.package.required" {
2310+
if propType != "olm.package" && propType != "olm.package.required" {
23192311
continue
23202312
}
23212313

2322-
propValue, ok := propMap["value"].(map[string]interface{})
2314+
propValue, ok := propMap["value"].(map[string]any)
23232315
if !ok {
23242316
continue
23252317
}
23262318

2327-
depPackageName, ok := propValue["packageName"].(string)
2328-
if !ok || depPackageName == "" {
2319+
pkgName, ok := propValue["packageName"].(string)
2320+
if !ok || pkgName == "" {
23292321
continue
23302322
}
23312323

2332-
packageDependencies.Insert(depPackageName)
2333-
packageDependencies = packageDependencies.Union(getBundleDependencies(installPlan, "", depPackageName))
2324+
switch propType {
2325+
case "olm.package":
2326+
packageToCSV[pkgName] = bundle.Identifier
2327+
case "olm.package.required":
2328+
if packageDependencies[bundle.Identifier] == nil {
2329+
packageDependencies[bundle.Identifier] = sets.Set[string]{}
2330+
}
2331+
2332+
packageDependencies[bundle.Identifier].Insert(pkgName)
2333+
}
2334+
}
2335+
}
2336+
2337+
approvedCSVs = approvedCSVs.Union(getDependencyCSVs(targetCSV, packageToCSV, packageDependencies))
2338+
2339+
return approvedCSVs
2340+
}
2341+
2342+
// getDependencyCSVs recursively converts the package dependencies to their CSV identifiers
2343+
func getDependencyCSVs(
2344+
startingCSV string, packageToCSV map[string]string, packageDependencies map[string]sets.Set[string],
2345+
) sets.Set[string] {
2346+
dependencyCSVs := sets.Set[string]{}
2347+
2348+
if startingCSV == "" || packageToCSV == nil || packageDependencies == nil {
2349+
return dependencyCSVs
2350+
}
2351+
2352+
packages, ok := packageDependencies[startingCSV]
2353+
if !ok || packages == nil {
2354+
return dependencyCSVs
2355+
}
2356+
2357+
for pkg := range packages {
2358+
if csv, ok := packageToCSV[pkg]; ok {
2359+
dependencyCSVs.Insert(csv)
23342360
}
23352361
}
23362362

2337-
return packageDependencies
2363+
if len(dependencyCSVs) == 0 {
2364+
// The startingCSV's dependencies are already satisfied
2365+
// by other installed CSVs. No additional CSVs need approval.
2366+
return dependencyCSVs
2367+
}
2368+
2369+
for csv := range dependencyCSVs {
2370+
dependencyCSVs = dependencyCSVs.Union(getDependencyCSVs(csv, packageToCSV, packageDependencies))
2371+
}
2372+
2373+
return dependencyCSVs
23382374
}
23392375

23402376
func (r *OperatorPolicyReconciler) mustnothaveInstallPlan(

controllers/operatorpolicy_controller_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,118 @@ func TestGetApprovedCSVs(t *testing.T) {
334334
}
335335
}
336336

337+
func TestGetApprovedCSVsWithPackageNameLookup(t *testing.T) {
338+
mtcOadpIPRaw, err := os.ReadFile("../test/resources/unit/mtc-oadp-installplan.yaml")
339+
if err != nil {
340+
t.Fatalf("Encountered an error when reading the mtc-oadp-installplan.yaml: %v", err)
341+
}
342+
343+
installPlan := &operatorv1alpha1.InstallPlan{}
344+
345+
err = yaml.Unmarshal(mtcOadpIPRaw, installPlan)
346+
if err != nil {
347+
t.Fatalf("Encountered an error when unmarshaling the mtc-oadp-installplan.yaml: %v", err)
348+
}
349+
350+
policy := &policyv1beta1.OperatorPolicy{
351+
ObjectMeta: metav1.ObjectMeta{
352+
Name: "mtc-operator",
353+
},
354+
Spec: policyv1beta1.OperatorPolicySpec{
355+
RemediationAction: "enforce",
356+
ComplianceType: "musthave",
357+
Severity: "medium",
358+
UpgradeApproval: "Automatic",
359+
Versions: []string{"mtc-operator.v1.8.9"},
360+
},
361+
}
362+
363+
subscription := &operatorv1alpha1.Subscription{
364+
Status: operatorv1alpha1.SubscriptionStatus{
365+
CurrentCSV: "mtc-operator.v1.8.9",
366+
},
367+
}
368+
369+
csvs := getApprovedCSVs(policy, subscription, installPlan)
370+
371+
expectedCSVs := sets.Set[string]{}
372+
expectedCSVs.Insert("mtc-operator.v1.8.9")
373+
expectedCSVs.Insert("oadp-operator.v1.5.0")
374+
375+
if !csvs.Equal(expectedCSVs) {
376+
t.Fatalf(
377+
"Expected both MTC and OADP CSVs to be approved. Got: %s, Expected: %s",
378+
strings.Join(csvs.UnsortedList(), ", "),
379+
strings.Join(expectedCSVs.UnsortedList(), ", "),
380+
)
381+
}
382+
}
383+
384+
func TestGetDependencyCSVs(t *testing.T) {
385+
happyPackageToCSV := map[string]string{
386+
"starting-operator": "starting-operator.v7.8.9",
387+
"first-dependency-operator": "primary-operator.v1.1.1",
388+
"second-dependency-operator": "secondary-operator.v2.2.2",
389+
}
390+
391+
happyPackageDependencies := map[string]sets.Set[string]{
392+
"starting-operator.v7.8.9": func() sets.Set[string] {
393+
s := sets.New[string]()
394+
s.Insert("first-dependency-operator")
395+
396+
return s
397+
}(),
398+
"primary-operator.v1.1.1": func() sets.Set[string] {
399+
s := sets.New[string]()
400+
s.Insert("second-dependency-operator")
401+
402+
return s
403+
}(),
404+
"secondary-operator.v2.2.2": sets.New[string](),
405+
}
406+
407+
packageDependenciesAlreadyInstalled := map[string]sets.Set[string]{
408+
"starting-operator.v7.8.9": func() sets.Set[string] {
409+
s := sets.New[string]()
410+
s.Insert("first-dependency-operator")
411+
412+
return s
413+
}(),
414+
}
415+
dependencyCSVsAlreadyInstalled := map[string]string{
416+
"irrelevant-uninstalled-operator": "irrelevant-uninstalled-operator.v1.2.3",
417+
}
418+
419+
expectedCSVs := sets.Set[string]{}
420+
421+
dependencyCSVs := getDependencyCSVs("starting-operator.v7.8.9", nil, nil)
422+
if !dependencyCSVs.Equal(expectedCSVs) {
423+
t.Fatalf("Expected no dependency CSVs to be approved. Got: %s, Expected: %s",
424+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
425+
strings.Join(expectedCSVs.UnsortedList(), ", "))
426+
}
427+
428+
dependencyCSVs = getDependencyCSVs(
429+
"starting-operator.v7.8.9", dependencyCSVsAlreadyInstalled, packageDependenciesAlreadyInstalled)
430+
if !dependencyCSVs.Equal(expectedCSVs) {
431+
t.Fatalf("Expected no dependency CSVs to be approved. Got: %s, Expected: %s",
432+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
433+
strings.Join(expectedCSVs.UnsortedList(), ", "))
434+
}
435+
436+
dependencyCSVs = getDependencyCSVs("starting-operator.v7.8.9", happyPackageToCSV, happyPackageDependencies)
437+
438+
expectedCSVs.Insert("primary-operator.v1.1.1")
439+
expectedCSVs.Insert("secondary-operator.v2.2.2")
440+
441+
if !dependencyCSVs.Equal(expectedCSVs) {
442+
t.Fatalf(
443+
"Expected dependency CSVs to be approved. Got: %s, Expected: %s",
444+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
445+
strings.Join(expectedCSVs.UnsortedList(), ", "))
446+
}
447+
}
448+
337449
func TestCanonicalizeVersions(t *testing.T) {
338450
policy := &policyv1beta1.OperatorPolicy{
339451
Spec: policyv1beta1.OperatorPolicySpec{
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: operators.coreos.com/v1alpha1
2+
kind: InstallPlan
3+
metadata:
4+
name: install-mtc-oadp
5+
namespace: openshift-adp
6+
spec:
7+
approval: Manual
8+
approved: false
9+
clusterServiceVersionNames:
10+
- mtc-operator.v1.8.9
11+
- oadp-operator.v1.5.0
12+
generation: 1
13+
status:
14+
bundleLookups:
15+
- identifier: oadp-operator.v1.5.0
16+
properties: '{"properties":[{"type":"olm.gvk","value":{"group":"oadp.openshift.io","kind":"CloudStorage","version":"v1alpha1"}},{"type":"olm.package","value":{"packageName":"redhat-oadp-operator","version":"1.5.0"}}]}'
17+
- identifier: mtc-operator.v1.8.9
18+
properties: '{"properties":[{"type":"olm.gvk","value":{"group":"migration.openshift.io","kind":"DirectImageMigration","version":"v1alpha1"}},{"type":"olm.package","value":{"packageName":"mtc-operator","version":"1.8.9"}},{"type":"olm.package.required","value":{"packageName":"redhat-oadp-operator","versionRange":">=1.2.3"}}]}'

0 commit comments

Comments
 (0)