Skip to content

Commit 7794db4

Browse files
authored
Add command to update olmv1 operator (#225)
Signed-off-by: Artur Zych <[email protected]> Co-authored-by: Artur Zych <[email protected]>
1 parent 6be5255 commit 7794db4

File tree

8 files changed

+497
-15
lines changed

8 files changed

+497
-15
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package olmv1
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/pflag"
6+
7+
"github.com/operator-framework/kubectl-operator/internal/cmd/internal/log"
8+
v1action "github.com/operator-framework/kubectl-operator/internal/pkg/v1/action"
9+
"github.com/operator-framework/kubectl-operator/pkg/action"
10+
)
11+
12+
// NewOperatorUpdateCmd allows updating a selected operator
13+
func NewOperatorUpdateCmd(cfg *action.Configuration) *cobra.Command {
14+
i := v1action.NewOperatorUpdate(cfg)
15+
i.Logf = log.Printf
16+
17+
cmd := &cobra.Command{
18+
Use: "operator <operator>",
19+
Short: "Update an operator",
20+
Args: cobra.ExactArgs(1),
21+
Run: func(cmd *cobra.Command, args []string) {
22+
i.Package = args[0]
23+
_, err := i.Run(cmd.Context())
24+
if err != nil {
25+
log.Fatalf("failed to update operator: %v", err)
26+
}
27+
log.Printf("operator %q updated", i.Package)
28+
},
29+
}
30+
bindOperatorUpdateFlags(cmd.Flags(), i)
31+
32+
return cmd
33+
}
34+
35+
func bindOperatorUpdateFlags(fs *pflag.FlagSet, i *v1action.OperatorUpdate) {
36+
fs.StringVar(&i.Version, "version", "", "desired operator version (single or range) in semVer format. AND operation with channels")
37+
fs.StringVar(&i.Selector, "selector", "", "filters the set of catalogs used in the bundle selection process. Empty means that all catalogs will be used in the bundle selection process")
38+
fs.StringArrayVar(&i.Channels, "channels", []string{}, "desired channels for operator versions. AND operation with version. Empty list means all available channels will be taken into consideration")
39+
fs.StringVar(&i.UpgradeConstraintPolicy, "upgrade-constraint-policy", "", "controls whether the upgrade path(s) defined in the catalog are enforced. One of CatalogProvided|SelfCertified), Default: CatalogProvided")
40+
fs.StringToStringVar(&i.Labels, "labels", map[string]string{}, "labels that will be set on the operator")
41+
fs.BoolVar(&i.IgnoreUnset, "ignore-unset", true, "when enabled, any unset flag value will not be changed. Disabling means that for each unset value a default will be used instead")
42+
}

internal/cmd/olmv1.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,22 @@ func newOlmV1Cmd(cfg *action.Configuration) *cobra.Command {
3838
}
3939
deleteCmd.AddCommand(olmv1.NewCatalogDeleteCmd(cfg))
4040

41+
updateCmd := &cobra.Command{
42+
Use: "update",
43+
Short: "Update a resource",
44+
Long: "Update a resource",
45+
}
46+
updateCmd.AddCommand(
47+
olmv1.NewOperatorUpdateCmd(cfg),
48+
)
49+
4150
cmd.AddCommand(
4251
olmv1.NewOperatorInstallCmd(cfg),
4352
olmv1.NewOperatorUninstallCmd(cfg),
4453
getCmd,
4554
createCmd,
4655
deleteCmd,
56+
updateCmd,
4757
)
4858

4959
return cmd

internal/pkg/v1/action/action_suite_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
. "github.com/onsi/ginkgo"
99
. "github.com/onsi/gomega"
1010

11+
apimeta "k8s.io/apimachinery/pkg/api/meta"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/types"
1214
"sigs.k8s.io/controller-runtime/pkg/client"
1315

1416
olmv1 "github.com/operator-framework/operator-controller/api/v1"
@@ -63,3 +65,69 @@ func newClusterCatalog(name string) *olmv1.ClusterCatalog {
6365
ObjectMeta: metav1.ObjectMeta{Name: name},
6466
}
6567
}
68+
69+
type extensionOpt func(*olmv1.ClusterExtension)
70+
71+
func withVersion(version string) extensionOpt {
72+
return func(ext *olmv1.ClusterExtension) {
73+
ext.Spec.Source.Catalog.Version = version
74+
}
75+
}
76+
77+
func withSourceType(sourceType string) extensionOpt {
78+
return func(ext *olmv1.ClusterExtension) {
79+
ext.Spec.Source.SourceType = sourceType
80+
}
81+
}
82+
83+
// nolint: unparam
84+
func withConstraintPolicy(policy string) extensionOpt {
85+
return func(ext *olmv1.ClusterExtension) {
86+
ext.Spec.Source.Catalog.UpgradeConstraintPolicy = olmv1.UpgradeConstraintPolicy(policy)
87+
}
88+
}
89+
90+
func withChannels(channels ...string) extensionOpt {
91+
return func(ext *olmv1.ClusterExtension) {
92+
ext.Spec.Source.Catalog.Channels = channels
93+
}
94+
}
95+
96+
func withLabels(labels map[string]string) extensionOpt {
97+
return func(ext *olmv1.ClusterExtension) {
98+
ext.SetLabels(labels)
99+
}
100+
}
101+
102+
func buildExtension(packageName string, opts ...extensionOpt) *olmv1.ClusterExtension {
103+
ext := &olmv1.ClusterExtension{
104+
Spec: olmv1.ClusterExtensionSpec{
105+
Source: olmv1.SourceConfig{
106+
Catalog: &olmv1.CatalogFilter{PackageName: packageName},
107+
},
108+
},
109+
}
110+
ext.SetName(packageName)
111+
for _, opt := range opts {
112+
opt(ext)
113+
}
114+
115+
return ext
116+
}
117+
118+
func updateOperatorConditionStatus(name string, cl client.Client, typ string, status metav1.ConditionStatus) error {
119+
var ext olmv1.ClusterExtension
120+
key := types.NamespacedName{Name: name}
121+
122+
if err := cl.Get(context.TODO(), key, &ext); err != nil {
123+
return err
124+
}
125+
126+
apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{
127+
Type: typ,
128+
Status: status,
129+
ObservedGeneration: ext.GetGeneration(),
130+
})
131+
132+
return cl.Update(context.TODO(), &ext)
133+
}

internal/pkg/v1/action/catalog_delete.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func NewCatalogDelete(cfg *action.Configuration) *CatalogDelete {
2828
func (cd *CatalogDelete) Run(ctx context.Context) ([]string, error) {
2929
// validate
3030
if cd.DeleteAll && cd.CatalogName != "" {
31-
return nil, errNameAndSelector
31+
return nil, ErrNameAndSelector
3232
}
3333

3434
// delete single, specified catalog
@@ -42,7 +42,7 @@ func (cd *CatalogDelete) Run(ctx context.Context) ([]string, error) {
4242
return nil, err
4343
}
4444
if len(catatalogList.Items) == 0 {
45-
return nil, errNoResourcesFound
45+
return nil, ErrNoResourcesFound
4646
}
4747

4848
errs := make([]error, 0, len(catatalogList.Items))

internal/pkg/v1/action/errors.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package action
33
import "errors"
44

55
var (
6-
errNoResourcesFound = errors.New("no resources found")
7-
errNameAndSelector = errors.New("name cannot be provided when a selector is specified")
6+
ErrNoResourcesFound = errors.New("no resources found")
7+
ErrNameAndSelector = errors.New("name cannot be provided when a selector is specified")
8+
ErrNoChange = errors.New("no changes detected - operator already in desired state")
89
)

internal/pkg/v1/action/helpers.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"slices"
7+
"strings"
78
"time"
89

910
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -46,20 +47,26 @@ func waitUntilCatalogStatusCondition(
4647
})
4748
}
4849

49-
func waitForDeletion(ctx context.Context, cl client.Client, obj client.Object) error {
50-
key := objectKeyForObject(obj)
51-
if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {
52-
if err := cl.Get(conditionCtx, key, obj); apierrors.IsNotFound(err) {
53-
return true, nil
54-
} else if err != nil {
50+
func waitUntilOperatorStatusCondition(
51+
ctx context.Context,
52+
cl getter,
53+
operator *olmv1.ClusterExtension,
54+
conditionType string,
55+
conditionStatus metav1.ConditionStatus,
56+
) error {
57+
opKey := objectKeyForObject(operator)
58+
return wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {
59+
if err := cl.Get(conditionCtx, opKey, operator); err != nil {
5560
return false, err
5661
}
57-
return false, nil
58-
}); err != nil {
59-
return fmt.Errorf("waiting for deletion: %w", err)
60-
}
6162

62-
return nil
63+
if slices.ContainsFunc(operator.Status.Conditions, func(cond metav1.Condition) bool {
64+
return cond.Type == conditionType && cond.Status == conditionStatus
65+
}) {
66+
return true, nil
67+
}
68+
return false, nil
69+
})
6370
}
6471

6572
func deleteWithTimeout(cl deleter, obj client.Object, timeout time.Duration) error {
@@ -72,3 +79,22 @@ func deleteWithTimeout(cl deleter, obj client.Object, timeout time.Duration) err
7279

7380
return nil
7481
}
82+
83+
func waitForDeletion(ctx context.Context, cl client.Client, objs ...client.Object) error {
84+
for _, obj := range objs {
85+
obj := obj
86+
lowerKind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
87+
key := objectKeyForObject(obj)
88+
if err := wait.PollUntilContextCancel(ctx, pollInterval, true, func(conditionCtx context.Context) (bool, error) {
89+
if err := cl.Get(conditionCtx, key, obj); apierrors.IsNotFound(err) {
90+
return true, nil
91+
} else if err != nil {
92+
return false, err
93+
}
94+
return false, nil
95+
}); err != nil {
96+
return fmt.Errorf("wait for %s %q deleted: %v", lowerKind, key.Name, err)
97+
}
98+
}
99+
return nil
100+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
"time"
9+
10+
"github.com/blang/semver/v4"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/types"
13+
14+
olmv1 "github.com/operator-framework/operator-controller/api/v1"
15+
16+
"github.com/operator-framework/kubectl-operator/pkg/action"
17+
)
18+
19+
type OperatorUpdate struct {
20+
cfg *action.Configuration
21+
22+
Package string
23+
24+
Version string
25+
Channels []string
26+
Selector string
27+
// parsedSelector is used internally to avoid potentially costly transformations
28+
// between string and metav1.LabelSelector formats
29+
parsedSelector *metav1.LabelSelector
30+
UpgradeConstraintPolicy string
31+
Labels map[string]string
32+
IgnoreUnset bool
33+
34+
CleanupTimeout time.Duration
35+
36+
Logf func(string, ...interface{})
37+
}
38+
39+
func NewOperatorUpdate(cfg *action.Configuration) *OperatorUpdate {
40+
return &OperatorUpdate{
41+
cfg: cfg,
42+
Logf: func(string, ...interface{}) {},
43+
}
44+
}
45+
46+
func (ou *OperatorUpdate) Run(ctx context.Context) (*olmv1.ClusterExtension, error) {
47+
var ext olmv1.ClusterExtension
48+
var err error
49+
50+
opKey := types.NamespacedName{Name: ou.Package}
51+
if err = ou.cfg.Client.Get(ctx, opKey, &ext); err != nil {
52+
return nil, err
53+
}
54+
55+
if ext.Spec.Source.SourceType != olmv1.SourceTypeCatalog {
56+
return nil, fmt.Errorf("unrecognized source type: %q", ext.Spec.Source.SourceType)
57+
}
58+
59+
ou.setDefaults(ext)
60+
61+
if ou.Version != "" {
62+
if _, err = semver.ParseRange(ou.Version); err != nil {
63+
return nil, fmt.Errorf("failed parsing version: %w", err)
64+
}
65+
}
66+
if ou.Selector != "" && ou.parsedSelector == nil {
67+
ou.parsedSelector, err = metav1.ParseToLabelSelector(ou.Selector)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed parsing selector: %w", err)
70+
}
71+
}
72+
73+
constraintPolicy := olmv1.UpgradeConstraintPolicy(ou.UpgradeConstraintPolicy)
74+
if !ou.needsUpdate(ext, constraintPolicy) {
75+
return nil, ErrNoChange
76+
}
77+
78+
ou.prepareUpdatedExtension(&ext, constraintPolicy)
79+
if err := ou.cfg.Client.Update(ctx, &ext); err != nil {
80+
return nil, err
81+
}
82+
83+
if err := waitUntilOperatorStatusCondition(ctx, ou.cfg.Client, &ext, olmv1.TypeInstalled, metav1.ConditionTrue); err != nil {
84+
return nil, fmt.Errorf("timed out waiting for operator: %w", err)
85+
}
86+
87+
return &ext, nil
88+
}
89+
90+
func (ou *OperatorUpdate) setDefaults(ext olmv1.ClusterExtension) {
91+
if !ou.IgnoreUnset {
92+
if ou.UpgradeConstraintPolicy == "" {
93+
ou.UpgradeConstraintPolicy = string(olmv1.UpgradeConstraintPolicyCatalogProvided)
94+
}
95+
96+
return
97+
}
98+
99+
// IgnoreUnset is enabled
100+
// set all unset values to what they are on the current object
101+
catalogSrc := ext.Spec.Source.Catalog
102+
if ou.Version == "" {
103+
ou.Version = catalogSrc.Version
104+
}
105+
if len(ou.Channels) == 0 {
106+
ou.Channels = catalogSrc.Channels
107+
}
108+
if ou.UpgradeConstraintPolicy == "" {
109+
ou.UpgradeConstraintPolicy = string(catalogSrc.UpgradeConstraintPolicy)
110+
}
111+
if len(ou.Labels) == 0 {
112+
ou.Labels = ext.Labels
113+
}
114+
if ou.Selector == "" && catalogSrc.Selector != nil {
115+
ou.parsedSelector = catalogSrc.Selector
116+
}
117+
}
118+
119+
func (ou *OperatorUpdate) needsUpdate(ext olmv1.ClusterExtension, constraintPolicy olmv1.UpgradeConstraintPolicy) bool {
120+
catalogSrc := ext.Spec.Source.Catalog
121+
122+
// object string form is used for comparison to:
123+
// - remove the need for potentially costly metav1.FormatLabelSelector calls
124+
// - avoid having to handle potential reordering of items from on cluster state
125+
sameSelectors := (catalogSrc.Selector == nil && ou.parsedSelector == nil) ||
126+
(catalogSrc.Selector != nil && ou.parsedSelector != nil &&
127+
catalogSrc.Selector.String() == ou.parsedSelector.String())
128+
129+
if catalogSrc.Version == ou.Version &&
130+
slices.Equal(catalogSrc.Channels, ou.Channels) &&
131+
catalogSrc.UpgradeConstraintPolicy == constraintPolicy &&
132+
maps.Equal(ext.Labels, ou.Labels) &&
133+
sameSelectors {
134+
return false
135+
}
136+
137+
return true
138+
}
139+
140+
func (ou *OperatorUpdate) prepareUpdatedExtension(ext *olmv1.ClusterExtension, constraintPolicy olmv1.UpgradeConstraintPolicy) {
141+
ext.SetLabels(ou.Labels)
142+
ext.Spec.Source.Catalog.Version = ou.Version
143+
ext.Spec.Source.Catalog.Selector = ou.parsedSelector
144+
ext.Spec.Source.Catalog.Channels = ou.Channels
145+
ext.Spec.Source.Catalog.UpgradeConstraintPolicy = constraintPolicy
146+
}

0 commit comments

Comments
 (0)