Skip to content

Commit 95933a7

Browse files
committed
pkg/cli/admin/upgrade/: Add oc adm upgrade recommend subcommand
Add the backbones for the new `oc adm upgrade recommend` subcommand. The logic is copied over from an existing package `upgrade` [1]. The logic is to be changed over the upcoming iterations of development. For the initial commit, the new `recommend` package contains the logic from the [1] package as of the [2] commit. Only the logic regarding the default output of the `oc adm upgrade` command and outputs regarding available or available but not recommended updates was copied over. This logic was placed behind a feature gate environment variable named `OC_ENABLE_CMD_UPGRADE_RECOMMEND`. The new subcommand can be tested by running: ``` $ OC_ENABLE_CMD_UPGRADE_RECOMMEND=true ./oc adm upgrade recommend ``` As most of the internal functions are not exported, the existing tests were copied over as well for the existing used functions in the new package `recommend`. [1] github.com/openshift/oc/pkg/cli/admin/upgrade [2] 93286c9
1 parent 93286c9 commit 95933a7

File tree

3 files changed

+280
-3
lines changed

3 files changed

+280
-3
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Package recommend displays recommended update information.
2+
package recommend
3+
4+
import (
5+
"context"
6+
"fmt"
7+
"sort"
8+
"strings"
9+
"text/tabwriter"
10+
11+
"github.com/blang/semver"
12+
"github.com/spf13/cobra"
13+
14+
apierrors "k8s.io/apimachinery/pkg/api/errors"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/cli-runtime/pkg/genericiooptions"
17+
kcmdutil "k8s.io/kubectl/pkg/cmd/util"
18+
"k8s.io/kubectl/pkg/util/templates"
19+
20+
configv1 "github.com/openshift/api/config/v1"
21+
configv1client "github.com/openshift/client-go/config/clientset/versioned"
22+
)
23+
24+
const (
25+
// clusterStatusFailing is set on the ClusterVersion status when a cluster
26+
// cannot reach the desired state. It is considered more serious than Degraded
27+
// and indicates the cluster is not healthy.
28+
clusterStatusFailing = configv1.ClusterStatusConditionType("Failing")
29+
)
30+
31+
func newOptions(streams genericiooptions.IOStreams) *options {
32+
return &options{
33+
IOStreams: streams,
34+
}
35+
}
36+
37+
func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
38+
o := newOptions(streams)
39+
cmd := &cobra.Command{
40+
Use: "recommend",
41+
Short: "Displays cluster update recommendations.",
42+
Long: templates.LongDesc(`
43+
Displays cluster update recommendations.
44+
45+
This subcommand is read-only and does not affect the state of the cluster.
46+
To request an update, use the 'oc adm upgrade' subcommand.
47+
`),
48+
Run: func(cmd *cobra.Command, args []string) {
49+
kcmdutil.CheckErr(o.Complete(f, cmd, args))
50+
kcmdutil.CheckErr(o.Run(cmd.Context()))
51+
},
52+
}
53+
flags := cmd.Flags()
54+
flags.BoolVar(&o.IncludeNotRecommended, "include-not-recommended", o.IncludeNotRecommended, "Display additional updates which are not recommended based on your cluster configuration.")
55+
56+
return cmd
57+
}
58+
59+
type options struct {
60+
genericiooptions.IOStreams
61+
62+
IncludeNotRecommended bool
63+
64+
Client configv1client.Interface
65+
}
66+
67+
func (o *options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string) error {
68+
kcmdutil.RequireNoArguments(cmd, args)
69+
70+
cfg, err := f.ToRESTConfig()
71+
if err != nil {
72+
return err
73+
}
74+
client, err := configv1client.NewForConfig(cfg)
75+
if err != nil {
76+
return err
77+
}
78+
o.Client = client
79+
return nil
80+
}
81+
82+
func (o *options) Run(ctx context.Context) error {
83+
cv, err := o.Client.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
84+
if err != nil {
85+
if apierrors.IsNotFound(err) {
86+
return fmt.Errorf("No cluster version information available - you must be connected to an OpenShift version 4 server to fetch the current version")
87+
}
88+
return err
89+
}
90+
91+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, clusterStatusFailing); c != nil {
92+
if c.Status != configv1.ConditionFalse {
93+
fmt.Fprintf(o.Out, "%s=%s:\n\n Reason: %s\n Message: %s\n\n", c.Type, c.Status, c.Reason, strings.ReplaceAll(c.Message, "\n", "\n "))
94+
}
95+
} else {
96+
fmt.Fprintf(o.ErrOut, "warning: No current %s info, see `oc describe clusterversion` for more details.\n", clusterStatusFailing)
97+
}
98+
99+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, configv1.OperatorProgressing); c != nil && len(c.Message) > 0 {
100+
if c.Status == configv1.ConditionTrue {
101+
fmt.Fprintf(o.Out, "info: An upgrade is in progress. %s\n", c.Message)
102+
} else {
103+
fmt.Fprintln(o.Out, c.Message)
104+
}
105+
} else {
106+
fmt.Fprintf(o.ErrOut, "warning: No current %s info, see `oc describe clusterversion` for more details.\n", configv1.OperatorProgressing)
107+
}
108+
fmt.Fprintln(o.Out)
109+
110+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, configv1.OperatorUpgradeable); c != nil && c.Status == configv1.ConditionFalse {
111+
fmt.Fprintf(o.Out, "%s=%s\n\n Reason: %s\n Message: %s\n\n", c.Type, c.Status, c.Reason, strings.ReplaceAll(c.Message, "\n", "\n "))
112+
}
113+
114+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, "ReleaseAccepted"); c != nil && c.Status != configv1.ConditionTrue {
115+
fmt.Fprintf(o.Out, "ReleaseAccepted=%s\n\n Reason: %s\n Message: %s\n\n", c.Status, c.Reason, strings.ReplaceAll(c.Message, "\n", "\n "))
116+
}
117+
118+
if cv.Spec.Channel != "" {
119+
if cv.Spec.Upstream == "" {
120+
fmt.Fprint(o.Out, "Upstream is unset, so the cluster will use an appropriate default.\n")
121+
} else {
122+
fmt.Fprintf(o.Out, "Upstream: %s\n", cv.Spec.Upstream)
123+
}
124+
if len(cv.Status.Desired.Channels) > 0 {
125+
fmt.Fprintf(o.Out, "Channel: %s (available channels: %s)\n", cv.Spec.Channel, strings.Join(cv.Status.Desired.Channels, ", "))
126+
} else {
127+
fmt.Fprintf(o.Out, "Channel: %s\n", cv.Spec.Channel)
128+
}
129+
}
130+
131+
if len(cv.Status.AvailableUpdates) > 0 {
132+
fmt.Fprintf(o.Out, "\nRecommended updates:\n\n")
133+
// set the minimal cell width to 14 to have a larger space between the columns for shorter versions
134+
w := tabwriter.NewWriter(o.Out, 14, 2, 1, ' ', 0)
135+
fmt.Fprintf(w, " VERSION\tIMAGE\n")
136+
// TODO: add metadata about version
137+
sortReleasesBySemanticVersions(cv.Status.AvailableUpdates)
138+
for _, update := range cv.Status.AvailableUpdates {
139+
fmt.Fprintf(w, " %s\t%s\n", update.Version, update.Image)
140+
}
141+
w.Flush()
142+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, configv1.RetrievedUpdates); c != nil && c.Status == configv1.ConditionFalse {
143+
fmt.Fprintf(o.ErrOut, "warning: Cannot refresh available updates:\n Reason: %s\n Message: %s\n\n", c.Reason, strings.ReplaceAll(c.Message, "\n", "\n "))
144+
}
145+
} else {
146+
if c := findClusterOperatorStatusCondition(cv.Status.Conditions, configv1.RetrievedUpdates); c != nil && c.Status == configv1.ConditionFalse {
147+
fmt.Fprintf(o.ErrOut, "warning: Cannot display available updates:\n Reason: %s\n Message: %s\n\n", c.Reason, strings.ReplaceAll(c.Message, "\n", "\n "))
148+
} else {
149+
fmt.Fprintf(o.Out, "No updates available. You may still upgrade to a specific release image with --to-image or wait for new updates to be available.\n")
150+
}
151+
}
152+
153+
if o.IncludeNotRecommended {
154+
if containsNotRecommendedUpdate(cv.Status.ConditionalUpdates) {
155+
sortConditionalUpdatesBySemanticVersions(cv.Status.ConditionalUpdates)
156+
fmt.Fprintf(o.Out, "\nUpdates with known issues:\n")
157+
for _, update := range cv.Status.ConditionalUpdates {
158+
if c := findCondition(update.Conditions, "Recommended"); c != nil && c.Status != metav1.ConditionTrue {
159+
fmt.Fprintf(o.Out, "\n Version: %s\n Image: %s\n", update.Release.Version, update.Release.Image)
160+
fmt.Fprintf(o.Out, " Reason: %s\n Message: %s\n", c.Reason, strings.ReplaceAll(strings.TrimSpace(c.Message), "\n", "\n "))
161+
}
162+
}
163+
} else {
164+
fmt.Fprintf(o.Out, "\nNo updates which are not recommended based on your cluster configuration are available.\n")
165+
}
166+
} else if containsNotRecommendedUpdate(cv.Status.ConditionalUpdates) {
167+
qualifier := ""
168+
for _, upgrade := range cv.Status.ConditionalUpdates {
169+
if c := findCondition(upgrade.Conditions, "Recommended"); c != nil && c.Status != metav1.ConditionTrue && c.Status != metav1.ConditionFalse {
170+
qualifier = fmt.Sprintf(", or where the recommended status is %q,", c.Status)
171+
break
172+
}
173+
}
174+
fmt.Fprintf(o.Out, "\nAdditional updates which are not recommended%s for your cluster configuration are available, to view those re-run the command with --include-not-recommended.\n", qualifier)
175+
}
176+
177+
// TODO: print previous versions
178+
179+
return nil
180+
}
181+
182+
func containsNotRecommendedUpdate(updates []configv1.ConditionalUpdate) bool {
183+
for _, update := range updates {
184+
if c := findCondition(update.Conditions, "Recommended"); c != nil && c.Status != metav1.ConditionTrue {
185+
return true
186+
}
187+
}
188+
return false
189+
}
190+
191+
// sortReleasesBySemanticVersions sorts the input slice in decreasing order.
192+
func sortReleasesBySemanticVersions(versions []configv1.Release) {
193+
sort.Slice(versions, func(i, j int) bool {
194+
a, errA := semver.Parse(versions[i].Version)
195+
b, errB := semver.Parse(versions[j].Version)
196+
if errA == nil && errB != nil {
197+
return true
198+
}
199+
if errB == nil && errA != nil {
200+
return false
201+
}
202+
if errA != nil && errB != nil {
203+
return versions[i].Version > versions[j].Version
204+
}
205+
return a.GT(b)
206+
})
207+
}
208+
209+
// sortConditionalUpdatesBySemanticVersions sorts the input slice in decreasing order.
210+
func sortConditionalUpdatesBySemanticVersions(updates []configv1.ConditionalUpdate) {
211+
sort.Slice(updates, func(i, j int) bool {
212+
a, errA := semver.Parse(updates[i].Release.Version)
213+
b, errB := semver.Parse(updates[j].Release.Version)
214+
if errA == nil && errB != nil {
215+
return true
216+
}
217+
if errB == nil && errA != nil {
218+
return false
219+
}
220+
if errA != nil && errB != nil {
221+
return updates[i].Release.Version > updates[j].Release.Version
222+
}
223+
return a.GT(b)
224+
})
225+
}
226+
227+
func findCondition(conditions []metav1.Condition, name string) *metav1.Condition {
228+
for i := range conditions {
229+
if conditions[i].Type == name {
230+
return &conditions[i]
231+
}
232+
}
233+
return nil
234+
}
235+
236+
func findClusterOperatorStatusCondition(conditions []configv1.ClusterOperatorStatusCondition, name configv1.ClusterStatusConditionType) *configv1.ClusterOperatorStatusCondition {
237+
for i := range conditions {
238+
if conditions[i].Type == name {
239+
return &conditions[i]
240+
}
241+
}
242+
return nil
243+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package recommend
2+
3+
import (
4+
"math/rand"
5+
"reflect"
6+
"testing"
7+
8+
configv1 "github.com/openshift/api/config/v1"
9+
)
10+
11+
func TestSortReleasesBySemanticVersions(t *testing.T) {
12+
expected := []configv1.Release{
13+
{Version: "10.0.0"},
14+
{Version: "2.0.10"},
15+
{Version: "2.0.5"},
16+
{Version: "2.0.1"},
17+
{Version: "2.0.0"},
18+
{Version: "not-sem-ver-2"},
19+
{Version: "not-sem-ver-1"},
20+
}
21+
22+
actual := make([]configv1.Release, len(expected))
23+
for i, j := range rand.Perm(len(expected)) {
24+
actual[i] = expected[j]
25+
}
26+
27+
sortReleasesBySemanticVersions(actual)
28+
if !reflect.DeepEqual(actual, expected) {
29+
t.Errorf("%v != %v", actual, expected)
30+
}
31+
}

pkg/cli/admin/upgrade/upgrade.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
imagereference "github.com/openshift/library-go/pkg/image/reference"
2727

2828
"github.com/openshift/oc/pkg/cli/admin/upgrade/channel"
29+
"github.com/openshift/oc/pkg/cli/admin/upgrade/recommend"
2930
"github.com/openshift/oc/pkg/cli/admin/upgrade/rollback"
3031
"github.com/openshift/oc/pkg/cli/admin/upgrade/status"
3132
)
@@ -113,15 +114,17 @@ func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command
113114
flags.BoolVar(&o.IncludeNotRecommended, "include-not-recommended", o.IncludeNotRecommended, "Display additional updates which are not recommended based on your cluster configuration.")
114115
flags.BoolVar(&o.AllowNotRecommended, "allow-not-recommended", o.AllowNotRecommended, "Allows upgrade to a version when it is supported but not recommended for updates.")
115116

117+
cmd.AddCommand(channel.New(f, streams))
118+
116119
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_STATUS").IsEnabled() {
117120
cmd.AddCommand(status.New(f, streams))
118121
}
119-
120-
cmd.AddCommand(channel.New(f, streams))
121-
122122
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_ROLLBACK").IsEnabled() {
123123
cmd.AddCommand(rollback.New(f, streams))
124124
}
125+
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_RECOMMEND").IsEnabled() {
126+
cmd.AddCommand(recommend.New(f, streams))
127+
}
125128

126129
return cmd
127130
}

0 commit comments

Comments
 (0)