Skip to content

Commit d234fc0

Browse files
Eric Stroczynskiankitathomas
authored andcommitted
feat(opm): fine-grained dependency selection in diffs (#756)
Signed-off-by: Eric Stroczynski <[email protected]> Upstream-repository: operator-registry Upstream-commit: a3253eb2fa97be52883af0a0c195d4a95500c04a
1 parent b5332db commit d234fc0

File tree

12 files changed

+692
-44
lines changed

12 files changed

+692
-44
lines changed

staging/operator-registry/alpha/action/testdata/index-declcfgs/exp-headsonly/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ schema: olm.channel
99
entries:
1010
- name: bar.v0.1.0
1111
- name: bar.v0.2.0
12+
replaces: bar.v0.1.0
1213
skips:
1314
- bar.v0.1.0
1415
- name: bar.v1.0.0

staging/operator-registry/alpha/action/testdata/index-declcfgs/latest/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ schema: olm.channel
99
entries:
1010
- name: bar.v0.1.0
1111
- name: bar.v0.2.0
12+
replaces: bar.v0.1.0
1213
skipRange: <0.2.0
1314
skips:
1415
- bar.v0.1.0

staging/operator-registry/alpha/action/testdata/index-declcfgs/old/index.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ name: alpha
99
entries:
1010
- name: bar.v0.1.0
1111
- name: bar.v0.2.0
12+
replaces: bar.v0.1.0
1213
skipRange: <0.2.0
1314
skips:
1415
- bar.v0.1.0

staging/operator-registry/alpha/declcfg/diff.go

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

33
import (
4+
"fmt"
45
"reflect"
56
"sort"
67
"sync"
@@ -167,7 +168,7 @@ func bundlesEqual(b1, b2 *model.Bundle) (bool, error) {
167168

168169
func addAllDependencies(newModel, oldModel, outputModel model.Model) error {
169170
// Get every oldModel's bundle's dependencies, and their dependencies, etc. by BFS.
170-
allProvidingBundles := []*model.Bundle{}
171+
providingBundlesByPackage := map[string][]*model.Bundle{}
171172
for curr := getBundles(outputModel); len(curr) != 0; {
172173
reqGVKs, reqPkgs, err := findDependencies(curr)
173174
if err != nil {
@@ -183,36 +184,85 @@ func addAllDependencies(newModel, oldModel, outputModel model.Model) error {
183184
for _, pkg := range newModel {
184185
providingBundles := getBundlesThatProvide(pkg, reqGVKs, reqPkgs)
185186
curr = append(curr, providingBundles...)
186-
allProvidingBundles = append(allProvidingBundles, providingBundles...)
187+
188+
oldPkg, oldHasPkg := oldModel[pkg.Name]
189+
for _, b := range providingBundles {
190+
// If the bundle is not in oldModel, add it to the set.
191+
// outputModel is checked below.
192+
add := true
193+
if oldHasPkg {
194+
if oldCh, oldHasCh := oldPkg.Channels[b.Channel.Name]; oldHasCh {
195+
_, oldHasBundle := oldCh.Bundles[b.Name]
196+
add = !oldHasBundle
197+
}
198+
}
199+
if add {
200+
providingBundlesByPackage[b.Package.Name] = append(providingBundlesByPackage[b.Package.Name], b)
201+
}
202+
}
187203
}
188204
}
189205

190206
// Add the diff between an oldModel dependency package and its new counterpart
191207
// or the entire package if oldModel does not have it.
192-
//
193-
// TODO(estroz): add bundles then fill in dependency upgrade graph
194-
// by selecting latest versions, as the EP specifies.
195-
dependencyPkgs := map[string]*model.Package{}
196-
for _, b := range allProvidingBundles {
197-
if _, copied := dependencyPkgs[b.Package.Name]; !copied {
198-
dependencyPkgs[b.Package.Name] = copyPackage(b.Package)
199-
}
200-
}
201-
for _, newDepPkg := range dependencyPkgs {
202-
// newDepPkg is a copy of a newModel pkg, so running diffPackages
203-
// on it and oldPkg, which may have some but not all bundles,
204-
// will produce a set of all bundles that outputModel doesn't have.
205-
// Otherwise, just add the whole package.
206-
if oldPkg, oldHasPkg := oldModel[newDepPkg.Name]; oldHasPkg {
207-
if err := diffPackages(oldPkg, newDepPkg); err != nil {
208+
for pkgName, bundles := range providingBundlesByPackage {
209+
newPkg := newModel[pkgName]
210+
heads := make(map[string]*model.Bundle, len(newPkg.Channels))
211+
for _, ch := range newPkg.Channels {
212+
var err error
213+
if heads[ch.Name], err = ch.Head(); err != nil {
208214
return err
209215
}
210-
if len(newDepPkg.Channels) == 0 {
211-
// Skip empty packages.
212-
continue
216+
}
217+
218+
// Sort by version then channel so bundles lower in the full graph are more likely
219+
// to be included in previous loops.
220+
sort.Slice(bundles, func(i, j int) bool {
221+
if bundles[i].Channel.Name == bundles[j].Channel.Name {
222+
return bundles[i].Version.LT(bundles[j].Version)
223+
}
224+
return bundles[i].Channel.Name < bundles[j].Channel.Name
225+
})
226+
227+
for _, b := range bundles {
228+
newCh := b.Channel
229+
230+
// Continue if b was added in a previous loop iteration.
231+
// Otherwise create a new package/channel for b if they do not exist.
232+
var (
233+
outputPkg *model.Package
234+
outputCh *model.Channel
235+
236+
outHasPkg, outHasCh bool
237+
)
238+
if outputPkg, outHasPkg = outputModel[b.Package.Name]; outHasPkg {
239+
if outputCh, outHasCh = outputPkg.Channels[b.Channel.Name]; outHasCh {
240+
if _, outputHasBundle := outputCh.Bundles[b.Name]; outputHasBundle {
241+
continue
242+
}
243+
}
244+
} else {
245+
outputPkg = copyPackageNoChannels(newPkg)
246+
outputModel[outputPkg.Name] = outputPkg
247+
}
248+
if !outHasCh {
249+
outputCh = copyChannelNoBundles(newCh, outputPkg)
250+
outputPkg.Channels[outputCh.Name] = outputCh
251+
}
252+
253+
head := heads[newCh.Name]
254+
graph := makeUpgradeGraph(newCh)
255+
intersectingBundles, intersectionFound := findIntersectingBundles(newCh, b, head, graph)
256+
if !intersectionFound {
257+
// This should never happen, since b and head are from the same model.
258+
return fmt.Errorf("channel %s: head %q not reachable from bundle %q", newCh.Name, head.Name, b.Name)
259+
}
260+
for _, ib := range intersectingBundles {
261+
if _, outHasBundle := outputCh.Bundles[ib.Name]; !outHasBundle {
262+
outputCh.Bundles[ib.Name] = copyBundle(ib, outputCh, outputPkg)
263+
}
213264
}
214265
}
215-
outputModel[newDepPkg.Name] = newDepPkg
216266
}
217267

218268
return nil
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package declcfg
2+
3+
import (
4+
"github.com/operator-framework/operator-registry/alpha/model"
5+
)
6+
7+
// makeUpgradeGraph creates a DAG of bundles with map key Bundle.Replaces.
8+
func makeUpgradeGraph(ch *model.Channel) map[string][]*model.Bundle {
9+
graph := map[string][]*model.Bundle{}
10+
for _, b := range ch.Bundles {
11+
b := b
12+
if b.Replaces != "" {
13+
graph[b.Replaces] = append(graph[b.Replaces], b)
14+
}
15+
}
16+
return graph
17+
}
18+
19+
// findIntersectingBundles finds the intersecting bundle of start and end in the
20+
// replaces upgrade graph graph by traversing down to the lowest graph node,
21+
// then returns every bundle higher than the intersection. It is possible
22+
// to find no intersection; this should only happen when start and end
23+
// are not part of the same upgrade graph.
24+
// Output bundle order is not guaranteed.
25+
// Precondition: start must be a bundle in ch.
26+
// Precondition: end must be ch's head.
27+
func findIntersectingBundles(ch *model.Channel, start, end *model.Bundle, graph map[string][]*model.Bundle) ([]*model.Bundle, bool) {
28+
// The intersecting set is equal to end if start is end.
29+
if start.Name == end.Name {
30+
return []*model.Bundle{end}, true
31+
}
32+
33+
// Construct start's replaces chain for comparison against end's.
34+
startChain := map[string]*model.Bundle{start.Name: nil}
35+
for curr := start; curr != nil && curr.Replaces != ""; curr = ch.Bundles[curr.Replaces] {
36+
startChain[curr.Replaces] = curr
37+
}
38+
39+
// Trace end's replaces chain until it intersects with start's, or the root is reached.
40+
var intersection string
41+
if _, inChain := startChain[end.Name]; inChain {
42+
intersection = end.Name
43+
} else {
44+
for curr := end; curr != nil && curr.Replaces != ""; curr = ch.Bundles[curr.Replaces] {
45+
if _, inChain := startChain[curr.Replaces]; inChain {
46+
intersection = curr.Replaces
47+
break
48+
}
49+
}
50+
}
51+
52+
// No intersection is found, delegate behavior to caller.
53+
if intersection == "" {
54+
return nil, false
55+
}
56+
57+
// Find all bundles that replace the intersection via BFS,
58+
// i.e. the set of bundles that fill the update graph between start and end.
59+
replacesIntersection := graph[intersection]
60+
replacesSet := map[string]*model.Bundle{}
61+
for _, b := range replacesIntersection {
62+
currName := ""
63+
for next := []*model.Bundle{b}; len(next) > 0; next = next[1:] {
64+
currName = next[0].Name
65+
if _, hasReplaces := replacesSet[currName]; !hasReplaces {
66+
replacers := graph[currName]
67+
next = append(next, replacers...)
68+
replacesSet[currName] = ch.Bundles[currName]
69+
}
70+
}
71+
}
72+
73+
// Remove every bundle between start and intersection exclusively,
74+
// since these bundles must already exist in the destination channel.
75+
for rep := start; rep != nil && rep.Name != intersection; rep = ch.Bundles[rep.Replaces] {
76+
delete(replacesSet, rep.Name)
77+
}
78+
79+
// Ensure both start and end are added to the output.
80+
replacesSet[start.Name] = start
81+
replacesSet[end.Name] = end
82+
var intersectingBundles []*model.Bundle
83+
for _, b := range replacesSet {
84+
intersectingBundles = append(intersectingBundles, b)
85+
}
86+
return intersectingBundles, true
87+
}

0 commit comments

Comments
 (0)