Skip to content

Commit df26903

Browse files
committed
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 d99383c commit df26903

File tree

3 files changed

+200
-42
lines changed

3 files changed

+200
-42
lines changed

controllers/operatorpolicy_controller.go

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,49 +2247,34 @@ 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).
2270-
func getBundleDependencies(
2271-
installPlan *operatorv1alpha1.InstallPlan, targetCSV string, packageName string,
2272-
) sets.Set[string] {
2273-
packageDependencies := sets.Set[string]{}
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.
2258+
func getBundleDependencies(installPlan *operatorv1alpha1.InstallPlan, targetCSV string) sets.Set[string] {
2259+
approvedCSVs := sets.Set[string]{}
22742260

2275-
if targetCSV == "" && packageName == "" {
2276-
return packageDependencies
2261+
if targetCSV == "" {
2262+
return approvedCSVs
22772263
}
22782264

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

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

2272+
for i, bundle := range installPlan.Status.BundleLookups {
22882273
if bundle.Properties == "" {
2289-
break
2274+
continue
22902275
}
22912276

2292-
props := map[string]interface{}{}
2277+
props := map[string]any{}
22932278

22942279
err := json.Unmarshal([]byte(bundle.Properties), &props)
22952280
if err != nil {
@@ -2304,37 +2289,86 @@ func getBundleDependencies(
23042289
break
23052290
}
23062291

2307-
propsList, ok := props["properties"].([]interface{})
2292+
propsList, ok := props["properties"].([]any)
23082293
if !ok {
2309-
break
2294+
continue
23102295
}
23112296

23122297
for _, prop := range propsList {
2313-
propMap, ok := prop.(map[string]interface{})
2298+
propMap, ok := prop.(map[string]any)
2299+
if !ok {
2300+
continue
2301+
}
2302+
2303+
propType, ok := propMap["type"].(string)
23142304
if !ok {
23152305
continue
23162306
}
23172307

2318-
if propMap["type"] != "olm.package.required" {
2308+
if propType != "olm.package" && propType != "olm.package.required" {
23192309
continue
23202310
}
23212311

2322-
propValue, ok := propMap["value"].(map[string]interface{})
2312+
propValue, ok := propMap["value"].(map[string]any)
23232313
if !ok {
23242314
continue
23252315
}
23262316

2327-
depPackageName, ok := propValue["packageName"].(string)
2328-
if !ok || depPackageName == "" {
2317+
pkgName, ok := propValue["packageName"].(string)
2318+
if !ok || pkgName == "" {
23292319
continue
23302320
}
23312321

2332-
packageDependencies.Insert(depPackageName)
2333-
packageDependencies = packageDependencies.Union(getBundleDependencies(installPlan, "", depPackageName))
2322+
switch propType {
2323+
case "olm.package":
2324+
packageToCSV[pkgName] = bundle.Identifier
2325+
case "olm.package.required":
2326+
if packageDependencies[bundle.Identifier] == nil {
2327+
packageDependencies[bundle.Identifier] = sets.Set[string]{}
2328+
}
2329+
2330+
packageDependencies[bundle.Identifier].Insert(pkgName)
2331+
}
23342332
}
23352333
}
23362334

2337-
return packageDependencies
2335+
approvedCSVs = approvedCSVs.Union(getDependencyCSVs(targetCSV, &packageToCSV, &packageDependencies))
2336+
2337+
return approvedCSVs
2338+
}
2339+
2340+
// getDependencyCSVs recursively converts the package dependencies to their CSV identifiers
2341+
func getDependencyCSVs(
2342+
startingCSV string, packageToCSV *map[string]string, packageDependencies *map[string]sets.Set[string],
2343+
) sets.Set[string] {
2344+
dependencyCSVs := sets.Set[string]{}
2345+
2346+
if startingCSV == "" || packageToCSV == nil || packageDependencies == nil {
2347+
return dependencyCSVs
2348+
}
2349+
2350+
packages := (*packageDependencies)[startingCSV]
2351+
if packages == nil {
2352+
return dependencyCSVs
2353+
}
2354+
2355+
for pkg := range packages {
2356+
if csv, ok := (*packageToCSV)[pkg]; ok {
2357+
dependencyCSVs.Insert(csv)
2358+
}
2359+
}
2360+
2361+
// This CSV's package dependencies are already satisfied
2362+
// by installed CSVs, so no additional CSVs need approval.
2363+
if len(dependencyCSVs) == 0 {
2364+
return dependencyCSVs
2365+
}
2366+
2367+
for csv := range dependencyCSVs {
2368+
dependencyCSVs = dependencyCSVs.Union(getDependencyCSVs(csv, packageToCSV, packageDependencies))
2369+
}
2370+
2371+
return dependencyCSVs
23382372
}
23392373

23402374
func (r *OperatorPolicyReconciler) mustnothaveInstallPlan(

controllers/operatorpolicy_controller_test.go

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ func TestMessageIncludesSubscription(t *testing.T) {
260260
}
261261
}
262262

263-
func TestGetApprovedCSVs(t *testing.T) {
263+
func TestGetApprovedCSVsWithMultipleCSVs(t *testing.T) {
264264
odfIPRaw, err := os.ReadFile("../test/resources/unit/odf-installplan.yaml")
265265
if err != nil {
266266
t.Fatalf("Encountered an error when reading the odf-installplan.yaml: %v", err)
@@ -334,6 +334,112 @@ 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+
"mtc-operator": "mtc-operator.v1.8.9",
387+
"oadp-operator": "oadp-operator.v1.5.0",
388+
}
389+
390+
happyPackageDependencies := map[string]sets.Set[string]{
391+
"mtc-operator.v1.8.9": func() sets.Set[string] {
392+
s := sets.New[string]()
393+
s.Insert("oadp-operator")
394+
395+
return s
396+
}(),
397+
"oadp-operator.v1.5.0": sets.New[string](),
398+
}
399+
400+
packageDependenciesAlreadyInstalled := map[string]sets.Set[string]{
401+
"mtc-operator.v1.8.9": sets.New[string](),
402+
}
403+
dependencyCSVsAlreadyInstalled := map[string]string{
404+
"irrelevant-operator": "irrelevant-operator.v1.2.3",
405+
}
406+
407+
expectedCSVs := sets.Set[string]{}
408+
409+
dependencyCSVs := getDependencyCSVs("mtc-operator.v1.8.9", nil, nil)
410+
if !dependencyCSVs.Equal(expectedCSVs) {
411+
t.Fatalf("Expected no dependency CSVs to be approved. Got: %s, Expected: %s",
412+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
413+
strings.Join(expectedCSVs.UnsortedList(), ", "))
414+
}
415+
416+
dependencyCSVs = getDependencyCSVs(
417+
"mtc-operator.v1.8.9", &dependencyCSVsAlreadyInstalled, &happyPackageDependencies)
418+
if !dependencyCSVs.Equal(expectedCSVs) {
419+
t.Fatalf("Expected no dependency CSVs to be approved. Got: %s, Expected: %s",
420+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
421+
strings.Join(expectedCSVs.UnsortedList(), ", "))
422+
}
423+
424+
dependencyCSVs = getDependencyCSVs("mtc-operator.v1.8.9", &happyPackageToCSV, &packageDependenciesAlreadyInstalled)
425+
if !dependencyCSVs.Equal(expectedCSVs) {
426+
t.Fatalf("Expected no dependency CSVs to be approved. Got: %s, Expected: %s",
427+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
428+
strings.Join(expectedCSVs.UnsortedList(), ", "))
429+
}
430+
431+
expectedCSVs.Insert("oadp-operator.v1.5.0")
432+
433+
dependencyCSVs = getDependencyCSVs("mtc-operator.v1.8.9", &happyPackageToCSV, &happyPackageDependencies)
434+
435+
if !dependencyCSVs.Equal(expectedCSVs) {
436+
t.Fatalf(
437+
"Expected OADP CSV to be approved. Got: %s, Expected: %s",
438+
strings.Join(dependencyCSVs.UnsortedList(), ", "),
439+
strings.Join(expectedCSVs.UnsortedList(), ", "))
440+
}
441+
}
442+
337443
func TestCanonicalizeVersions(t *testing.T) {
338444
policy := &policyv1beta1.OperatorPolicy{
339445
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)