diff --git a/alpha/action/diff.go b/alpha/action/diff.go index cb9269626..9102f2fb2 100644 --- a/alpha/action/diff.go +++ b/alpha/action/diff.go @@ -26,6 +26,8 @@ type Diff struct { SkipDependencies bool IncludeConfig DiffIncludeConfig + // IncludeAdditively catalog objects specified in IncludeConfig. + IncludeAdditively bool Logger *logrus.Entry } @@ -69,9 +71,10 @@ func (a Diff) Run(ctx context.Context) (*declcfg.DeclarativeConfig, error) { } g := &declcfg.DiffGenerator{ - Logger: a.Logger, - SkipDependencies: a.SkipDependencies, - Includer: convertIncludeConfigToIncluder(a.IncludeConfig), + Logger: a.Logger, + SkipDependencies: a.SkipDependencies, + Includer: convertIncludeConfigToIncluder(a.IncludeConfig), + IncludeAdditively: a.IncludeAdditively, } diffModel, err := g.Run(oldModel, newModel) if err != nil { diff --git a/alpha/action/diff_test.go b/alpha/action/diff_test.go index ae4082bfd..64414614e 100644 --- a/alpha/action/diff_test.go +++ b/alpha/action/diff_test.go @@ -60,6 +60,7 @@ func TestDiff(t *testing.T) { IncludeConfig: DiffIncludeConfig{ Packages: []DiffIncludePackage{{Name: "baz"}}, }, + IncludeAdditively: true, }, expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-include-pkg")), assertion: require.NoError, @@ -77,6 +78,7 @@ func TestDiff(t *testing.T) { }, }, }, + IncludeAdditively: true, }, expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-include-channel")), assertion: require.NoError, @@ -94,6 +96,7 @@ func TestDiff(t *testing.T) { }, }, }, + IncludeAdditively: true, }, expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-include-channel")), assertion: require.NoError, @@ -111,6 +114,7 @@ func TestDiff(t *testing.T) { }, }, }, + IncludeAdditively: true, }, expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-include-channel")), assertion: require.NoError, @@ -129,6 +133,7 @@ func TestDiff(t *testing.T) { }, }, }, + IncludeAdditively: true, }, expectedCfg: loadDirFS(t, indicesDir, filepath.Join("testdata", "index-declcfgs", "exp-include-channel")), assertion: require.NoError, diff --git a/alpha/declcfg/diff.go b/alpha/declcfg/diff.go index fbc550dbc..8234c0c4d 100644 --- a/alpha/declcfg/diff.go +++ b/alpha/declcfg/diff.go @@ -23,6 +23,8 @@ type DiffGenerator struct { SkipDependencies bool // Includer for adding catalog objects to Run() output. Includer DiffIncluder + // IncludeAdditively catalog objects specified in Includer in headsOnly mode. + IncludeAdditively bool initOnce sync.Once } @@ -32,13 +34,21 @@ func (g *DiffGenerator) init() { if g.Logger == nil { g.Logger = &logrus.Entry{} } + if g.Includer.Logger == nil { + g.Includer.Logger = g.Logger + } }) } -// Run returns a Model containing everything in newModel not in oldModel, -// and all bundles that exist in oldModel but are different in newModel. -// If oldModel is empty, only channel heads in newModel's packages are -// added to the output Model. All dependencies not in oldModel are also added. +// Run returns a Model containing a subset of catalog objects in newModel: +// - If g.Includer contains objects: +// - If g.IncludeAdditively is false, a diff will be generated only on those objects, +// depending on the mode. +// - If g.IncludeAdditionally is true, the diff will contain included objects, +// plus those added by the mode. +// - If in heads-only mode (oldModel == nil), then the heads of channels are added to the output. +// - If in latest mode, a diff between old and new Models is added to the output. +// - Dependencies are added in all modes if g.SkipDependencies is false. func (g *DiffGenerator) Run(oldModel, newModel model.Model) (model.Model, error) { g.init() @@ -48,73 +58,101 @@ func (g *DiffGenerator) Run(oldModel, newModel model.Model) (model.Model, error) outputModel := model.Model{} - // Add included packages/channels/bundles from newModel to outputModel. - // Code further down handles packages and channels included in outputModel. - g.Includer.Logger = g.Logger - if err := g.Includer.Run(newModel, outputModel); err != nil { - return nil, err - } - - if len(oldModel) == 0 { - // Heads-only mode. + // Prunes old objects from outputModel if they exist. + latestPruneFromOutput := func() error { - // Make shallow copies of packages and channels that are only - // filled with channel heads. - for _, newPkg := range newModel { - // This package may have been created in the include step. - outputPkg, pkgIncluded := outputModel[newPkg.Name] - if !pkgIncluded { - outputPkg = copyPackageNoChannels(newPkg) - outputModel[outputPkg.Name] = outputPkg - } - for _, newCh := range newPkg.Channels { - if _, chIncluded := outputPkg.Channels[newCh.Name]; chIncluded { - // Head (and other bundles) were added in the include step. - continue - } - outputCh := copyChannelNoBundles(newCh, outputPkg) - outputPkg.Channels[outputCh.Name] = outputCh - head, err := newCh.Head() - if err != nil { - return nil, err - } - outputBundle := copyBundle(head, outputCh, outputPkg) - outputModel.AddBundle(*outputBundle) - } - } - } else { - // Latest mode. - - // Copy newModel to create an output model by deletion, - // which is more succinct than by addition and potentially - // more memory efficient. - for _, newPkg := range newModel { - if _, pkgIncluded := outputModel[newPkg.Name]; pkgIncluded { - // The user has specified the state they want this package to have in the diff - // via an inclusion entry, so the package created above should not be changed. - continue - } - outputModel[newPkg.Name] = copyPackage(newPkg) - } - - // NB(estroz): if a net-new package or channel is published, - // this currently adds the entire package. I'm fairly sure - // this behavior is ok because the next diff after a new - // package is published still has only new data. for _, outputPkg := range outputModel { oldPkg, oldHasPkg := oldModel[outputPkg.Name] if !oldHasPkg { // outputPkg was already copied to outputModel above. continue } - if err := diffPackages(oldPkg, outputPkg); err != nil { - return nil, err + if err := pruneOldFromNewPackage(oldPkg, outputPkg); err != nil { + return err } if len(outputPkg.Channels) == 0 { // Remove empty packages. delete(outputModel, outputPkg.Name) } } + + return nil + } + + headsOnlyMode := len(oldModel) == 0 + latestMode := !headsOnlyMode + isInclude := len(g.Includer.Packages) != 0 + + switch { + case !g.IncludeAdditively && isInclude: // Only diff between included objects. + + // Add included packages/channels/bundles from newModel to outputModel. + if err := g.Includer.Run(newModel, outputModel); err != nil { + return nil, err + } + + if latestMode { + if err := latestPruneFromOutput(); err != nil { + return nil, err + } + } + + case isInclude: // Add included objects to outputModel. + + // Add included packages/channels/bundles from newModel to outputModel. + if err := g.Includer.Run(newModel, outputModel); err != nil { + return nil, err + } + + fallthrough + default: + + if headsOnlyMode { // Net-new diff of heads only. + + // Make shallow copies of packages and channels that are only + // filled with channel heads. + for _, newPkg := range newModel { + // This package may have been created in the include step. + outputPkg, pkgIncluded := outputModel[newPkg.Name] + if !pkgIncluded { + outputPkg = copyPackageNoChannels(newPkg) + outputModel[outputPkg.Name] = outputPkg + } + for _, newCh := range newPkg.Channels { + if _, chIncluded := outputPkg.Channels[newCh.Name]; chIncluded { + // Head (and other bundles) were added in the include step. + continue + } + outputCh := copyChannelNoBundles(newCh, outputPkg) + outputPkg.Channels[outputCh.Name] = outputCh + head, err := newCh.Head() + if err != nil { + return nil, err + } + outputBundle := copyBundle(head, outputCh, outputPkg) + outputModel.AddBundle(*outputBundle) + } + } + + } else { // Diff between old and new Model. + + // Copy newModel to create an output model by deletion, + // which is more succinct than by addition. + for _, newPkg := range newModel { + if _, pkgIncluded := outputModel[newPkg.Name]; pkgIncluded { + // The user has specified the state they want this package to have in the diff + // via an inclusion entry, so the package created above should not be changed. + continue + } + outputModel[newPkg.Name] = copyPackage(newPkg) + } + + if err := latestPruneFromOutput(); err != nil { + return nil, err + } + + } + } if !g.SkipDependencies { @@ -126,8 +164,8 @@ func (g *DiffGenerator) Run(oldModel, newModel model.Model) (model.Model, error) // Default channel may not have been copied, so set it to the new default channel here. for _, outputPkg := range outputModel { - outputHasDefault := false newPkg := newModel[outputPkg.Name] + var outputHasDefault bool outputPkg.DefaultChannel, outputHasDefault = outputPkg.Channels[newPkg.DefaultChannel.Name] if !outputHasDefault { // Create a name-only channel since oldModel contains the channel already. @@ -138,9 +176,9 @@ func (g *DiffGenerator) Run(oldModel, newModel model.Model) (model.Model, error) return outputModel, nil } -// diffPackages removes any bundles and channels from newPkg that +// pruneOldFromNewPackage prune any bundles and channels from newPkg that // are in oldPkg, but not those that differ in any way. -func diffPackages(oldPkg, newPkg *model.Package) error { +func pruneOldFromNewPackage(oldPkg, newPkg *model.Package) error { for _, newCh := range newPkg.Channels { oldCh, oldHasCh := oldPkg.Channels[newCh.Name] if !oldHasCh { diff --git a/alpha/declcfg/diff_test.go b/alpha/declcfg/diff_test.go index 51f92a908..c644d4d2b 100644 --- a/alpha/declcfg/diff_test.go +++ b/alpha/declcfg/diff_test.go @@ -3,6 +3,7 @@ package declcfg import ( "testing" + "github.com/blang/semver" "github.com/stretchr/testify/require" "github.com/operator-framework/operator-registry/alpha/model" @@ -991,6 +992,246 @@ func TestDiffLatest(t *testing.T) { }, }, }, + { + name: "HasDiff/IncludePackage", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{{Name: "foo.v0.1.0"}}}, + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{{Name: "bar.v0.1.0"}}}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.1.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.1.0")}, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{{Name: "foo.v0.1.0"}}}, + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, {Name: "bar.v0.2.0", Replaces: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.1.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.2.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.2.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{{Name: "bar"}}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.2.0", Replaces: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "bar.v0.2.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.2.0")}, + }, + }, + }, + }, + { + name: "HasDiff/IncludeChannel", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{{Name: "foo.v0.1.0"}}}, + {Schema: schemaChannel, Name: "alpha", Package: "foo", Entries: []ChannelEntry{{Name: "foo.v0.1.0-alpha.0"}}}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-alpha.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0-alpha.0")}, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "alpha"}, // Make sure the default channel is still updated. + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.2.0", Replaces: "foo.v0.1.0"}}, + }, + {Schema: schemaChannel, Name: "alpha", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0-alpha.0"}, {Name: "foo.v0.2.0-alpha.0", Replaces: "foo.v0.1.0-alpha.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-alpha.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0-alpha.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0-alpha.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{{Name: "foo", Channels: []DiffIncludeChannel{{Name: "stable"}}}}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "alpha"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.2.0", Replaces: "foo.v0.1.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + }, + }, + }, + { + name: "HasDiff/IncludeVersion", + oldCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.2.0", Replaces: "foo.v0.1.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + }, + }, + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.1.1", Replaces: "foo.v0.1.0"}, + {Name: "foo.v0.2.0", Replaces: "foo.v0.1.1"}, {Name: "foo.v0.3.0", Replaces: "foo.v0.2.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.1", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.1")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.3.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.3.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{ + {Name: "foo", Channels: []DiffIncludeChannel{ + {Name: "stable", Versions: []semver.Version{{Major: 0, Minor: 2, Patch: 0}}}}, + }}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.3.0", Replaces: "foo.v0.2.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.3.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.3.0")}, + }, + }, + }, + }, } for _, s := range specs { @@ -1494,6 +1735,561 @@ func TestDiffHeadsOnly(t *testing.T) { }, }, }, + { + name: "HasDiff/IncludeAdditive", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "etcd", Entries: []ChannelEntry{ + {Name: "etcd.v0.9.0"}, + {Name: "etcd.v0.9.1", Replaces: "etcd.v0.9.0"}, + {Name: "etcd.v0.9.2", Replaces: "etcd.v0.9.1"}, + {Name: "etcd.v0.9.3", Replaces: "etcd.v0.9.2"}, + {Name: "etcd.v1.0.0", Replaces: "etcd.v0.9.3", Skips: []string{"etcd.v0.9.1", "etcd.v0.9.2", "etcd.v0.9.3"}}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildPackageRequired("etcd", "<0.9.2"), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "bar.v0.1.0", + Package: "bar", + Image: "reg/bar:latest", + Properties: []property.Property{ + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("bar", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.3", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.3"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v1.0.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("etcd", "1.0.0"), + }, + }, + }, + }, + g: &DiffGenerator{ + IncludeAdditively: true, + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{ + { + Name: "etcd", + Channels: []DiffIncludeChannel{{ + Name: "stable", + Versions: []semver.Version{{Major: 0, Minor: 9, Patch: 2}}}, + }}, + { + Name: "bar", + Channels: []DiffIncludeChannel{{Name: "stable"}}, + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "etcd", Entries: []ChannelEntry{ + {Name: "etcd.v0.9.1", Replaces: "etcd.v0.9.0"}, + {Name: "etcd.v0.9.2", Replaces: "etcd.v0.9.1"}, + {Name: "etcd.v0.9.3", Replaces: "etcd.v0.9.2"}, + {Name: "etcd.v1.0.0", Replaces: "etcd.v0.9.3", Skips: []string{"etcd.v0.9.1", "etcd.v0.9.2", "etcd.v0.9.3"}}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "bar.v0.1.0", + Package: "bar", + Image: "reg/bar:latest", + Properties: []property.Property{ + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("bar", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.3", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.3"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v1.0.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "1.0.0"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", "<0.9.2"), + }, + }, + }, + }, + }, + { + name: "HasDiff/IncludePackage", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{{Name: "foo.v0.1.0"}}}, + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, {Name: "bar.v0.2.0", Replaces: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.1.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.2.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.2.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{{Name: "bar"}}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, {Name: "bar.v0.2.0", Replaces: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "bar.v0.1.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "bar.v0.2.0", Package: "bar", Image: "reg/bar:latest", + Properties: []property.Property{property.MustBuildPackage("bar", "0.2.0")}, + }, + }, + }, + }, + { + name: "HasDiff/IncludeChannel", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "alpha"}, // Make sure the default channel is still updated. + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.2.0", Replaces: "foo.v0.1.0"}}, + }, + {Schema: schemaChannel, Name: "alpha", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0-alpha.0"}, {Name: "foo.v0.2.0-alpha.0", Replaces: "foo.v0.1.0-alpha.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0-alpha.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0-alpha.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0-alpha.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0-alpha.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{{Name: "foo", Channels: []DiffIncludeChannel{{Name: "stable"}}}}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "alpha"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.2.0", Replaces: "foo.v0.1.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + }, + }, + }, + { + name: "HasDiff/IncludeVersion", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, {Name: "foo.v0.1.1", Replaces: "foo.v0.1.0"}, + {Name: "foo.v0.2.0", Replaces: "foo.v0.1.1"}, {Name: "foo.v0.3.0", Replaces: "foo.v0.2.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.1", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.1.1")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.3.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.3.0")}, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{ + {Name: "foo", Channels: []DiffIncludeChannel{ + {Name: "stable", Versions: []semver.Version{{Major: 0, Minor: 2, Patch: 0}}}}, + }}, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.2.0", Replaces: "foo.v0.1.1"}, {Name: "foo.v0.3.0", Replaces: "foo.v0.2.0"}}, + }, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.2.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.2.0")}, + }, + { + Schema: schemaBundle, + Name: "foo.v0.3.0", Package: "foo", Image: "reg/foo:latest", + Properties: []property.Property{property.MustBuildPackage("foo", "0.3.0")}, + }, + }, + }, + }, + { + name: "HasDiff/IncludeNonAdditive", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "etcd", Entries: []ChannelEntry{ + {Name: "etcd.v0.9.0"}, + {Name: "etcd.v0.9.1", Replaces: "etcd.v0.9.0"}, + {Name: "etcd.v0.9.2", Replaces: "etcd.v0.9.1"}, + {Name: "etcd.v0.9.3", Replaces: "etcd.v0.9.2"}, + {Name: "etcd.v1.0.0", Replaces: "etcd.v0.9.3", Skips: []string{"etcd.v0.9.1", "etcd.v0.9.2", "etcd.v0.9.3"}}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "foo", Entries: []ChannelEntry{ + {Name: "foo.v0.1.0"}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildPackageRequired("etcd", "<0.9.2"), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "bar.v0.1.0", + Package: "bar", + Image: "reg/bar:latest", + Properties: []property.Property{ + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("bar", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.1"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.3", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.3"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v1.0.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("etcd", "1.0.0"), + }, + }, + }, + }, + g: &DiffGenerator{ + Includer: DiffIncluder{ + Packages: []DiffIncludePackage{ + { + Name: "etcd", + Channels: []DiffIncludeChannel{{ + Name: "stable", + Versions: []semver.Version{{Major: 0, Minor: 9, Patch: 3}}}, + }}, + { + Name: "bar", + Channels: []DiffIncludeChannel{{Name: "stable"}}, + }, + }, + }, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "bar", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + }, + Channels: []Channel{ + {Schema: schemaChannel, Name: "stable", Package: "bar", Entries: []ChannelEntry{ + {Name: "bar.v0.1.0"}, + }}, + {Schema: schemaChannel, Name: "stable", Package: "etcd", Entries: []ChannelEntry{ + {Name: "etcd.v0.9.3", Replaces: "etcd.v0.9.2"}, + {Name: "etcd.v1.0.0", Replaces: "etcd.v0.9.3", Skips: []string{"etcd.v0.9.1", "etcd.v0.9.2", "etcd.v0.9.3"}}, + }}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "bar.v0.1.0", + Package: "bar", + Image: "reg/bar:latest", + Properties: []property.Property{ + property.MustBuildGVKRequired("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildPackage("bar", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.3", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.3"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v1.0.0", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildGVK("etcd.database.coreos.com", "v1", "EtcdBackup"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "1.0.0"), + }, + }, + }, + }, + }, } for _, s := range specs { diff --git a/cmd/opm/alpha/diff/cmd.go b/cmd/opm/alpha/diff/cmd.go index 5cf91892e..752e4d343 100644 --- a/cmd/opm/alpha/diff/cmd.go +++ b/cmd/opm/alpha/diff/cmd.go @@ -24,10 +24,11 @@ const ( ) type diff struct { - oldRefs []string - newRefs []string - skipDeps bool - includeFile string + oldRefs []string + newRefs []string + skipDeps bool + includeAdditive bool + includeFile string output string caFile string @@ -36,6 +37,18 @@ type diff struct { logger *logrus.Entry } +// Example include file needs to be formatted separately so indentation is not messed up. +var includeFileExample = fmt.Sprintf(`packages: +%[1]s- name: foo +%[1]s- name: bar +%[1]s channels: +%[1]s - name: stable +%[1]s- name: baz +%[1]s channels: +%[1]s - name: alpha +%[1]s versions: +%[1]s - 0.2.0-alpha.0`, templates.Indentation) + func NewCmd() *cobra.Command { a := diff{ logger: logrus.NewEntry(logrus.New()), @@ -44,27 +57,28 @@ func NewCmd() *cobra.Command { Use: "diff [old-refs]... new-refs...", Short: "Diff old and new catalog references into a declarative config", Long: templates.LongDesc(` -Diff a set of old and new catalog references ("refs") to produce a -declarative config containing only packages channels, and versions not present -in the old set, and versions that differ between the old and new sets. This is known as "latest" mode. - -These references are passed through 'opm render' to produce a single declarative config. -Bundle image refs are not supported directly; a valid "olm.package" declarative config object -referring to the bundle's package must exist in all input refs. - -This command has special behavior when old-refs are omitted, called "heads-only" mode: -instead of the output being that of 'opm render refs...' -(which would be the case given the preceding behavior description), -only the channel heads of all channels in all packages are included in the output, -and dependencies. Dependencies are assumed to be provided by either an old ref, -in which case they are not included in the diff, or a new ref, in which -case they are included. Dependencies provided by some catalog unknown to -'opm alpha diff' will not cause the command to error, but an error will occur -if that catalog is not serving these dependencies at runtime. -Dependency inclusion can be turned off with --no-deps, although this is not recommended +'diff' returns a declarative config containing packages, channels, and versions +from new-refs, optionally removing those in old-refs or those omitted by an include config file. + +Each set of refs is passed to 'opm render ' to produce a single, normalized delcarative config. + +Depending on what arguments are provided to the command, a particular "mode" is invoked to produce a diff: + +- If in heads-only mode (old-refs is not specified), then the heads of channels in new-refs are added to the output. +- If in latest mode (old-refs is specified), a diff between old-refs and new-refs is added to the output. +- If --include-file is set, items from that file will be added to the diff: + - If --include-additive is false (the default), a diff will be generated only on those objects, depending on the mode. + - If --include-additive is true, the diff will contain included objects, plus those added by the mode's invocation. + +Dependencies are added in all modes if --skip-deps is false (the default). +Dependencies are assumed to be provided by either an old-ref, in which case they are not included in the diff, +or a new-ref, in which case they are included. +Dependencies provided by some catalog unknown to 'diff' will not cause the command to error, +but an error will occur if that catalog is not serving these dependencies at runtime. +While dependency inclusion can be turned off with --skip-deps, doing so is not recommended unless you are certain some in-cluster catalog satisfies all dependencies. `), - Example: templates.Examples(` + Example: fmt.Sprintf(templates.Examples(` # Create a directory for your declarative config diff. mkdir -p my-catalog-index @@ -78,21 +92,18 @@ opm alpha diff registry.org/my-catalog:abc123 registry.org/my-catalog:def456 -o opm alpha diff registry.org/my-catalog:def456 -o yaml > my-catalog-index/index.yaml # OR: -# Create a heads-only catalog, but include all of package "foo", package "bar" channel "stable", +# Only include all of package "foo", package "bar" channel "stable", # and package "baz" channel "alpha" version "0.2.0-alpha.0" (and its upgrade graph) in the diff. cat < include.yaml -packages: -- name: foo -- name: bar - channels: - - name: stable -- name: baz - channels: - - name: alpha - versions: - - 0.2.0-alpha.0 +%s EOF -opm alpha diff registry.org/my-catalog:def456 -i include.yaml -o yaml > my-catalog-index/index.yaml +opm alpha diff registry.org/my-catalog:def456 -i include.yaml -o yaml > pruned-index/index.yaml + +# OR: +# Include all of package "foo", package "bar" channel "stable", +# and package "baz" channel "alpha" version "0.2.0-alpha.0" in the diff +# on top of heads of all other channels in all packages (using the above include.yaml). +opm alpha diff registry.org/my-catalog:def456 -i include.yaml --include-additive -o yaml > pruned-index/index.yaml # FINALLY: # Build an index image containing the diff-ed declarative config, @@ -100,7 +111,7 @@ opm alpha diff registry.org/my-catalog:def456 -i include.yaml -o yaml > my-catal opm alpha generate dockerfile ./my-catalog-index docker build -t registry.org/my-catalog:diff-latest -f index.Dockerfile . docker push registry.org/my-catalog:diff-latest -`), +`), includeFileExample), Args: cobra.RangeArgs(1, 2), PreRunE: func(cmd *cobra.Command, args []string) error { if a.debug { @@ -116,9 +127,11 @@ docker push registry.org/my-catalog:diff-latest cmd.Flags().StringVarP(&a.output, "output", "o", "yaml", "Output format (json|yaml)") cmd.Flags().StringVar(&a.caFile, "ca-file", "", "the root Certificates to use with this command") - cmd.Flags().StringVarP(&a.includeFile, "include-file", "i", "", "YAML defining packages, "+ - "channels, and/or bundles/versions to include in the diff from the new refs. Upgrade graphs "+ - "from individual bundles/versions to their channel's head are added to the diff") + cmd.Flags().StringVarP(&a.includeFile, "include-file", "i", "", + "YAML defining packages, channels, and/or bundles/versions to extract from the new refs. "+ + "Upgrade graphs from individual bundles/versions to their channel's head are also included") + cmd.Flags().BoolVar(&a.includeAdditive, "include-additive", false, + "Ref objects from --include-file are returned on top of 'heads-only' or 'latest' output") cmd.Flags().BoolVar(&a.debug, "debug", false, "enable debug logging") return cmd @@ -127,9 +140,8 @@ docker push registry.org/my-catalog:diff-latest func (a *diff) addFunc(cmd *cobra.Command, args []string) error { a.parseArgs(args) - skipTLS, err := cmd.Flags().GetBool("skip-tls") - if err != nil { - logrus.Panic(err) + if cmd.Flags().Changed("include-additive") && a.includeFile == "" { + a.logger.Fatal("must set --include-file if --include-additive is set") } var write func(declcfg.DeclarativeConfig, io.Writer) error @@ -142,6 +154,10 @@ func (a *diff) addFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid --output value: %q", a.output) } + skipTLS, err := cmd.Flags().GetBool("skip-tls") + if err != nil { + logrus.Panic(err) + } rootCAs, err := certs.RootCAs(a.caFile) if err != nil { a.logger.Fatalf("error getting root CAs: %v", err) @@ -157,11 +173,12 @@ func (a *diff) addFunc(cmd *cobra.Command, args []string) error { }() diff := action.Diff{ - Registry: reg, - OldRefs: a.oldRefs, - NewRefs: a.newRefs, - SkipDependencies: a.skipDeps, - Logger: a.logger, + Registry: reg, + OldRefs: a.oldRefs, + NewRefs: a.newRefs, + SkipDependencies: a.skipDeps, + IncludeAdditively: a.includeAdditive, + Logger: a.logger, } if a.includeFile != "" {