From 86be58173e8dc7878efde88fea50bbce6a80ddee Mon Sep 17 00:00:00 2001 From: Rashmi Gottipati Date: Thu, 17 Apr 2025 13:25:18 -0400 Subject: [PATCH] Add command for updating olmv1 catalog Signed-off-by: Rashmi Gottipati --- internal/cmd/internal/olmv1/catalog_update.go | 40 ++++++++ internal/cmd/olmv1.go | 1 + internal/pkg/v1/action/action_suite_test.go | 40 ++++++++ internal/pkg/v1/action/catalog_update.go | 84 +++++++++++++++++ internal/pkg/v1/action/catalog_update_test.go | 91 +++++++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 internal/cmd/internal/olmv1/catalog_update.go create mode 100644 internal/pkg/v1/action/catalog_update.go create mode 100644 internal/pkg/v1/action/catalog_update_test.go diff --git a/internal/cmd/internal/olmv1/catalog_update.go b/internal/cmd/internal/olmv1/catalog_update.go new file mode 100644 index 00000000..b74687a6 --- /dev/null +++ b/internal/cmd/internal/olmv1/catalog_update.go @@ -0,0 +1,40 @@ +package olmv1 + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/operator-framework/kubectl-operator/internal/cmd/internal/log" + v1action "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action" + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +// NewCatalogUpdateCmd allows updating a selected clustercatalog +func NewCatalogUpdateCmd(cfg *action.Configuration) *cobra.Command { + i := v1action.NewCatalogUpdate(cfg) + i.Logf = log.Printf + + cmd := &cobra.Command{ + Use: "catalog ", + Short: "Update a catalog", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + i.CatalogName = args[0] + _, err := i.Run(cmd.Context()) + if err != nil { + log.Fatalf("failed to update catalog: %v", err) + } + log.Printf("catalog %q updated", i.CatalogName) + }, + } + bindCatalogUpdateFlags(cmd.Flags(), i) + + return cmd +} + +func bindCatalogUpdateFlags(fs *pflag.FlagSet, i *v1action.CatalogUpdate) { + fs.Int32Var(&i.Priority, "priority", 0, "priority determines the likelihood of a catalog being selected in conflict scenarios") + fs.IntVar(&i.PollIntervalMinutes, "source-poll-interval-minutes", 5, "catalog source polling interval [in minutes]") + fs.StringToStringVar(&i.Labels, "labels", map[string]string{}, "labels that will be added to the catalog") + fs.StringVar(&i.AvailabilityMode, "availability-mode", "", "available means that the catalog should be active and serving data") +} diff --git a/internal/cmd/olmv1.go b/internal/cmd/olmv1.go index fad8b763..0d53e7d3 100644 --- a/internal/cmd/olmv1.go +++ b/internal/cmd/olmv1.go @@ -48,6 +48,7 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command { } updateCmd.AddCommand( olmv1.NewExtensionUpdateCmd(cfg), + olmv1.NewCatalogUpdateCmd(cfg), ) installCmd := &cobra.Command{ diff --git a/internal/pkg/v1/action/action_suite_test.go b/internal/pkg/v1/action/action_suite_test.go index d7e5e570..2904ff37 100644 --- a/internal/pkg/v1/action/action_suite_test.go +++ b/internal/pkg/v1/action/action_suite_test.go @@ -110,6 +110,8 @@ func newClusterCatalog(name string) *olmv1.ClusterCatalog { type extensionOpt func(*olmv1.ClusterExtension) +type catalogOpt func(*olmv1.ClusterCatalog) + func withVersion(version string) extensionOpt { return func(ext *olmv1.ClusterExtension) { ext.Spec.Source.Catalog.Version = version @@ -122,6 +124,28 @@ func withSourceType(sourceType string) extensionOpt { } } +func withCatalogSourceType(sourceType olmv1.SourceType) catalogOpt { + return func(catalog *olmv1.ClusterCatalog) { + catalog.Spec.Source.Type = sourceType + } +} + +func withCatalogSourcePriority(priority int32) catalogOpt { + return func(catalog *olmv1.ClusterCatalog) { + catalog.Spec.Priority = int32(priority) + } +} + +func withCatalogPollInterval(pollInterval int, ref string) catalogOpt { + return func(catalog *olmv1.ClusterCatalog) { + if catalog.Spec.Source.Image == nil { + catalog.Spec.Source.Image = &olmv1.ImageSource{} + } + catalog.Spec.Source.Image.Ref = ref + catalog.Spec.Source.Image.PollIntervalMinutes = &pollInterval + } +} + // nolint: unparam func withConstraintPolicy(policy string) extensionOpt { return func(ext *olmv1.ClusterExtension) { @@ -157,6 +181,22 @@ func buildExtension(packageName string, opts ...extensionOpt) *olmv1.ClusterExte return ext } +func buildCatalog(catalogName string, opts ...catalogOpt) *olmv1.ClusterCatalog { + catalog := &olmv1.ClusterCatalog{ + Spec: olmv1.ClusterCatalogSpec{ + Source: olmv1.CatalogSource{ + Type: olmv1.SourceTypeImage, + }, + }, + } + catalog.SetName(catalogName) + for _, opt := range opts { + opt(catalog) + } + + return catalog +} + func updateExtensionConditionStatus(name string, cl client.Client, typ string, status metav1.ConditionStatus) error { var ext olmv1.ClusterExtension key := types.NamespacedName{Name: name} diff --git a/internal/pkg/v1/action/catalog_update.go b/internal/pkg/v1/action/catalog_update.go new file mode 100644 index 00000000..90a17f47 --- /dev/null +++ b/internal/pkg/v1/action/catalog_update.go @@ -0,0 +1,84 @@ +package action + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + + olmv1 "github.com/operator-framework/operator-controller/api/v1" + + "github.com/operator-framework/kubectl-operator/pkg/action" +) + +type CatalogUpdate struct { + config *action.Configuration + CatalogName string + + Priority int32 + PollIntervalMinutes int + Labels map[string]string + AvailabilityMode string + + Logf func(string, ...interface{}) +} + +func NewCatalogUpdate(config *action.Configuration) *CatalogUpdate { + return &CatalogUpdate{ + config: config, + Logf: func(string, ...interface{}) {}, + } +} + +func (cu *CatalogUpdate) Run(ctx context.Context) (*olmv1.ClusterCatalog, error) { + var catalog olmv1.ClusterCatalog + var err error + + cuKey := types.NamespacedName{ + Name: cu.CatalogName, + Namespace: cu.config.Namespace, + } + if err = cu.config.Client.Get(ctx, cuKey, &catalog); err != nil { + return nil, err + } + + if catalog.Spec.Source.Type != olmv1.SourceTypeImage { + return nil, fmt.Errorf("unrecognized source type: %q", catalog.Spec.Source.Type) + } + + cu.setDefaults(catalog) + + cu.setUpdatedCatalog(&catalog) + if err := cu.config.Client.Update(ctx, &catalog); err != nil { + return nil, err + } + + return &catalog, nil +} + +func (cu *CatalogUpdate) setUpdatedCatalog(catalog *olmv1.ClusterCatalog) { + catalog.SetLabels(cu.Labels) + catalog.Spec.Priority = cu.Priority + if catalog.Spec.Source.Image != nil && catalog.Spec.Source.Image.PollIntervalMinutes != nil { + catalog.Spec.Source.Image.PollIntervalMinutes = &cu.PollIntervalMinutes + } + catalog.Spec.AvailabilityMode = olmv1.AvailabilityMode(cu.AvailabilityMode) +} + +func (cu *CatalogUpdate) setDefaults(catalog olmv1.ClusterCatalog) { + catalogSrc := catalog.Spec.Source + if catalogSrc.Image != nil && catalogSrc.Image.PollIntervalMinutes != nil { + if cu.PollIntervalMinutes == 0 { + cu.PollIntervalMinutes = *catalogSrc.Image.PollIntervalMinutes + } + } + if cu.AvailabilityMode == "" { + cu.AvailabilityMode = string(catalog.Spec.AvailabilityMode) + } + if cu.Priority == 0 { + cu.Priority = catalog.Spec.Priority + } + if len(cu.Labels) == 0 { + cu.Labels = catalog.Labels + } +} diff --git a/internal/pkg/v1/action/catalog_update_test.go b/internal/pkg/v1/action/catalog_update_test.go new file mode 100644 index 00000000..5e2d1af0 --- /dev/null +++ b/internal/pkg/v1/action/catalog_update_test.go @@ -0,0 +1,91 @@ +package action_test + +import ( + "context" + "maps" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + internalaction "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action" + "github.com/operator-framework/kubectl-operator/pkg/action" + olmv1 "github.com/operator-framework/operator-controller/api/v1" +) + +var _ = Describe("CatalogUpdate", func() { + setupEnv := func(catalogs ...client.Object) action.Configuration { + var cfg action.Configuration + + sch, err := action.NewScheme() + Expect(err).To(BeNil()) + + cl := fake.NewClientBuilder(). + WithObjects(catalogs...). + WithScheme(sch). + Build() + cfg.Scheme = sch + cfg.Client = cl + + return cfg + } + + It("fails finding existing catalog", func() { + cfg := setupEnv() + + updater := internalaction.NewCatalogUpdate(&cfg) + updater.CatalogName = "does-not-exist" + cat, err := updater.Run(context.TODO()) + + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("not found")) + Expect(cat).To(BeNil()) + }) + + It("fails to handle catalog with unknown source type", func() { + cfg := setupEnv(buildCatalog("test", withCatalogSourceType("invalid-type"))) + + updater := internalaction.NewCatalogUpdate(&cfg) + updater.CatalogName = "test" + _, err := updater.Run(context.TODO()) + + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("unrecognized source type")) + }) + + It("successfully updates catalog", func() { + testCatalog := buildCatalog( + "testCatalog", + withCatalogSourceType(olmv1.SourceTypeImage), + withCatalogPollInterval(5, "testCatalog"), + withCatalogSourcePriority(1), + ) + cfg := setupEnv(testCatalog) + + updater := internalaction.NewCatalogUpdate(&cfg) + updater.CatalogName = "testCatalog" + updater.Priority = int32(1) + updater.Labels = map[string]string{"c": "d"} + updater.AvailabilityMode = string(olmv1.AvailabilityModeAvailable) + updater.PollIntervalMinutes = int(5) + catalog, err := updater.Run(context.TODO()) + + Expect(err).To(BeNil()) + Expect(testCatalog).NotTo(BeNil()) + Expect(maps.Equal(catalog.Labels, updater.Labels)).To(BeTrue()) + Expect(catalog.Spec.Priority).To(Equal(updater.Priority)) + Expect(catalog.Spec.Source.Image.PollIntervalMinutes).ToNot(BeNil()) + Expect(*catalog.Spec.Source.Image.PollIntervalMinutes).To(Equal(int(5))) + Expect(catalog.Spec.AvailabilityMode). + To(Equal(olmv1.AvailabilityMode(updater.AvailabilityMode))) + + }) +}) + +func TestCatalogUpdate(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Catalog Update Suite") +}