Skip to content

Commit c268e49

Browse files
Merge pull request #2170 from hongkailiu/accept-cmd
OTA-1548: set up accepted risks
2 parents 8d7f53e + b9c6614 commit c268e49

5 files changed

Lines changed: 449 additions & 18 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package accept
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/google/go-cmp/cmp"
10+
configv1 "github.com/openshift/api/config/v1"
11+
configv1client "github.com/openshift/client-go/config/clientset/versioned"
12+
"github.com/spf13/cobra"
13+
apierrors "k8s.io/apimachinery/pkg/api/errors"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/types"
16+
"k8s.io/apimachinery/pkg/util/sets"
17+
"k8s.io/cli-runtime/pkg/genericiooptions"
18+
kcmdutil "k8s.io/kubectl/pkg/cmd/util"
19+
"k8s.io/kubectl/pkg/util/templates"
20+
)
21+
22+
func newOptions(streams genericiooptions.IOStreams) *options {
23+
return &options{
24+
IOStreams: streams,
25+
}
26+
}
27+
28+
var (
29+
acceptExample = templates.Examples(`
30+
# Accept RiskA and RiskB and stop accepting RiskC if accepted
31+
oc adm upgrade accept RiskA,RiskB,RiskC-
32+
33+
# Accept RiskA and RiskB and nothing else
34+
oc adm upgrade accept --replace RiskA,RiskB
35+
36+
# Accept no risks
37+
oc adm upgrade accept --clear
38+
`)
39+
40+
acceptLong = templates.LongDesc(`
41+
Manage update risk acceptance.
42+
43+
Multiple risks are concatenated with comma. By default, the command appends the provided accepted risks into the existing
44+
list. If --replace is specified, the existing accepted risks will be replaced with the provided
45+
ones instead of appending. Placing "-" as suffix to an accepted risk will lead to
46+
removal if it exists and no-ops otherwise. If --replace is specified, the suffix "-" on the risks
47+
is not allowed.
48+
49+
Passing --clear removes all existing accepted risks.
50+
`)
51+
)
52+
53+
func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
54+
o := newOptions(streams)
55+
cmd := &cobra.Command{
56+
Use: "accept",
57+
Short: "Accept risks exposed to conditional updates.",
58+
Long: acceptLong,
59+
Example: acceptExample,
60+
Run: func(cmd *cobra.Command, args []string) {
61+
kcmdutil.CheckErr(o.Complete(f, cmd, args))
62+
kcmdutil.CheckErr(o.Run())
63+
},
64+
}
65+
66+
flags := cmd.Flags()
67+
flags.BoolVar(&o.replace, "replace", false, "Replace existing accepted risks with new ones")
68+
flags.BoolVar(&o.clear, "clear", false, "Remove all existing accepted risks")
69+
return cmd
70+
}
71+
72+
// clusterVersionInterface is the subset of configv1client.ClusterVersionInterface
73+
// that we need, for easier mocking in unit tests.
74+
type clusterVersionInterface interface {
75+
Get(ctx context.Context, name string, opts metav1.GetOptions) (*configv1.ClusterVersion, error)
76+
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *configv1.ClusterVersion, err error)
77+
}
78+
79+
type options struct {
80+
genericiooptions.IOStreams
81+
82+
Client configv1client.Interface
83+
replace bool
84+
clear bool
85+
add sets.Set[string]
86+
remove sets.Set[string]
87+
}
88+
89+
func (o *options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string) error {
90+
if o.clear && o.replace {
91+
return kcmdutil.UsageErrorf(cmd, "--clear and --replace are mutually exclusive")
92+
}
93+
94+
if o.clear {
95+
kcmdutil.RequireNoArguments(cmd, args)
96+
} else if len(args) == 0 {
97+
return kcmdutil.UsageErrorf(cmd, "no positional arguments given")
98+
}
99+
100+
if len(args) > 1 {
101+
return kcmdutil.UsageErrorf(cmd, "multiple positional arguments given")
102+
} else if len(args) == 1 {
103+
o.add = sets.New[string]()
104+
o.remove = sets.New[string]()
105+
for _, s := range strings.Split(args[0], ",") {
106+
trimmed := strings.TrimSpace(s)
107+
if trimmed == "-" || trimmed == "" {
108+
return kcmdutil.UsageErrorf(cmd, "illegal risk %q", trimmed)
109+
}
110+
if strings.HasSuffix(trimmed, "-") {
111+
o.remove.Insert(strings.TrimSuffix(trimmed, "-"))
112+
} else {
113+
o.add.Insert(trimmed)
114+
}
115+
}
116+
}
117+
118+
if conflict := o.add.Intersection(o.remove); conflict.Len() > 0 {
119+
return kcmdutil.UsageErrorf(cmd, "requested risks with both Risk and Risk-: %s", strings.Join(sets.List(conflict), ","))
120+
}
121+
122+
if o.replace && o.remove.Len() > 0 {
123+
return kcmdutil.UsageErrorf(cmd, "The suffix '-' on risks is not allowed if --replace is specified")
124+
}
125+
126+
cfg, err := f.ToRESTConfig()
127+
if err != nil {
128+
return err
129+
}
130+
client, err := configv1client.NewForConfig(cfg)
131+
if err != nil {
132+
return err
133+
}
134+
o.Client = client
135+
return nil
136+
}
137+
138+
func (o *options) Run() error {
139+
ctx := context.TODO()
140+
cv, err := o.Client.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
141+
if err != nil {
142+
if apierrors.IsNotFound(err) {
143+
return fmt.Errorf("no cluster version information available - you must be connected to an OpenShift server to fetch the current version")
144+
}
145+
return err
146+
}
147+
148+
var existing []configv1.AcceptRisk
149+
if cv.Spec.DesiredUpdate != nil {
150+
existing = cv.Spec.DesiredUpdate.AcceptRisks
151+
}
152+
acceptRisks := getAcceptRisks(existing, o.replace, o.clear, o.add, o.remove)
153+
154+
if diff := cmp.Diff(acceptRisks, existing); diff != "" {
155+
if err := patchDesiredUpdate(ctx, acceptRisks, o.Client.ConfigV1().ClusterVersions(), "version"); err != nil {
156+
return err
157+
}
158+
var names []string
159+
for _, risk := range acceptRisks {
160+
names = append(names, risk.Name)
161+
}
162+
fmt.Fprintf(o.Out, "info: Accept risks are [%s]\n", strings.Join(names, ", ")) //nolint:errcheck
163+
} else {
164+
fmt.Fprintf(o.Out, "info: Accept risks are not changed\n") //nolint:errcheck
165+
}
166+
167+
return nil
168+
}
169+
170+
func getAcceptRisks(existing []configv1.AcceptRisk, replace, clear bool, add sets.Set[string], remove sets.Set[string]) []configv1.AcceptRisk {
171+
var acceptRisks []configv1.AcceptRisk
172+
173+
if clear {
174+
return acceptRisks
175+
}
176+
177+
if !replace {
178+
for _, risk := range existing {
179+
if !remove.Has(risk.Name) {
180+
acceptRisks = append(acceptRisks, *risk.DeepCopy())
181+
}
182+
}
183+
}
184+
185+
riskNames := sets.New[string]()
186+
for _, risk := range acceptRisks {
187+
riskNames.Insert(risk.Name)
188+
}
189+
190+
for _, name := range sets.List[string](add) {
191+
if !riskNames.Has(name) && !remove.Has(name) {
192+
acceptRisks = append(acceptRisks, configv1.AcceptRisk{
193+
Name: name,
194+
})
195+
}
196+
}
197+
198+
return acceptRisks
199+
}
200+
201+
func patchDesiredUpdate(ctx context.Context, acceptRisks []configv1.AcceptRisk, client clusterVersionInterface,
202+
clusterVersionName string) error {
203+
acceptRisksJSON, err := json.Marshal(acceptRisks)
204+
if err != nil {
205+
return fmt.Errorf("marshal ClusterVersion patch: %v", err)
206+
}
207+
patch := []byte(fmt.Sprintf(`{"spec": {"desiredUpdate": {"acceptRisks": %s}}}`, acceptRisksJSON))
208+
if _, err := client.Patch(ctx, clusterVersionName, types.MergePatchType, patch,
209+
metav1.PatchOptions{}); err != nil {
210+
return fmt.Errorf("unable to accept risks: %v", err)
211+
}
212+
return nil
213+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package accept
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
configv1 "github.com/openshift/api/config/v1"
8+
"k8s.io/apimachinery/pkg/util/sets"
9+
)
10+
11+
func Test_getAcceptRisks(t *testing.T) {
12+
for _, testCase := range []struct {
13+
name string
14+
existing []configv1.AcceptRisk
15+
replace bool
16+
clear bool
17+
plus sets.Set[string]
18+
minus sets.Set[string]
19+
expected []configv1.AcceptRisk
20+
}{
21+
{
22+
name: "all zeros",
23+
},
24+
{
25+
name: "riskA, riskB + riskB + riskC - riskA - riskD",
26+
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
27+
plus: sets.New[string]("riskB", "riskC"),
28+
minus: sets.New[string]("riskA", "riskD"),
29+
expected: []configv1.AcceptRisk{
30+
{Name: "riskB"},
31+
{Name: "riskC"},
32+
},
33+
},
34+
{
35+
name: "replace",
36+
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
37+
plus: sets.New[string]("riskB", "riskC"),
38+
minus: sets.New[string]("does not matter"),
39+
replace: true,
40+
expected: []configv1.AcceptRisk{
41+
{Name: "riskB"},
42+
{Name: "riskC"},
43+
},
44+
},
45+
{
46+
name: "clear",
47+
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
48+
plus: sets.New[string]("not important"),
49+
minus: sets.New[string]("does not matter"),
50+
clear: true,
51+
},
52+
} {
53+
t.Run(testCase.name, func(t *testing.T) {
54+
actual := getAcceptRisks(testCase.existing, testCase.replace, testCase.clear, testCase.plus, testCase.minus)
55+
if diff := cmp.Diff(testCase.expected, actual); diff != "" {
56+
t.Errorf("getAcceptRisks() mismatch (-want +got):\n%s", diff)
57+
}
58+
})
59+
}
60+
}

pkg/cli/admin/upgrade/upgrade.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
configv1client "github.com/openshift/client-go/config/clientset/versioned"
2626
imagereference "github.com/openshift/library-go/pkg/image/reference"
2727

28+
"github.com/openshift/oc/pkg/cli/admin/upgrade/accept"
2829
"github.com/openshift/oc/pkg/cli/admin/upgrade/channel"
2930
"github.com/openshift/oc/pkg/cli/admin/upgrade/recommend"
3031
"github.com/openshift/oc/pkg/cli/admin/upgrade/rollback"
@@ -105,7 +106,7 @@ func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command
105106
`),
106107
Run: func(cmd *cobra.Command, args []string) {
107108
kcmdutil.CheckErr(o.Complete(f, cmd, args))
108-
kcmdutil.CheckErr(o.Run())
109+
kcmdutil.CheckErr(o.Run(cmd.Context()))
109110
},
110111
}
111112
flags := cmd.Flags()
@@ -126,6 +127,9 @@ func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command
126127
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_ROLLBACK").IsEnabled() {
127128
cmd.AddCommand(rollback.New(f, streams))
128129
}
130+
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_ACCEPT_RISKS").IsEnabled() {
131+
cmd.AddCommand(accept.New(f, streams))
132+
}
129133
cmd.AddCommand(recommend.New(f, streams))
130134

131135
return cmd
@@ -200,26 +204,34 @@ func (o *Options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string
200204
return nil
201205
}
202206

203-
func (o *Options) Run() error {
204-
cv, err := o.Client.ConfigV1().ClusterVersions().Get(context.TODO(), "version", metav1.GetOptions{})
207+
func (o *Options) Run(ctx context.Context) error {
208+
cv, err := o.Client.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
205209
if err != nil {
206210
if apierrors.IsNotFound(err) {
207211
return fmt.Errorf("No cluster version information available - you must be connected to an OpenShift version 4 server to fetch the current version")
208212
}
209213
return err
210214
}
211215

212-
ctx := context.TODO()
213-
214216
switch {
215217
case o.Clear:
216218
if cv.Spec.DesiredUpdate == nil {
217219
fmt.Fprintf(o.Out, "info: No update in progress\n")
218220
return nil
219221
}
220222
original := cv.Spec.DesiredUpdate
221-
cv.Spec.DesiredUpdate = nil
222-
updated, err := o.Client.ConfigV1().ClusterVersions().Patch(context.TODO(), cv.Name, types.MergePatchType, []byte(`{"spec":{"desiredUpdate":null}}`), metav1.PatchOptions{})
223+
if original != nil && original.AcceptRisks != nil {
224+
cv.Spec.DesiredUpdate = &configv1.Update{
225+
AcceptRisks: original.AcceptRisks,
226+
}
227+
} else {
228+
cv.Spec.DesiredUpdate = nil
229+
}
230+
bytes, err := json.Marshal(cv.Spec.DesiredUpdate)
231+
if err != nil {
232+
return fmt.Errorf("failed to marshal update: %v", err)
233+
}
234+
updated, err := o.Client.ConfigV1().ClusterVersions().Patch(ctx, cv.Name, types.MergePatchType, []byte(fmt.Sprintf(`{"spec":{"desiredUpdate":%s}}`, bytes)), metav1.PatchOptions{})
223235
if err != nil {
224236
return fmt.Errorf("Unable to cancel current rollout: %v", err)
225237
}
@@ -252,6 +264,9 @@ func (o *Options) Run() error {
252264
fmt.Fprintln(o.ErrOut, "warning: --force overrides cluster verification of your supplied release image and waives any update precondition failures. Only use this if you are testing unsigned release images or you are working around a known bug in the cluster-version operator and you have verified the authenticity of the provided image yourself.")
253265
}
254266

267+
if cv.Spec.DesiredUpdate != nil {
268+
update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
269+
}
255270
if err := patchDesiredUpdate(ctx, update, o.Client, cv.Name); err != nil {
256271

257272
return err
@@ -391,6 +406,9 @@ func (o *Options) Run() error {
391406
fmt.Fprintf(o.ErrOut, "warning: --allow-upgrade-with-warnings is bypassing: %s\n", err)
392407
}
393408

409+
if cv.Spec.DesiredUpdate != nil {
410+
update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
411+
}
394412
if err := patchDesiredUpdate(ctx, update, o.Client, cv.Name); err != nil {
395413
return err
396414
}

0 commit comments

Comments
 (0)