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
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ go 1.23.0
toolchain go1.23.4

require (
github.com/blang/semver/v4 v4.0.0
github.com/containerd/containerd v1.7.25
github.com/containerd/platforms v0.2.1
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.36.2
github.com/opencontainers/image-spec v1.1.0
github.com/operator-framework/api v0.29.0
github.com/operator-framework/catalogd v1.1.0
github.com/operator-framework/operator-controller v1.1.0
github.com/operator-framework/operator-lifecycle-manager v0.23.1
github.com/operator-framework/operator-registry v1.50.0
Expand All @@ -32,7 +34,6 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.12.9 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/containerd/cgroups/v3 v3.0.3 // indirect
github.com/containerd/containerd/api v1.8.0 // indirect
github.com/containerd/continuity v0.4.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/operator-framework/api v0.29.0 h1:TxAR8RCO+I4FjRrY4PSMgnlmbxNWeD8pzHXp7xwHNmw=
github.com/operator-framework/api v0.29.0/go.mod h1:0whQE4mpMDd2zyHkQe+bFa3DLoRs6oGWCbu8dY/3pyc=
github.com/operator-framework/catalogd v1.1.0 h1:mu2DYL5mpREEAAP+uPG+CMSsfsJkgrIasgLRG8nvwJg=
github.com/operator-framework/catalogd v1.1.0/go.mod h1:8Je9CqMPwhNgRoqGX5OPsLYHsEoTDvPnELLLKRw1RHE=
github.com/operator-framework/operator-controller v1.1.0 h1:h0b1SSuv9ZiIgI8dTuutSPVL4uIeyvTW3gOB2szkBMQ=
github.com/operator-framework/operator-controller v1.1.0/go.mod h1:dJIt5/gfm1n3y9IeX4kpSlpu4CFq8WFVHU2n9ZDVUkA=
github.com/operator-framework/operator-lifecycle-manager v0.23.1 h1:Xw2ml1T4W2ieoFaVwanW/eFlZ11yAOJZUpUI8RLSql8=
Expand Down
13 changes: 13 additions & 0 deletions internal/cmd/internal/olmv1/action_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package olmv1

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestCommand(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Internal action Suite")
}
38 changes: 38 additions & 0 deletions internal/cmd/internal/olmv1/catalog_installed_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package olmv1

import (
"github.com/spf13/cobra"

"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"
)

// NewCatalogInstalledGetCmd handles get commands in the form of:
// catalog(s) [catalog_name] - this will either list all the installed operators
// if no catalog_name has been provided or display the details of the specific
// one otherwise
func NewCatalogInstalledGetCmd(cfg *action.Configuration) *cobra.Command {
i := v1action.NewCatalogInstalledGet(cfg)
i.Logf = log.Printf

cmd := &cobra.Command{
Use: "catalog [catalog_name]",
Aliases: []string{"catalogs"},
Args: cobra.RangeArgs(0, 1),
Short: "Display one or many installed catalogs",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 1 {
i.CatalogName = args[0]
}
installedCatalogs, err := i.Run(cmd.Context())
if err != nil {
log.Fatalf("failed getting installed catalog(s): %v", err)
}

printFormattedCatalogs(installedCatalogs...)
},
}

return cmd
}
38 changes: 38 additions & 0 deletions internal/cmd/internal/olmv1/operator_installed_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package olmv1

import (
"github.com/spf13/cobra"

"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"
)

// NewOperatorInstalledGetCmd handles get commands in the form of:
// operator(s) [operator_name] - this will either list all the installed operators
// if no operator_name has been provided or display the details of the specific
// one otherwise
func NewOperatorInstalledGetCmd(cfg *action.Configuration) *cobra.Command {
i := v1action.NewOperatorInstalledGet(cfg)
i.Logf = log.Printf

cmd := &cobra.Command{
Use: "operator [operator_name]",
Aliases: []string{"operators"},
Args: cobra.RangeArgs(0, 1),
Short: "Display one or many installed operators",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 1 {
i.OperatorName = args[0]
}
installedExtensions, err := i.Run(cmd.Context())
if err != nil {
log.Fatalf("failed getting installed operator(s): %v", err)
}

printFormattedOperators(installedExtensions...)
},
}

return cmd
}
90 changes: 90 additions & 0 deletions internal/cmd/internal/olmv1/printing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package olmv1

import (
"cmp"
"fmt"
"os"
"slices"
"text/tabwriter"
"time"

"github.com/blang/semver/v4"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"

catalogdv1 "github.com/operator-framework/catalogd/api/v1"
olmv1 "github.com/operator-framework/operator-controller/api/v1"
)

func printFormattedOperators(extensions ...olmv1.ClusterExtension) {
tw := tabwriter.NewWriter(os.Stdout, 3, 4, 2, ' ', 0)
_, _ = fmt.Fprint(tw, "NAME\tINSTALLED BUNDLE\tVERSION\tSOURCE TYPE\tINSTALLED\tPROGRESSING\tAGE\n")

sortOperators(extensions)
for _, ext := range extensions {
age := time.Since(ext.CreationTimestamp.Time)
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
ext.Name,
ext.Status.Install.Bundle.Name,
ext.Status.Install.Bundle.Version,
ext.Spec.Source.SourceType,
status(ext.Status.Conditions, olmv1.TypeInstalled),
status(ext.Status.Conditions, olmv1.TypeProgressing),
duration.HumanDuration(age),
)
}
_ = tw.Flush()
}

func printFormattedCatalogs(catalogs ...catalogdv1.ClusterCatalog) {
tw := tabwriter.NewWriter(os.Stdout, 3, 4, 2, ' ', 0)
_, _ = fmt.Fprint(tw, "NAME\tAVAILABILITY\tPRIORITY\tLASTUNPACKED\tSERVING\tAGE\n")

sortCatalogs(catalogs)
for _, cat := range catalogs {
age := time.Since(cat.CreationTimestamp.Time)
lastUnpacked := time.Since(cat.Status.LastUnpacked.Time)
_, _ = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\t%s\t%s\n",
cat.Name,
string(cat.Spec.AvailabilityMode),
cat.Spec.Priority,
duration.HumanDuration(lastUnpacked),
status(cat.Status.Conditions, catalogdv1.TypeServing),
duration.HumanDuration(age),
)
}
_ = tw.Flush()
}

// sortOperators sorts operators in place and uses the following sorting order:
// name (asc), version (desc)
func sortOperators(extensions []olmv1.ClusterExtension) {
slices.SortFunc(extensions, func(a, b olmv1.ClusterExtension) int {
return cmp.Or(
cmp.Compare(a.Name, b.Name),
-semver.MustParse(a.Status.Install.Bundle.Version).Compare(semver.MustParse(b.Status.Install.Bundle.Version)),
)
})
}

// sortCatalogs sorts catalogs in place and uses the following sorting order:
// availability (asc), priority (desc), name (asc)
func sortCatalogs(catalogs []catalogdv1.ClusterCatalog) {
slices.SortFunc(catalogs, func(a, b catalogdv1.ClusterCatalog) int {
return cmp.Or(
cmp.Compare(a.Spec.AvailabilityMode, b.Spec.AvailabilityMode),
-cmp.Compare(a.Spec.Priority, b.Spec.Priority),
cmp.Compare(a.Name, b.Name),
)
})
}

func status(conditions []metav1.Condition, typ string) string {
for _, condition := range conditions {
if condition.Type == typ {
return string(condition.Status)
}
}

return "Unknown"
}
68 changes: 68 additions & 0 deletions internal/cmd/internal/olmv1/printing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package olmv1

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

olmv1catalogd "github.com/operator-framework/catalogd/api/v1"
olmv1 "github.com/operator-framework/operator-controller/api/v1"
)

var _ = Describe("SortCatalogs", func() {
It("sorts catalogs in correct order", func() {
catalogs := []olmv1catalogd.ClusterCatalog{
newClusterCatalog("cat-unavailable-0", olmv1catalogd.AvailabilityModeUnavailable, 0),
newClusterCatalog("cat-unavailable-1", olmv1catalogd.AvailabilityModeUnavailable, 1),
newClusterCatalog("cat-available-0", olmv1catalogd.AvailabilityModeAvailable, 0),
newClusterCatalog("cat-available-1", olmv1catalogd.AvailabilityModeAvailable, 1),
}
sortCatalogs(catalogs)

Expect(catalogs[0].Name).To(Equal("cat-available-1"))
Expect(catalogs[1].Name).To(Equal("cat-available-0"))
Expect(catalogs[2].Name).To(Equal("cat-unavailable-1"))
Expect(catalogs[3].Name).To(Equal("cat-unavailable-0"))
})
})

var _ = Describe("SortOperators", func() {
It("sorts operators in correct order", func() {
operators := []olmv1.ClusterExtension{
newClusterExtension("op-1", "1.0.0"),
newClusterExtension("op-1", "1.0.1"),
newClusterExtension("op-1", "1.0.1-rc4"),
newClusterExtension("op-1", "1.0.1-rc2"),
newClusterExtension("op-2", "2.0.0"),
}
sortOperators(operators)

Expect(operators[0].Status.Install.Bundle.Version).To(Equal("1.0.1"))
Expect(operators[1].Status.Install.Bundle.Version).To(Equal("1.0.1-rc4"))
Expect(operators[2].Status.Install.Bundle.Version).To(Equal("1.0.1-rc2"))
Expect(operators[3].Status.Install.Bundle.Version).To(Equal("1.0.0"))
Expect(operators[4].Status.Install.Bundle.Version).To(Equal("2.0.0"))
})
})

func newClusterCatalog(name string, availabilityMode olmv1catalogd.AvailabilityMode, priority int32) olmv1catalogd.ClusterCatalog {
return olmv1catalogd.ClusterCatalog{
ObjectMeta: metav1.ObjectMeta{Name: name},
Spec: olmv1catalogd.ClusterCatalogSpec{AvailabilityMode: availabilityMode, Priority: priority},
}
}

func newClusterExtension(name, version string) olmv1.ClusterExtension {
return olmv1.ClusterExtension{
ObjectMeta: metav1.ObjectMeta{Name: name},
Status: olmv1.ClusterExtensionStatus{
Install: &olmv1.ClusterExtensionInstallStatus{
Bundle: olmv1.BundleMetadata{
Name: name,
Version: version,
},
},
},
}
}
11 changes: 11 additions & 0 deletions internal/cmd/olmv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
Long: "Manage operators via OLMv1 in a cluster from the command line.",
}

getCmd := &cobra.Command{
Use: "get",
Short: "Display one or many OLMv1-specific resource(s)",
Long: "Display one or many OLMv1-specific resource(s)",
}
getCmd.AddCommand(
olmv1.NewOperatorInstalledGetCmd(cfg),
olmv1.NewCatalogInstalledGetCmd(cfg),
)

cmd.AddCommand(
olmv1.NewOperatorInstallCmd(cfg),
olmv1.NewOperatorUninstallCmd(cfg),
getCmd,
)

return cmd
Expand Down
13 changes: 13 additions & 0 deletions internal/pkg/v1/action/action_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package action_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestCommand(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Internal action Suite")
}
47 changes: 47 additions & 0 deletions internal/pkg/v1/action/catalog_installed_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package action

import (
"context"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

olmv1catalogd "github.com/operator-framework/catalogd/api/v1"

"github.com/operator-framework/kubectl-operator/pkg/action"
)

type CatalogInstalledGet struct {
config *action.Configuration
CatalogName string

Logf func(string, ...interface{})
}

func NewCatalogInstalledGet(cfg *action.Configuration) *CatalogInstalledGet {
return &CatalogInstalledGet{
config: cfg,
Logf: func(string, ...interface{}) {},
}
}

func (i *CatalogInstalledGet) Run(ctx context.Context) ([]olmv1catalogd.ClusterCatalog, error) {
// get
if i.CatalogName != "" {
var result olmv1catalogd.ClusterCatalog

opKey := types.NamespacedName{Name: i.CatalogName}
err := i.config.Client.Get(ctx, opKey, &result)
if err != nil {
return nil, err
}

return []olmv1catalogd.ClusterCatalog{result}, nil
}

// list
var result olmv1catalogd.ClusterCatalogList
err := i.config.Client.List(ctx, &result, &client.ListOptions{})

return result.Items, err
}
Loading
Loading