Skip to content

Commit 0d82ca4

Browse files
committed
install: support starting-csv; add upgrade command
1 parent 986626c commit 0d82ca4

File tree

6 files changed

+286
-59
lines changed

6 files changed

+286
-59
lines changed

internal/cmd/operator_list.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ func newOperatorListCmd(cfg *action.Configuration) *cobra.Command {
4949
nsCol = "\tNAMESPACE"
5050
}
5151
tw := tabwriter.NewWriter(os.Stdout, 3, 4, 2, ' ', 0)
52-
_, _ = fmt.Fprintf(tw, "PACKAGE%s\tSUBSCRIPTION\tINSTALLED CSV\tSTATUS\tAGE\n", nsCol)
52+
_, _ = fmt.Fprintf(tw, "PACKAGE%s\tSUBSCRIPTION\tINSTALLED CSV\tCURRENT CSV\tSTATUS\tAGE\n", nsCol)
5353
for _, sub := range subs {
5454
ns := ""
5555
if allNamespaces {
5656
ns = "\t" + sub.Namespace
5757
}
5858
age := time.Now().Sub(sub.CreationTimestamp.Time)
59-
_, _ = fmt.Fprintf(tw, "%s%s\t%s\t%s\t%s\t%s\n", sub.Spec.Package, ns, sub.Name, sub.Status.InstalledCSV, sub.Status.State, duration.HumanDuration(age))
59+
_, _ = fmt.Fprintf(tw, "%s%s\t%s\t%s\t%s\t%s\t%s\n", sub.Spec.Package, ns, sub.Name, sub.Status.InstalledCSV, sub.Status.CurrentCSV, sub.Status.State, duration.HumanDuration(age))
6060
}
6161
_ = tw.Flush()
6262

internal/cmd/operator_upgrade.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/joelanford/kubectl-operator/internal/pkg/action"
9+
"github.com/joelanford/kubectl-operator/internal/pkg/log"
10+
)
11+
12+
func newOperatorUpgradeCmd(cfg *action.Configuration) *cobra.Command {
13+
u := action.NewOperatorUpgrade(cfg)
14+
cmd := &cobra.Command{
15+
Use: "upgrade <operator>",
16+
Short: "Upgrade an operator",
17+
Args: cobra.ExactArgs(1),
18+
Run: func(cmd *cobra.Command, args []string) {
19+
u.Package = args[0]
20+
ctx, cancel := context.WithTimeout(cmd.Context(), u.UpgradeTimeout)
21+
defer cancel()
22+
csv, err := u.Run(ctx)
23+
if err != nil {
24+
log.Fatalf("failed to upgrade operator: %v", err)
25+
}
26+
log.Printf("operator %q upgraded; installed csv is %q", u.Package, csv.Name)
27+
},
28+
}
29+
u.BindFlags(cmd.Flags())
30+
return cmd
31+
}

internal/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ operators from the installed catalogs.`,
3030
cmd.AddCommand(
3131
newCatalogCmd(&cfg),
3232
newOperatorInstallCmd(&cfg),
33+
newOperatorUpgradeCmd(&cfg),
3334
newOperatorUninstallCmd(&cfg),
3435
newOperatorListCmd(&cfg),
3536
newOperatorListAvailableCmd(&cfg),

internal/pkg/action/operator_install.go

Lines changed: 109 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type OperatorInstall struct {
2424

2525
Package string
2626
Channel string
27+
StartingCSV string
2728
Approval subscription.ApprovalValue
2829
InstallMode operator.InstallMode
2930
InstallTimeout time.Duration
@@ -40,6 +41,7 @@ func NewOperatorInstall(cfg *Configuration) *OperatorInstall {
4041
func (i *OperatorInstall) BindFlags(fs *pflag.FlagSet) {
4142
fs.StringVarP(&i.Channel, "channel", "c", "", "subscription channel")
4243
fs.VarP(&i.Approval, "approval", "a", fmt.Sprintf("approval (%s or %s)", v1alpha1.ApprovalManual, v1alpha1.ApprovalAutomatic))
44+
fs.StringVarP(&i.StartingCSV, "starting-csv", "s", "", "install specific csv for operator")
4345
fs.VarP(&i.InstallMode, "install-mode", "i", "install mode")
4446
fs.DurationVarP(&i.InstallTimeout, "timeout", "t", time.Minute, "the amount of time to wait before cancelling the install")
4547
fs.DurationVar(&i.CleanupTimeout, "cleanup-timeout", time.Minute, "the amount to time to wait before cancelling cleanup")
@@ -82,69 +84,26 @@ func (i *OperatorInstall) Run(ctx context.Context) (*v1alpha1.ClusterServiceVers
8284
return nil, err
8385
}
8486

85-
opts := []subscription.Option{}
86-
opts = append(opts, subscription.InstallPlanApproval(i.Approval.Approval))
87-
88-
subKey := types.NamespacedName{
89-
Namespace: i.config.Namespace,
90-
Name: i.Package,
91-
}
92-
sourceKey := types.NamespacedName{
93-
Namespace: pm.Status.CatalogSourceNamespace,
94-
Name: pm.Status.CatalogSource,
95-
}
96-
sub := subscription.Build(subKey, i.Channel, sourceKey, opts...)
97-
if err := i.config.Client.Create(ctx, sub); err != nil {
98-
return nil, fmt.Errorf("create subscription: %v", err)
87+
sub, err := i.createSubscription(ctx, pm)
88+
if err != nil {
89+
return nil, err
9990
}
10091
log.Printf("subscription %q created", sub.Name)
10192

102-
// We need to approve the initial install plan
103-
if i.Approval.Approval == v1alpha1.ApprovalManual {
104-
if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) {
105-
if err := i.config.Client.Get(ctx, subKey, sub); err != nil {
106-
return false, err
107-
}
108-
if sub.Status.InstallPlanRef != nil {
109-
return true, nil
110-
}
111-
return false, nil
112-
}, ctx.Done()); err != nil {
113-
return nil, fmt.Errorf("waiting for subscription install plan to exist: %v", err)
114-
}
115-
116-
ip := v1alpha1.InstallPlan{}
117-
ipKey := types.NamespacedName{
118-
Namespace: sub.Status.InstallPlanRef.Namespace,
119-
Name: sub.Status.InstallPlanRef.Name,
120-
}
121-
if err := i.config.Client.Get(ctx, ipKey, &ip); err != nil {
122-
return nil, fmt.Errorf("get install plan: %v", err)
123-
}
124-
ip.Spec.Approved = true
125-
if err := i.config.Client.Update(ctx, &ip); err != nil {
126-
return nil, fmt.Errorf("approve install plan: %v", err)
127-
}
93+
ip, err := i.getInstallPlan(ctx, sub)
94+
if err != nil {
95+
return nil, err
12896
}
12997

130-
if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) {
131-
if err := i.config.Client.Get(ctx, subKey, sub); err != nil {
132-
return false, err
133-
}
134-
if sub.Status.State == v1alpha1.SubscriptionStateAtLatest {
135-
return true, nil
98+
// We need to approve the initial install plan
99+
if i.Approval.Approval == v1alpha1.ApprovalManual {
100+
if err := i.approveInstallPlan(ctx, ip); err != nil {
101+
return nil, err
136102
}
137-
return false, nil
138-
}, ctx.Done()); err != nil {
139-
return nil, fmt.Errorf("waiting for subscription state \"AtLatestKnown\": %v", err)
140103
}
141104

142-
csvKey := types.NamespacedName{
143-
Namespace: i.config.Namespace,
144-
Name: sub.Status.InstalledCSV,
145-
}
146-
csv := &v1alpha1.ClusterServiceVersion{}
147-
if err := i.config.Client.Get(ctx, csvKey, csv); err != nil {
105+
csv, err := i.getCSV(ctx, ip)
106+
if err != nil {
148107
return nil, fmt.Errorf("get clusterserviceversion: %v", err)
149108
}
150109
return csv, nil
@@ -249,6 +208,101 @@ func (i *OperatorInstall) getPackageChannel(pm *operatorsv1.PackageManifest) (*o
249208
return packageChannel, nil
250209
}
251210

211+
func (i *OperatorInstall) createSubscription(ctx context.Context, pm *operatorsv1.PackageManifest) (*v1alpha1.Subscription, error) {
212+
opts := []subscription.Option{
213+
subscription.InstallPlanApproval(i.Approval.Approval),
214+
}
215+
if i.StartingCSV != "" {
216+
opts = append(opts, subscription.StartingCSV(i.StartingCSV))
217+
}
218+
219+
subKey := types.NamespacedName{
220+
Namespace: i.config.Namespace,
221+
Name: i.Package,
222+
}
223+
sourceKey := types.NamespacedName{
224+
Namespace: pm.Status.CatalogSourceNamespace,
225+
Name: pm.Status.CatalogSource,
226+
}
227+
sub := subscription.Build(subKey, i.Channel, sourceKey, opts...)
228+
if err := i.config.Client.Create(ctx, sub); err != nil {
229+
return nil, fmt.Errorf("create subscription: %v", err)
230+
231+
}
232+
return sub, nil
233+
}
234+
235+
func (i *OperatorInstall) getInstallPlan(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.InstallPlan, error) {
236+
subKey := types.NamespacedName{
237+
Namespace: sub.GetNamespace(),
238+
Name: sub.GetName(),
239+
}
240+
if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) {
241+
if err := i.config.Client.Get(ctx, subKey, sub); err != nil {
242+
return false, err
243+
}
244+
if sub.Status.InstallPlanRef != nil {
245+
return true, nil
246+
}
247+
return false, nil
248+
}, ctx.Done()); err != nil {
249+
return nil, fmt.Errorf("waiting for install plan to exist: %v", err)
250+
}
251+
252+
ip := v1alpha1.InstallPlan{}
253+
ipKey := types.NamespacedName{
254+
Namespace: sub.Status.InstallPlanRef.Namespace,
255+
Name: sub.Status.InstallPlanRef.Name,
256+
}
257+
if err := i.config.Client.Get(ctx, ipKey, &ip); err != nil {
258+
return nil, fmt.Errorf("get install plan: %v", err)
259+
}
260+
return &ip, nil
261+
}
262+
263+
func (i *OperatorInstall) approveInstallPlan(ctx context.Context, ip *v1alpha1.InstallPlan) error {
264+
ip.Spec.Approved = true
265+
if err := i.config.Client.Update(ctx, ip); err != nil {
266+
return fmt.Errorf("approve install plan: %v", err)
267+
}
268+
return nil
269+
}
270+
271+
func (i *OperatorInstall) getCSV(ctx context.Context, ip *v1alpha1.InstallPlan) (*v1alpha1.ClusterServiceVersion, error) {
272+
ipKey := types.NamespacedName{
273+
Namespace: ip.GetNamespace(),
274+
Name: ip.GetName(),
275+
}
276+
if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) {
277+
if err := i.config.Client.Get(ctx, ipKey, ip); err != nil {
278+
return false, err
279+
}
280+
if ip.Status.Phase == v1alpha1.InstallPlanPhaseComplete {
281+
return true, nil
282+
}
283+
return false, nil
284+
}, ctx.Done()); err != nil {
285+
return nil, fmt.Errorf("waiting for operator installation to complete: %v", err)
286+
}
287+
288+
csvKey := types.NamespacedName{
289+
Namespace: i.config.Namespace,
290+
}
291+
for _, s := range ip.Status.Plan {
292+
if s.Resource.Kind == "ClusterServiceVersion" {
293+
csvKey.Name = s.Resource.Name
294+
}
295+
}
296+
if csvKey.Name == "" {
297+
return nil, fmt.Errorf("could not find installed CSV in install plan")
298+
}
299+
csv := &v1alpha1.ClusterServiceVersion{}
300+
if err := i.config.Client.Get(ctx, csvKey, csv); err != nil {
301+
return nil, fmt.Errorf("get clusterserviceversion: %v", err)
302+
}
303+
return csv, nil
304+
}
305+
252306
func (i *OperatorInstall) cleanup(ctx context.Context, sub *v1alpha1.Subscription) {
253307
if err := i.config.Client.Delete(ctx, sub); err != nil {
254308
log.Printf("delete subscription %q: %v", sub.Name, err)
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
9+
"github.com/spf13/pflag"
10+
"k8s.io/apimachinery/pkg/types"
11+
"k8s.io/apimachinery/pkg/util/wait"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
"github.com/joelanford/kubectl-operator/internal/pkg/log"
15+
)
16+
17+
type OperatorUpgrade struct {
18+
config *Configuration
19+
20+
Package string
21+
Channel string
22+
UpgradeTimeout time.Duration
23+
}
24+
25+
func NewOperatorUpgrade(cfg *Configuration) *OperatorUpgrade {
26+
return &OperatorUpgrade{
27+
config: cfg,
28+
}
29+
}
30+
31+
func (u *OperatorUpgrade) BindFlags(fs *pflag.FlagSet) {
32+
fs.StringVarP(&u.Channel, "channel", "c", "", "subscription channel")
33+
fs.DurationVarP(&u.UpgradeTimeout, "timeout", "t", time.Minute, "the amount of time to wait before cancelling the upgrade")
34+
}
35+
36+
func (u *OperatorUpgrade) Run(ctx context.Context) (*v1alpha1.ClusterServiceVersion, error) {
37+
subs := v1alpha1.SubscriptionList{}
38+
if err := u.config.Client.List(ctx, &subs, client.InNamespace(u.config.Namespace)); err != nil {
39+
return nil, fmt.Errorf("list subscriptions: %v", err)
40+
}
41+
42+
var sub *v1alpha1.Subscription
43+
for _, s := range subs.Items {
44+
if u.Package == s.Spec.Package {
45+
sub = &s
46+
break
47+
}
48+
}
49+
if sub == nil {
50+
return nil, fmt.Errorf("operator package %q not found", u.Package)
51+
}
52+
53+
ip, err := u.getInstallPlan(ctx, sub)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
if err := u.approveInstallPlan(ctx, ip); err != nil {
59+
return nil, fmt.Errorf("approve install plan: %v", err)
60+
}
61+
62+
csv, err := u.getCSV(ctx, ip)
63+
if err != nil {
64+
return nil, fmt.Errorf("get clusterserviceversion: %v", err)
65+
}
66+
return csv, nil
67+
}
68+
69+
func (u *OperatorUpgrade) getInstallPlan(ctx context.Context, sub *v1alpha1.Subscription) (*v1alpha1.InstallPlan, error) {
70+
if sub.Status.InstallPlanRef == nil {
71+
return nil, fmt.Errorf("subscription does not reference an install plan")
72+
}
73+
if sub.Status.InstalledCSV == sub.Status.CurrentCSV {
74+
return nil, fmt.Errorf("operator is already at latest version")
75+
}
76+
77+
ip := v1alpha1.InstallPlan{}
78+
ipKey := types.NamespacedName{
79+
Namespace: sub.Status.InstallPlanRef.Namespace,
80+
Name: sub.Status.InstallPlanRef.Name,
81+
}
82+
if err := u.config.Client.Get(ctx, ipKey, &ip); err != nil {
83+
return nil, fmt.Errorf("get install plan: %v", err)
84+
}
85+
return &ip, nil
86+
}
87+
88+
func (u *OperatorUpgrade) approveInstallPlan(ctx context.Context, ip *v1alpha1.InstallPlan) error {
89+
ip.Spec.Approved = true
90+
if err := u.config.Client.Update(ctx, ip); err != nil {
91+
return fmt.Errorf("approve install plan: %v", err)
92+
}
93+
return nil
94+
}
95+
96+
func (u *OperatorUpgrade) getCSV(ctx context.Context, ip *v1alpha1.InstallPlan) (*v1alpha1.ClusterServiceVersion, error) {
97+
ipKey := types.NamespacedName{
98+
Namespace: ip.GetNamespace(),
99+
Name: ip.GetName(),
100+
}
101+
if err := wait.PollImmediateUntil(time.Millisecond*250, func() (bool, error) {
102+
if err := u.config.Client.Get(ctx, ipKey, ip); err != nil {
103+
return false, err
104+
}
105+
if ip.Status.Phase == v1alpha1.InstallPlanPhaseComplete {
106+
return true, nil
107+
}
108+
return false, nil
109+
}, ctx.Done()); err != nil {
110+
return nil, fmt.Errorf("waiting for operator installation to complete: %v", err)
111+
}
112+
113+
csvKey := types.NamespacedName{
114+
Namespace: u.config.Namespace,
115+
}
116+
for _, s := range ip.Status.Plan {
117+
if s.Resource.Kind == "ClusterServiceVersion" {
118+
csvKey.Name = s.Resource.Name
119+
}
120+
}
121+
if csvKey.Name == "" {
122+
return nil, fmt.Errorf("could not find installed CSV in install plan")
123+
}
124+
csv := &v1alpha1.ClusterServiceVersion{}
125+
if err := u.config.Client.Get(ctx, csvKey, csv); err != nil {
126+
return nil, fmt.Errorf("get clusterserviceversion: %v", err)
127+
}
128+
return csv, nil
129+
}
130+
131+
func (u *OperatorUpgrade) cleanup(ctx context.Context, sub *v1alpha1.Subscription) {
132+
if err := u.config.Client.Delete(ctx, sub); err != nil {
133+
log.Printf("delete subscription %q: %v", sub.Name, err)
134+
}
135+
}

0 commit comments

Comments
 (0)