diff --git a/cmd/opm/alpha/diff/cmd.go b/cmd/opm/alpha/diff/cmd.go index 5da7e9288..eb0726296 100644 --- a/cmd/opm/alpha/diff/cmd.go +++ b/cmd/opm/alpha/diff/cmd.go @@ -24,8 +24,9 @@ const ( ) type diff struct { - oldRefs []string - newRefs []string + oldRefs []string + newRefs []string + skipDeps bool output string caFile string @@ -59,6 +60,8 @@ 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 +unless you are certain some in-cluster catalog satisfies all dependencies. NOTE: for now, if any dependency exists, the entire dependency's package is added to the diff. In the future, these packages will be pruned such that only the latest dependencies @@ -66,19 +69,24 @@ satisfying a package version range or GVK, and their upgrade graph(s) to their l channel head(s), are included in the diff. `), Example: templates.Examples(` -# Diff a catalog at some old state and latest state into a declarative config index. -mkdir -p catalog-index -opm alpha diff registry.org/my-catalog:abc123 registry.org/my-catalog:def456 -o yaml > ./my-catalog-index/index.yaml +# Create a directory for your declarative config diff. +mkdir -p my-catalog-index -# Build and push this index into an index image. -opm alpha generate dockerfile ./my-catalog-index -docker build -t registry.org/my-catalog:latest-abc123-def456 -f index.Dockerfile . -docker push registry.org/my-catalog:latest-abc123-def456 +# THEN: +# Create a new catalog from a diff between an old and the latest +# state of a catalog as a declarative config index. +opm alpha diff registry.org/my-catalog:abc123 registry.org/my-catalog:def456 -o yaml > ./my-catalog-index/index.yaml -# Create a new catalog from the heads of an existing catalog, then build and push the image like above. +# OR: +# Create a new catalog from the heads of an existing catalog. opm alpha diff registry.org/my-catalog:def456 -o yaml > my-catalog-index/index.yaml -docker build -t registry.org/my-catalog:headsonly-def456 -f index.Dockerfile . -docker push registry.org/my-catalog:headsonly-def456 + +# FINALLY: +# Build an index image containing the diff-ed declarative config, +# then tag and push it. +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 `), Args: cobra.RangeArgs(1, 2), PreRunE: func(cmd *cobra.Command, args []string) error { @@ -91,6 +99,8 @@ docker push registry.org/my-catalog:headsonly-def456 RunE: a.addFunc, } + cmd.Flags().BoolVar(&a.skipDeps, "skip-deps", false, "do not include bundle dependencies in the output catalog") + cmd.Flags().StringVarP(&a.output, "output", "o", "yaml", "Output format (json|yaml)") cmd.Flags().StringVarP(&a.caFile, "ca-file", "", "", "the root Certificates to use with this command") @@ -103,7 +113,7 @@ func (a *diff) addFunc(cmd *cobra.Command, args []string) error { skipTLS, err := cmd.Flags().GetBool("skip-tls") if err != nil { - panic(err) + logrus.Panic(err) } var write func(declcfg.DeclarativeConfig, io.Writer) error @@ -134,10 +144,11 @@ func (a *diff) addFunc(cmd *cobra.Command, args []string) error { defer cancel() diff := action.Diff{ - Registry: reg, - OldRefs: a.oldRefs, - NewRefs: a.newRefs, - Logger: a.logger, + Registry: reg, + OldRefs: a.oldRefs, + NewRefs: a.newRefs, + SkipDependencies: a.skipDeps, + Logger: a.logger, } cfg, err := diff.Run(ctx) if err != nil { @@ -159,7 +170,7 @@ func (a *diff) parseArgs(args []string) { case 2: old, new = args[0], args[1] default: - panic("should never be here, CLI must enforce arg size") + logrus.Panic("should never be here, CLI must enforce arg size") } if old != "" { a.oldRefs = strings.Split(old, ",") diff --git a/internal/action/diff.go b/internal/action/diff.go index 6b022a137..7dd192f60 100644 --- a/internal/action/diff.go +++ b/internal/action/diff.go @@ -17,6 +17,9 @@ type Diff struct { OldRefs []string NewRefs []string + // SkipDependencies directs Run() to not include dependencies + // of bundles included in the diff if true. + SkipDependencies bool Logger *logrus.Entry } @@ -59,7 +62,11 @@ func (a Diff) Run(ctx context.Context) (*declcfg.DeclarativeConfig, error) { return nil, fmt.Errorf("error converting new declarative config to model: %v", err) } - diffModel, err := declcfg.Diff(oldModel, newModel) + g := &declcfg.DiffGenerator{ + Logger: a.Logger, + SkipDependencies: a.SkipDependencies, + } + diffModel, err := g.Run(oldModel, newModel) if err != nil { return nil, fmt.Errorf("error generating diff: %v", err) } diff --git a/internal/declcfg/diff.go b/internal/declcfg/diff.go index f1fb9259b..8f1cf6b41 100644 --- a/internal/declcfg/diff.go +++ b/internal/declcfg/diff.go @@ -3,19 +3,41 @@ package declcfg import ( "reflect" "sort" + "sync" "github.com/blang/semver" "github.com/mitchellh/hashstructure/v2" + "github.com/sirupsen/logrus" "github.com/operator-framework/operator-registry/internal/model" "github.com/operator-framework/operator-registry/internal/property" ) -// Diff returns a Model containing everything in newModel not in oldModel, +// DiffGenerator configures how diffs are created via Run(). +type DiffGenerator struct { + Logger *logrus.Entry + + // SkipDependencies directs Run() to not include dependencies + // of bundles included in the diff if true. + SkipDependencies bool + + initOnce sync.Once +} + +func (g *DiffGenerator) init() { + g.initOnce.Do(func() { + if g.Logger == nil { + g.Logger = &logrus.Entry{} + } + }) +} + +// 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. -func Diff(oldModel, newModel model.Model) (model.Model, error) { +func (g *DiffGenerator) Run(oldModel, newModel model.Model) (model.Model, error) { + g.init() // TODO(estroz): loading both oldModel and newModel into memory may // exceed process/hardware limits. Instead, store models on-disk then @@ -71,9 +93,11 @@ func Diff(oldModel, newModel model.Model) (model.Model, error) { } } - // Add dependencies to outputModel not already present in oldModel. - if err := addAllDependencies(newModel, oldModel, outputModel); err != nil { - return nil, err + if !g.SkipDependencies { + // Add dependencies to outputModel not already present in oldModel. + if err := addAllDependencies(newModel, oldModel, outputModel); err != nil { + return nil, err + } } // Default channel may not have been copied, so set it to the new default channel here. diff --git a/internal/declcfg/diff_test.go b/internal/declcfg/diff_test.go index c155b8c80..fee8b01b2 100644 --- a/internal/declcfg/diff_test.go +++ b/internal/declcfg/diff_test.go @@ -20,6 +20,7 @@ func init() { func TestDiffLatest(t *testing.T) { type spec struct { name string + g *DiffGenerator oldCfg DeclarativeConfig newCfg DeclarativeConfig expCfg DeclarativeConfig @@ -31,6 +32,7 @@ func TestDiffLatest(t *testing.T) { name: "NoDiff/Empty", oldCfg: DeclarativeConfig{}, newCfg: DeclarativeConfig{}, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{}, }, { @@ -69,6 +71,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{}, }, { @@ -107,6 +110,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{}, }, { @@ -146,6 +150,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, @@ -263,6 +268,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, @@ -362,6 +368,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, @@ -429,6 +436,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -517,6 +525,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -617,6 +626,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -704,6 +714,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -804,6 +815,7 @@ func TestDiffLatest(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -849,7 +861,7 @@ func TestDiffLatest(t *testing.T) { newModel, err := ConvertToModel(s.newCfg) require.NoError(t, err) - outputModel, err := Diff(oldModel, newModel) + outputModel, err := s.g.Run(oldModel, newModel) s.assertion(t, err) outputCfg := ConvertFromModel(outputModel) @@ -861,6 +873,7 @@ func TestDiffLatest(t *testing.T) { func TestDiffHeadsOnly(t *testing.T) { type spec struct { name string + g *DiffGenerator newCfg DeclarativeConfig expCfg DeclarativeConfig assertion require.ErrorAssertionFunc @@ -870,6 +883,7 @@ func TestDiffHeadsOnly(t *testing.T) { { name: "NoDiff/Empty", newCfg: DeclarativeConfig{}, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{}, }, { @@ -891,6 +905,7 @@ func TestDiffHeadsOnly(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, @@ -990,6 +1005,7 @@ func TestDiffHeadsOnly(t *testing.T) { }, }, }, + g: &DiffGenerator{}, expCfg: DeclarativeConfig{ Packages: []Package{ {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, @@ -1030,6 +1046,85 @@ func TestDiffHeadsOnly(t *testing.T) { }, }, }, + { + // Testing SkipDependencies only really makes sense in heads-only mode, + // since new dependencies are always added. + name: "HasDiff/SkipDependencies", + newCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackageRequired("etcd", "<=0.9.1"), + property.MustBuildPackage("foo", "0.1.0"), + }, + }, + { + Schema: schemaBundle, + Name: "etcd.v0.9.1", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + 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.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + }, + }, + g: &DiffGenerator{ + SkipDependencies: true, + }, + expCfg: DeclarativeConfig{ + Packages: []Package{ + {Schema: schemaPackage, Name: "etcd", DefaultChannel: "stable"}, + {Schema: schemaPackage, Name: "foo", DefaultChannel: "stable"}, + }, + Bundles: []Bundle{ + { + Schema: schemaBundle, + Name: "etcd.v0.9.2", + Package: "etcd", + Image: "reg/etcd:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", "etcd.v0.9.1"), + property.MustBuildGVK("etcd.database.coreos.com", "v1beta2", "EtcdBackup"), + property.MustBuildPackage("etcd", "0.9.2"), + }, + }, + { + Schema: schemaBundle, + Name: "foo.v0.1.0", + Package: "foo", + Image: "reg/foo:latest", + Properties: []property.Property{ + property.MustBuildChannel("stable", ""), + property.MustBuildPackage("foo", "0.1.0"), + property.MustBuildPackageRequired("etcd", "<=0.9.1"), + }, + }, + }, + }, + }, } for _, s := range specs { @@ -1041,7 +1136,7 @@ func TestDiffHeadsOnly(t *testing.T) { newModel, err := ConvertToModel(s.newCfg) require.NoError(t, err) - outputModel, err := Diff(model.Model{}, newModel) + outputModel, err := s.g.Run(model.Model{}, newModel) s.assertion(t, err) outputCfg := ConvertFromModel(outputModel)