Skip to content

Commit b3d8d89

Browse files
Merge pull request #2005 from ecordell/benluddy-nicer-invariants
Add meaningful string representations for resolution invariants
2 parents 31c1c56 + ee37f17 commit b3d8d89

File tree

7 files changed

+258
-182
lines changed

7 files changed

+258
-182
lines changed

pkg/controller/registry/resolver/installabletypes.go

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"strings"
66

77
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
8-
98
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver"
109
)
1110

@@ -37,7 +36,7 @@ func (i *BundleInstallable) AddDependency(dependencies []solver.Identifier) {
3736
}
3837

3938
func (i *BundleInstallable) BundleSourceInfo() (string, string, registry.CatalogKey, error) {
40-
info := strings.Split(string(i.identifier), "/")
39+
info := strings.Split(i.identifier.String(), "/")
4140
// This should be enforced by Kube naming constraints
4241
if len(info) != 4 {
4342
return "", "", registry.CatalogKey{}, fmt.Errorf("Unable to parse identifier %s for source info", i.identifier)
@@ -52,7 +51,7 @@ func (i *BundleInstallable) BundleSourceInfo() (string, string, registry.Catalog
5251
}
5352

5453
func bundleId(bundle, channel string, catalog registry.CatalogKey) solver.Identifier {
55-
return solver.Identifier(fmt.Sprintf("%s/%s/%s", catalog.String(), channel, bundle))
54+
return solver.IdentifierFromString(fmt.Sprintf("%s/%s/%s", catalog.String(), channel, bundle))
5655
}
5756

5857
func NewBundleInstallable(bundle, channel string, catalog registry.CatalogKey, constraints ...solver.Constraint) BundleInstallable {
@@ -62,58 +61,100 @@ func NewBundleInstallable(bundle, channel string, catalog registry.CatalogKey, c
6261
}
6362
}
6463

65-
func NewSubscriptionInstallable(pkg string) SubscriptionInstallable {
66-
return SubscriptionInstallable{
67-
identifier: solver.Identifier(pkg),
68-
constraints: []solver.Constraint{
69-
ConstraintSubscriptionExists,
70-
},
71-
}
72-
}
73-
74-
type SubscriptionInstallable struct {
64+
type GenericInstallable struct {
7565
identifier solver.Identifier
7666
constraints []solver.Constraint
7767
}
7868

79-
func (r SubscriptionInstallable) Identifier() solver.Identifier {
80-
return r.identifier
69+
func (i GenericInstallable) Identifier() solver.Identifier {
70+
return i.identifier
8171
}
8272

83-
func (r SubscriptionInstallable) Constraints() []solver.Constraint {
84-
return r.constraints
73+
func (i GenericInstallable) Constraints() []solver.Constraint {
74+
return i.constraints
8575
}
8676

87-
func (r *SubscriptionInstallable) AddDependency(dependencies []solver.Identifier) {
77+
func NewSubscriptionInstallable(name string, dependencies []solver.Identifier) solver.Installable {
78+
result := GenericInstallable{
79+
identifier: solver.IdentifierFromString(fmt.Sprintf("subscription:%s", name)),
80+
constraints: []solver.Constraint{
81+
PrettyConstraint(solver.Mandatory(), fmt.Sprintf("subscription %s exists", name)),
82+
},
83+
}
84+
8885
if len(dependencies) == 0 {
89-
r.constraints = append(r.constraints, ConstraintSubscriptionWithoutCandidates)
90-
return
86+
result.constraints = append(result.constraints, PrettyConstraint(solver.Dependency(), fmt.Sprintf("no operators found matching the criteria of subscription %s", name)))
87+
return result
9188
}
9289

9390
s := make([]string, len(dependencies))
9491
for i, each := range dependencies {
95-
s[i] = string(each)
92+
s[i] = each.String()
93+
}
94+
var req string
95+
if len(s) == 1 {
96+
req = s[0]
97+
} else {
98+
req = fmt.Sprintf("at least one of %s or %s", strings.Join(s[:len(s)-1], ", "), s[len(s)-1])
99+
}
100+
result.constraints = append(result.constraints, PrettyConstraint(solver.Dependency(dependencies...), fmt.Sprintf("subscription %s requires %s", name, req)))
101+
102+
return result
103+
}
104+
105+
func NewSingleAPIProviderInstallable(group, version, kind string, providers []solver.Identifier) solver.Installable {
106+
gvk := fmt.Sprintf("%s (%s/%s)", kind, group, version)
107+
result := GenericInstallable{
108+
identifier: solver.IdentifierFromString(gvk),
109+
}
110+
if len(providers) <= 1 {
111+
// The constraints are pointless without more than one provider.
112+
return result
113+
}
114+
result.constraints = append(result.constraints, PrettyConstraint(solver.Mandatory(), fmt.Sprintf("there can be only one provider of %s", gvk)))
115+
116+
var s []string
117+
for _, p := range providers {
118+
s = append(s, p.String())
96119
}
97-
r.constraints = append(r.constraints, PrettyConstraint(solver.Dependency(dependencies...), fmt.Sprintf("subscription to %%s requires at least one of %s", strings.Join(s, ", "))))
120+
msg := fmt.Sprintf("%s and %s provide %s", strings.Join(s[:len(s)-1], ", "), s[len(s)-1], gvk)
121+
result.constraints = append(result.constraints, PrettyConstraint(solver.AtMost(1, providers...), msg))
122+
123+
return result
98124
}
99125

100-
func PrettyConstraint(c solver.Constraint, format string) solver.Constraint {
126+
func NewSinglePackageInstanceInstallable(pkg string, providers []solver.Identifier) solver.Installable {
127+
result := GenericInstallable{
128+
identifier: solver.IdentifierFromString(pkg),
129+
}
130+
if len(providers) <= 1 {
131+
// The constraints are pointless without more than one provider.
132+
return result
133+
}
134+
result.constraints = append(result.constraints, PrettyConstraint(solver.Mandatory(), fmt.Sprintf("there can be only one operator from package %s", pkg)))
135+
136+
var s []string
137+
for _, p := range providers {
138+
s = append(s, p.String())
139+
}
140+
msg := fmt.Sprintf("%s and %s originate from package %s", strings.Join(s[:len(s)-1], ", "), s[len(s)-1], pkg)
141+
result.constraints = append(result.constraints, PrettyConstraint(solver.AtMost(1, providers...), msg))
142+
143+
return result
144+
}
145+
146+
func PrettyConstraint(c solver.Constraint, msg string) solver.Constraint {
101147
return prettyConstraint{
102148
Constraint: c,
103-
format: format,
149+
msg: msg,
104150
}
105151
}
106152

107153
type prettyConstraint struct {
108154
solver.Constraint
109-
format string
155+
msg string
110156
}
111157

112-
func (pc prettyConstraint) String(subject solver.Identifier) string {
113-
return fmt.Sprintf(pc.format, subject)
158+
func (pc prettyConstraint) String(_ solver.Identifier) string {
159+
return pc.msg
114160
}
115-
116-
var (
117-
ConstraintSubscriptionExists = PrettyConstraint(solver.Mandatory(), "a subscription to package %s exists in the namespace")
118-
ConstraintSubscriptionWithoutCandidates = PrettyConstraint(solver.Dependency(), "no operators found matching the subscription criteria for %s")
119-
)

pkg/controller/registry/resolver/resolver.go

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func (r *SatResolver) SolveOperators(namespaces []string, csvs []*v1alpha1.Clust
102102
}
103103

104104
// find operators, in channel order, that can skip from the current version or list the current in "replaces"
105-
subInstallables, err := r.getSubscriptionInstallables(pkg, sub.Namespace, current, catalog, predicates, channelFilter, namespacedCache, visited)
105+
subInstallables, err := r.getSubscriptionInstallables(sub.Name, pkg, sub.Namespace, current, catalog, predicates, channelFilter, namespacedCache, visited)
106106
if err != nil {
107107
errs = append(errs, err)
108108
continue
@@ -180,13 +180,10 @@ func (r *SatResolver) SolveOperators(namespaces []string, csvs []*v1alpha1.Clust
180180
return operators, nil
181181
}
182182

183-
func (r *SatResolver) getSubscriptionInstallables(pkg, namespace string, current *Operator, catalog registry.CatalogKey, cachePredicates []OperatorPredicate, channelPredicates []OperatorPredicate, namespacedCache MultiCatalogOperatorFinder, visited map[OperatorSurface]*BundleInstallable) (map[solver.Identifier]solver.Installable, error) {
183+
func (r *SatResolver) getSubscriptionInstallables(name, pkg, namespace string, current *Operator, catalog registry.CatalogKey, cachePredicates []OperatorPredicate, channelPredicates []OperatorPredicate, namespacedCache MultiCatalogOperatorFinder, visited map[OperatorSurface]*BundleInstallable) (map[solver.Identifier]solver.Installable, error) {
184184
installables := make(map[solver.Identifier]solver.Installable, 0)
185185
candidates := make([]*BundleInstallable, 0)
186186

187-
subInstallable := NewSubscriptionInstallable(pkg)
188-
installables[subInstallable.Identifier()] = &subInstallable
189-
190187
bundles := namespacedCache.Catalog(catalog).Find(cachePredicates...)
191188

192189
// bundles in the default channel appear first, then lexicographically order by channel name
@@ -263,7 +260,8 @@ func (r *SatResolver) getSubscriptionInstallables(pkg, namespace string, current
263260
}
264261

265262
// all candidates added as options for this constraint
266-
subInstallable.AddDependency(depIds)
263+
subInstallable := NewSubscriptionInstallable(name, depIds)
264+
installables[subInstallable.Identifier()] = subInstallable
267265

268266
return installables, nil
269267
}
@@ -423,7 +421,8 @@ func (r *SatResolver) newSnapshotForNamespace(namespace string, subs []*v1alpha1
423421

424422
func (r *SatResolver) addInvariants(namespacedCache MultiCatalogOperatorFinder, installables map[solver.Identifier]solver.Installable) {
425423
// no two operators may provide the same GVK or Package in a namespace
426-
conflictToInstallable := make(map[string][]solver.Installable)
424+
gvkConflictToInstallable := make(map[opregistry.GVKProperty][]solver.Identifier)
425+
packageConflictToInstallable := make(map[string][]solver.Identifier)
427426
for _, installable := range installables {
428427
bundleInstallable, ok := installable.(*BundleInstallable)
429428
if !ok {
@@ -449,12 +448,7 @@ func (r *SatResolver) addInvariants(namespacedCache MultiCatalogOperatorFinder,
449448
if err != nil {
450449
continue
451450
}
452-
key := fmt.Sprintf("gvkunique/%s/%s/%s", prop.Group, prop.Version, prop.Kind)
453-
_, ok := conflictToInstallable[key]
454-
if !ok {
455-
conflictToInstallable[key] = make([]solver.Installable, 0)
456-
}
457-
conflictToInstallable[key] = append(conflictToInstallable[key], installable)
451+
gvkConflictToInstallable[prop] = append(gvkConflictToInstallable[prop], installable.Identifier())
458452
}
459453

460454
// cannot have the same package
@@ -467,27 +461,18 @@ func (r *SatResolver) addInvariants(namespacedCache MultiCatalogOperatorFinder,
467461
if err != nil {
468462
continue
469463
}
470-
key := fmt.Sprintf("pkgunique/%s", prop.PackageName)
471-
_, ok := conflictToInstallable[key]
472-
if !ok {
473-
conflictToInstallable[key] = make([]solver.Installable, 0)
474-
}
475-
conflictToInstallable[key] = append(conflictToInstallable[key], installable)
464+
packageConflictToInstallable[prop.PackageName] = append(packageConflictToInstallable[prop.PackageName], installable.Identifier())
476465
}
477466
}
478467

479-
for key, is := range conflictToInstallable {
480-
if len(is) <= 1 {
481-
continue
482-
}
468+
for gvk, is := range gvkConflictToInstallable {
469+
s := NewSingleAPIProviderInstallable(gvk.Group, gvk.Version, gvk.Kind, is)
470+
installables[s.Identifier()] = s
471+
}
483472

484-
ids := make([]solver.Identifier, 0)
485-
for _, d := range is {
486-
ids = append(ids, d.Identifier())
487-
}
488-
s := NewSubscriptionInstallable(key)
489-
s.constraints = append(s.constraints, solver.AtMost(1, ids...))
490-
installables[s.Identifier()] = &s
473+
for pkg, is := range packageConflictToInstallable {
474+
s := NewSinglePackageInstanceInstallable(pkg, is)
475+
installables[s.Identifier()] = s
491476
}
492477
}
493478

pkg/controller/registry/resolver/resolver_test.go

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resolver
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/blang/semver/v4"
@@ -1241,34 +1242,36 @@ func TestSolveOperators_TransferApiOwnership(t *testing.T) {
12411242
}
12421243

12431244
var operators OperatorSet
1244-
for _, p := range phases {
1245-
fakeNamespacedOperatorCache := NamespacedOperatorCache{
1246-
snapshots: map[registry.CatalogKey]*CatalogSnapshot{
1247-
catalog: p.catalog,
1248-
},
1249-
}
1250-
satResolver := SatResolver{
1251-
cache: getFakeOperatorCache(fakeNamespacedOperatorCache),
1252-
log: logrus.New(),
1253-
}
1254-
csvs := make([]*v1alpha1.ClusterServiceVersion, 0)
1255-
for _, o := range operators {
1256-
var pkg, channel string
1257-
if b := o.Bundle(); b != nil {
1258-
pkg = b.PackageName
1259-
channel = b.ChannelName
1245+
for i, p := range phases {
1246+
t.Run(fmt.Sprintf("phase %d", i+1), func(t *testing.T) {
1247+
fakeNamespacedOperatorCache := NamespacedOperatorCache{
1248+
snapshots: map[registry.CatalogKey]*CatalogSnapshot{
1249+
catalog: p.catalog,
1250+
},
1251+
}
1252+
satResolver := SatResolver{
1253+
cache: getFakeOperatorCache(fakeNamespacedOperatorCache),
1254+
log: logrus.New(),
1255+
}
1256+
csvs := make([]*v1alpha1.ClusterServiceVersion, 0)
1257+
for _, o := range operators {
1258+
var pkg, channel string
1259+
if b := o.Bundle(); b != nil {
1260+
pkg = b.PackageName
1261+
channel = b.ChannelName
1262+
}
1263+
csvs = append(csvs, existingOperator(namespace, o.Identifier(), pkg, channel, o.Replaces(), o.ProvidedAPIs(), o.RequiredAPIs(), nil, nil))
12601264
}
1261-
csvs = append(csvs, existingOperator(namespace, o.Identifier(), pkg, channel, o.Replaces(), o.ProvidedAPIs(), o.RequiredAPIs(), nil, nil))
1262-
}
12631265

1264-
var err error
1265-
operators, err = satResolver.SolveOperators([]string{"olm"}, csvs, p.subs)
1266-
assert.NoError(t, err)
1267-
for k := range p.expected {
1268-
require.NotNil(t, operators[k])
1269-
assert.EqualValues(t, k, operators[k].Identifier())
1270-
}
1271-
assert.Equal(t, len(p.expected), len(operators))
1266+
var err error
1267+
operators, err = satResolver.SolveOperators([]string{"olm"}, csvs, p.subs)
1268+
assert.NoError(t, err)
1269+
for k := range p.expected {
1270+
require.NotNil(t, operators[k])
1271+
assert.EqualValues(t, k, operators[k].Identifier())
1272+
}
1273+
assert.Equal(t, len(p.expected), len(operators))
1274+
})
12721275
}
12731276
}
12741277

pkg/controller/registry/resolver/solver/installable.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ func (id Identifier) String() string {
88
return string(id)
99
}
1010

11+
// IdentifierFromString returns an Identifier based on a provided
12+
// string.
13+
func IdentifierFromString(s string) Identifier {
14+
return Identifier(s)
15+
}
16+
1117
// Installable values are the basic unit of problems and solutions
1218
// understood by this package.
1319
type Installable interface {

0 commit comments

Comments
 (0)