Skip to content

Bug 1983673: Check for pruned bundles on add in replaces mode #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion staging/operator-registry/cmd/opm/index/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ var (

This command will add the given set of bundle images (specified by the --bundles option) to an index image (provided by the --from-index option).

If multiple bundles are given with '--mode=replaces' (the default), bundles are added to the index by order of ascending (semver) version unless the update graph specified by replaces requires a different input order; e.g. 1.0.0 replaces 1.0.1 would result in [1.0.1, 1.0.0] instead of the [1.0.0, 1.0.1] normally expected of semver. However, for most cases (e.g. 1.0.1 replaces 1.0.0) the bundle with the highest version is used to set the default channel of the related package.
If multiple bundles are given with '--mode=replaces' (the default), bundles are added to the index by order of ascending (semver) version unless the update graph specified by replaces requires a different input order; e.g. 1.0.0 replaces 1.0.1 would result in [1.0.1, 1.0.0] instead of the [1.0.0, 1.0.1] normally expected of semver. However, for most cases (e.g. 1.0.1 replaces 1.0.0) the bundle with the highest version is used to set the default channel of the related package.

Caveat: in replaces mode, the head of a channel is always the bundle with the highest semver. Any bundles upgrading from this channel-head will be pruned.
An upgrade graph that looks like:
0.1.1 -> 0.1.2 -> 0.1.2-1
will be pruned on add to:
0.1.1 -> 0.1.2
`)

addExample = templates.Examples(`
Expand Down
105 changes: 104 additions & 1 deletion staging/operator-registry/pkg/lib/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,16 @@ func populate(ctx context.Context, loader registry.Load, graphLoader registry.Gr
}

populator := registry.NewDirectoryPopulator(loader, graphLoader, querier, unpackedImageMap, overwriteImageMap, overwrite)
return populator.Populate(mode)
if err := populator.Populate(mode); err != nil {
return err
}

for _, imgMap := range overwriteImageMap {
for to, from := range imgMap {
unpackedImageMap[to] = from
}
}
return checkForBundles(ctx, querier.(*sqlite.SQLQuerier), graphLoader, unpackedImageMap)
}

type DeleteFromRegistryRequest struct {
Expand Down Expand Up @@ -402,3 +411,97 @@ func checkForBundlePaths(querier registry.GRPCQuery, bundlePaths []string) ([]st
}
return found, missing, nil
}

// packagesFromUnpackedRefs creates packages from a set of unpacked ref dirs without their upgrade edges.
func packagesFromUnpackedRefs(bundles map[image.Reference]string) (map[string]registry.Package, error) {
graph := map[string]registry.Package{}
for to, from := range bundles {
b, err := registry.NewImageInput(to, from)
if err != nil {
return nil, fmt.Errorf("failed to parse unpacked bundle image %s: %v", to, err)
}
v, err := b.Bundle.Version()
if err != nil {
return nil, fmt.Errorf("failed to parse version for %s (%s): %v", b.Bundle.Name, b.Bundle.BundleImage, err)
}
key := registry.BundleKey{
CsvName: b.Bundle.Name,
Version: v,
BundlePath: b.Bundle.BundleImage,
}
if _, ok := graph[b.Bundle.Package]; !ok {
graph[b.Bundle.Package] = registry.Package{
Name: b.Bundle.Package,
Channels: map[string]registry.Channel{},
}
}
for _, c := range b.Bundle.Channels {
if _, ok := graph[b.Bundle.Package].Channels[c]; !ok {
graph[b.Bundle.Package].Channels[c] = registry.Channel{
Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{},
}
}
graph[b.Bundle.Package].Channels[c].Nodes[key] = nil
}
}

return graph, nil
}

// replaces mode selects highest version as channel head and
// prunes any bundles in the upgrade chain after the channel head.
// check for the presence of all bundles after a replaces-mode add.
func checkForBundles(ctx context.Context, q *sqlite.SQLQuerier, g registry.GraphLoader, bundles map[image.Reference]string) error {
if len(bundles) == 0 {
return nil
}

required, err := packagesFromUnpackedRefs(bundles)
if err != nil {
return err
}

var errs []error
for _, pkg := range required {
graph, err := g.Generate(pkg.Name)
if err != nil {
errs = append(errs, fmt.Errorf("unable to verify added bundles for package %s: %v", pkg.Name, err))
continue
}

for channel, missing := range pkg.Channels {
// trace replaces chain for reachable bundles
for next := []registry.BundleKey{graph.Channels[channel].Head}; len(next) > 0; next = next[1:] {
delete(missing.Nodes, next[0])
for edge := range graph.Channels[channel].Nodes[next[0]] {
next = append(next, edge)
}
}

for bundle := range missing.Nodes {
// check if bundle is deprecated. Bundles readded after deprecation should not be present in index and can be ignored.
deprecated, err := isDeprecated(ctx, q, bundle)
if err != nil {
errs = append(errs, fmt.Errorf("could not validate pruned bundle %s (%s) as deprecated: %v", bundle.CsvName, bundle.BundlePath, err))
}
if !deprecated {
errs = append(errs, fmt.Errorf("added bundle %s pruned from package %s, channel %s: this may be due to incorrect channel head (%s)", bundle.BundlePath, pkg.Name, channel, graph.Channels[channel].Head.CsvName))
}
}
}
}
return utilerrors.NewAggregate(errs)
}

func isDeprecated(ctx context.Context, q *sqlite.SQLQuerier, bundle registry.BundleKey) (bool, error) {
props, err := q.GetPropertiesForBundle(ctx, bundle.CsvName, bundle.Version, bundle.BundlePath)
if err != nil {
return false, err
}
for _, prop := range props {
if prop.Type == registry.DeprecatedType {
return true, nil
}
}
return false, nil
}
Loading