Skip to content
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
85 changes: 85 additions & 0 deletions alpha/action/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"context"
"errors"
"fmt"
"io"

"github.com/blang/semver"
"github.com/sirupsen/logrus"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/yaml"

"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/model"
Expand All @@ -21,6 +25,8 @@ type Diff struct {
// of bundles included in the diff if true.
SkipDependencies bool

IncludeConfig DiffIncludeConfig

Logger *logrus.Entry
}

Expand Down Expand Up @@ -65,6 +71,7 @@ func (a Diff) Run(ctx context.Context) (*declcfg.DeclarativeConfig, error) {
g := &declcfg.DiffGenerator{
Logger: a.Logger,
SkipDependencies: a.SkipDependencies,
Includer: convertIncludeConfigToIncluder(a.IncludeConfig),
}
diffModel, err := g.Run(oldModel, newModel)
if err != nil {
Expand All @@ -81,3 +88,81 @@ func (p Diff) validate() error {
}
return nil
}

// DiffIncludeConfig configures Diff.Run() to include a set of packages,
// channels, and/or bundle versions in the output DeclarativeConfig.
// These override other diff mechanisms. For example, if running in
// heads-only mode but package "foo" channel "stable" is specified,
// the entire "stable" channel (all channel bundles) is added to the output.
type DiffIncludeConfig struct {
// Packages to include.
Packages []DiffIncludePackage `json:"packages" yaml:"packages"`
}

// DiffIncludePackage contains a name (required) and channels and/or versions
// (optional) to include in the diff. The full package is only included if no channels
// or versions are specified.
type DiffIncludePackage struct {
// Name of package.
Name string `json:"name" yaml:"name"`
// Channels to include.
Channels []DiffIncludeChannel `json:"channels,omitempty" yaml:"channels,omitempty"`
// Versions to include. All channels containing these versions
// are parsed for an upgrade graph.
Versions []semver.Version `json:"versions,omitempty" yaml:"versions,omitempty"`
}

// DiffIncludeChannel contains a name (required) and versions (optional)
// to include in the diff. The full channel is only included if no versions are specified.
type DiffIncludeChannel struct {
// Name of channel.
Name string `json:"name" yaml:"name"`
// Versions to include.
Versions []semver.Version `json:"versions,omitempty" yaml:"versions,omitempty"`
}

// LoadDiffIncludeConfig loads a (YAML or JSON) DiffIncludeConfig from r.
func LoadDiffIncludeConfig(r io.Reader) (c DiffIncludeConfig, err error) {
dec := yaml.NewYAMLOrJSONDecoder(r, 8)
if err := dec.Decode(&c); err != nil {
return DiffIncludeConfig{}, err
}

if len(c.Packages) == 0 {
return c, fmt.Errorf("must specify at least one package in include config")
}

var errs []error
for pkgI, pkg := range c.Packages {
if pkg.Name == "" {
errs = append(errs, fmt.Errorf("package at index %v requires a name", pkgI))
continue
}
for chI, ch := range pkg.Channels {
if ch.Name == "" {
errs = append(errs, fmt.Errorf("package %s: channel at index %v requires a name", pkg.Name, chI))
continue
}
}
}
return c, utilerrors.NewAggregate(errs)
}

func convertIncludeConfigToIncluder(c DiffIncludeConfig) (includer declcfg.DiffIncluder) {
includer.Packages = make([]declcfg.DiffIncludePackage, len(c.Packages))
for pkgI, cpkg := range c.Packages {
pkg := &includer.Packages[pkgI]
pkg.Name = cpkg.Name
pkg.AllChannels.Versions = cpkg.Versions

if len(cpkg.Channels) != 0 {
pkg.Channels = make([]declcfg.DiffIncludeChannel, len(cpkg.Channels))
for chI, cch := range cpkg.Channels {
ch := &pkg.Channels[chI]
ch.Name = cch.Name
ch.Versions = cch.Versions
}
}
}
return includer
}
160 changes: 149 additions & 11 deletions alpha/action/diff_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package action_test
package action

import (
"bytes"
"context"
"embed"
"errors"
Expand All @@ -10,10 +11,10 @@ import (
"strings"
"testing"

"github.com/blang/semver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/operator-framework/operator-registry/alpha/action"
"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/pkg/containertools"
"github.com/operator-framework/operator-registry/pkg/image"
Expand All @@ -23,7 +24,7 @@ import (
func TestDiff(t *testing.T) {
type spec struct {
name string
diff action.Diff
diff Diff
expectedCfg *declcfg.DeclarativeConfig
assertion require.ErrorAssertionFunc
}
Expand All @@ -34,7 +35,7 @@ func TestDiff(t *testing.T) {
specs := []spec{
{
name: "Success/Latest",
diff: action.Diff{
diff: Diff{
Registry: registry,
OldRefs: []string{filepath.Join("testdata", "index-declcfgs", "old")},
NewRefs: []string{filepath.Join("testdata", "index-declcfgs", "latest")},
Expand All @@ -44,7 +45,7 @@ func TestDiff(t *testing.T) {
},
{
name: "Success/HeadsOnly",
diff: action.Diff{
diff: Diff{
Registry: registry,
NewRefs: []string{filepath.Join("testdata", "index-declcfgs", "latest")},
},
Expand All @@ -53,22 +54,22 @@ func TestDiff(t *testing.T) {
},
{
name: "Fail/NewBundleImage",
diff: action.Diff{
diff: Diff{
Registry: registry,
NewRefs: []string{"test.registry/foo-operator/foo-bundle:v0.1.0"},
},
assertion: func(t require.TestingT, err error, _ ...interface{}) {
if !assert.Error(t, err) {
require.Fail(t, "expected an error")
}
if !errors.Is(err, action.ErrNotAllowed) {
require.Fail(t, "err is not action.ErrNotAllowed", err)
if !errors.Is(err, ErrNotAllowed) {
require.Fail(t, "err is not ErrNotAllowed", err)
}
},
},
{
name: "Fail/OldBundleImage",
diff: action.Diff{
diff: Diff{
Registry: registry,
OldRefs: []string{"test.registry/foo-operator/foo-bundle:v0.1.0"},
NewRefs: []string{filepath.Join("testdata", "index-declcfgs", "latest")},
Expand All @@ -77,8 +78,8 @@ func TestDiff(t *testing.T) {
if !assert.Error(t, err) {
require.Fail(t, "expected an error")
}
if !errors.Is(err, action.ErrNotAllowed) {
require.Fail(t, "err is not action.ErrNotAllowed", err)
if !errors.Is(err, ErrNotAllowed) {
require.Fail(t, "err is not ErrNotAllowed", err)
}
},
},
Expand All @@ -93,6 +94,143 @@ func TestDiff(t *testing.T) {
}
}

func TestLoadDiffIncludeConfig(t *testing.T) {
type spec struct {
name string
input string
expectedCfg DiffIncludeConfig
expectedIncluder declcfg.DiffIncluder
assertion require.ErrorAssertionFunc
}

specs := []spec{
{
name: "Success/Basic",
input: `
packages:
- name: foo
`,
expectedCfg: DiffIncludeConfig{
Packages: []DiffIncludePackage{{Name: "foo"}},
},
expectedIncluder: declcfg.DiffIncluder{
Packages: []declcfg.DiffIncludePackage{{Name: "foo"}},
},
assertion: require.NoError,
},
{
name: "Success/MultiPackage",
input: `
packages:
- name: foo
channels:
- name: stable
versions:
- 0.1.0
- 0.2.0
versions:
- 1.0.0
- name: bar
channels:
- name: stable
versions:
- 0.1.0
versions:
- 1.0.0
`,
expectedCfg: DiffIncludeConfig{
Packages: []DiffIncludePackage{
{
Name: "foo",
Channels: []DiffIncludeChannel{
{Name: "stable", Versions: []semver.Version{
semver.MustParse("0.1.0"),
semver.MustParse("0.2.0"),
}},
},
Versions: []semver.Version{semver.MustParse("1.0.0")},
},
{
Name: "bar",
Channels: []DiffIncludeChannel{
{Name: "stable", Versions: []semver.Version{
semver.MustParse("0.1.0"),
}},
},
Versions: []semver.Version{semver.MustParse("1.0.0")},
},
},
},
expectedIncluder: declcfg.DiffIncluder{
Packages: []declcfg.DiffIncludePackage{
{
Name: "foo",
Channels: []declcfg.DiffIncludeChannel{
{Name: "stable", Versions: []semver.Version{
semver.MustParse("0.1.0"),
semver.MustParse("0.2.0"),
}},
},
AllChannels: declcfg.DiffIncludeChannel{
Versions: []semver.Version{semver.MustParse("1.0.0")},
},
},
{
Name: "bar",
Channels: []declcfg.DiffIncludeChannel{
{Name: "stable", Versions: []semver.Version{
semver.MustParse("0.1.0"),
}},
},
AllChannels: declcfg.DiffIncludeChannel{
Versions: []semver.Version{semver.MustParse("1.0.0")},
},
},
},
},
assertion: require.NoError,
},
{
name: "Fail/Empty",
input: ``,
assertion: require.Error,
},
{
name: "Fail/NoPackageName",
input: `
packages:
- channels:
- name: stable
versions:
- 0.1.0
`,
assertion: require.Error,
},
{
name: "Fail/NoChannelName",
input: `
packages:
- name: foo
channels:
- versions:
- 0.1.0
`,
assertion: require.Error,
},
}

for _, s := range specs {
t.Run(s.name, func(t *testing.T) {
actualCfg, err := LoadDiffIncludeConfig(bytes.NewBufferString(s.input))
s.assertion(t, err)
if err == nil {
require.Equal(t, s.expectedCfg, actualCfg)
require.Equal(t, s.expectedIncluder, convertIncludeConfigToIncluder(actualCfg))
}
})
}
}

var (
//go:embed testdata/foo-bundle-v0.1.0/manifests/*
//go:embed testdata/foo-bundle-v0.1.0/metadata/*
Expand Down
Loading