Skip to content

Commit

Permalink
check for pruned bundles on add in replaces mode (#732)
Browse files Browse the repository at this point in the history
* check for pruned bundles in replaces-mode add

Signed-off-by: Ankita Thomas <ankithom@redhat.com>

* improve readability, generate testdata

Signed-off-by: Ankita Thomas <ankithom@redhat.com>
  • Loading branch information
ankitathomas committed Aug 9, 2021
1 parent c2fc1fd commit d446d3c
Show file tree
Hide file tree
Showing 3 changed files with 539 additions and 2 deletions.
8 changes: 7 additions & 1 deletion 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 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

0 comments on commit d446d3c

Please sign in to comment.