Skip to content
Open
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292
github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7
github.com/openshift/api v0.0.0-20260109135506-3920bba77f16
github.com/openshift/build-machinery-go v0.0.0-20251020112516-49aa9f5db6d8
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13
github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/prometheus/client_golang v1.23.2
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,12 @@ github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+L
github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292 h1:3athg6KQ+TaNfW4BWZDlGFt1ImSZEJWgzXtPC1VPITI=
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250804142706-7b3ab438a292/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7 h1:fdvcDJySvjVJctbPbdLPoMiMk+bls34+eq6tWOqdFZg=
github.com/openshift/api v0.0.0-20251111013132-5c461e21bdb7/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/api v0.0.0-20260109135506-3920bba77f16 h1:EfTfmlNBtG/xauH9gcnq64J08nYTBKyilbl/EUbxGno=
github.com/openshift/api v0.0.0-20260109135506-3920bba77f16/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
github.com/openshift/build-machinery-go v0.0.0-20251020112516-49aa9f5db6d8 h1:2sktNP3CNpDb5F9rIg1qcBYU4lFxsOfBsUSP32LwAPo=
github.com/openshift/build-machinery-go v0.0.0-20251020112516-49aa9f5db6d8/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE=
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY=
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM=
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 h1:6rd4zSo2UaWQcAPZfHK9yzKVqH0BnMv1hqMzqXZyTds=
github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13/go.mod h1:YvOmPmV7wcJxpfhTDuFqqs2Xpb3M3ovsM6Qs/i2ptq4=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08=
github.com/openshift/library-go v0.0.0-20251222131241-289839b3ffe8 h1:TWqbSjaYbZGgB6EmnEN6Hc8lQYYCgju2qORBX7Ix1LI=
Expand Down
212 changes: 212 additions & 0 deletions pkg/cli/admin/upgrade/accept/accept.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package accept

import (
"context"
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
"strings"

configv1 "github.com/openshift/api/config/v1"
configv1client "github.com/openshift/client-go/config/clientset/versioned"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericiooptions"
kcmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/templates"
)

func newOptions(streams genericiooptions.IOStreams) *options {
return &options{
IOStreams: streams,
}
}

var (
acceptExample = templates.Examples(`
# Accept RiskA and RiskB and stop accepting RiskC if accepted
oc adm upgrade accept RiskA,RiskB,RiskC-

# Accept RiskA and RiskB and nothing else
oc adm upgrade accept --replace RiskA,RiskB

# Accept no risks
oc adm upgrade accept --clear
`)

acceptLong = templates.LongDesc(`
Manage update risk acceptance.

Multiple risks are concatenated with comma. By default, the command appends the provided accepted risks into the existing
list. If --replace is specified, the existing accepted risks will be replaced with the provided
ones instead of appending. Placing "-" as suffix to an accepted risk will lead to
removal if it exists and no-ops otherwise. If --replace is specified, the suffix "-" on the risks
is not allowed.

Passing --clear removes all existing excepted risks.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

User-facing typo in long help text.

At Line [49], “excepted risks” should be “accepted risks”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cli/admin/upgrade/accept/accept.go` at line 49, The long help text for
the accept command contains a typo: change the phrase "excepted risks" to
"accepted risks" in the long description string (the Long/LongHelp text
associated with the accept command, e.g., in the acceptCmd declaration or its
help variable in accept.go) so the user-facing help displays "accepted risks".

`)
)

func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := newOptions(streams)
cmd := &cobra.Command{
Use: "accept",
Short: "Accept risks exposed to conditional updates.",
Long: acceptLong,
Example: acceptExample,
Run: func(cmd *cobra.Command, args []string) {
kcmdutil.CheckErr(o.Complete(f, cmd, args))
kcmdutil.CheckErr(o.Run(cmd.Context()))
},
}

flags := cmd.Flags()
flags.BoolVar(&o.replace, "replace", false, "Replace existing accepted risks with new ones")
flags.BoolVar(&o.clear, "clear", false, "Remove all existing accepted risks")
return cmd
}

// clusterVersionInterface is the subset of configv1client.ClusterVersionInterface
// that we need, for easier mocking in unit tests.
type clusterVersionInterface interface {
Get(ctx context.Context, name string, opts metav1.GetOptions) (*configv1.ClusterVersion, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *configv1.ClusterVersion, err error)
}

type options struct {
genericiooptions.IOStreams

Client configv1client.Interface
replace bool
clear bool
add sets.Set[string]
remove sets.Set[string]
}

func (o *options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string) error {
if o.clear && o.replace {
return kcmdutil.UsageErrorf(cmd, "--clear and --replace are mutually exclusive")
}

if o.clear {
kcmdutil.RequireNoArguments(cmd, args)
} else if len(args) == 0 {
return kcmdutil.UsageErrorf(cmd, "no positional arguments given")
}

if len(args) > 1 {
return kcmdutil.UsageErrorf(cmd, "multiple positional arguments given")
} else if len(args) == 1 {
o.add = sets.New[string]()
o.remove = sets.New[string]()
for _, s := range strings.Split(args[0], ",") {
trimmed := strings.TrimSpace(s)
if trimmed == "-" || trimmed == "" {
return kcmdutil.UsageErrorf(cmd, "illegal risk %q", trimmed)
}
if strings.HasSuffix(trimmed, "-") {
o.remove.Insert(strings.TrimSuffix(trimmed, "-"))
} else {
o.add.Insert(trimmed)
}
}
}

if conflict := o.add.Intersection(o.remove); conflict.Len() > 0 {
return kcmdutil.UsageErrorf(cmd, "requested risks with both Risk and Risk-: %s", strings.Join(sets.List(conflict), ","))
}

if o.replace && o.remove.Len() > 0 {
return kcmdutil.UsageErrorf(cmd, "The suffix '-' on risks is not allowed if --replace is specified")
}

cfg, err := f.ToRESTConfig()
if err != nil {
return err
}
client, err := configv1client.NewForConfig(cfg)
if err != nil {
return err
}
o.Client = client
return nil
}

func (o *options) Run(ctx context.Context) error {
cv, err := o.Client.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("no cluster version information available - you must be connected to an OpenShift server to fetch the current version")
}
return err
}

var existing []configv1.AcceptRisk
if cv.Spec.DesiredUpdate != nil {
existing = cv.Spec.DesiredUpdate.AcceptRisks
}
acceptRisks := getAcceptRisks(existing, o.replace, o.clear, o.add, o.remove)

if diff := cmp.Diff(acceptRisks, existing); diff != "" {
if err := patchDesiredUpdate(context.TODO(), acceptRisks, o.Client.ConfigV1().ClusterVersions(), "version"); err != nil {
return err
Comment on lines +154 to +155
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the passed context for patch calls.

At Line [155], context.TODO() ignores the caller context from Run(ctx), so cancellation/timeouts won’t propagate to the API call.

Proposed fix
-		if err := patchDesiredUpdate(context.TODO(), acceptRisks, o.Client.ConfigV1().ClusterVersions(), "version"); err != nil {
+		if err := patchDesiredUpdate(ctx, acceptRisks, o.Client.ConfigV1().ClusterVersions(), "version"); err != nil {
 			return err
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cli/admin/upgrade/accept/accept.go` around lines 155 - 156, The call to
patchDesiredUpdate currently uses context.TODO(), which prevents Run(ctx)
cancellation/timeouts from propagating; change the call to pass the incoming
context (ctx) instead. Locate the invocation of patchDesiredUpdate in the accept
flow (the call using patchDesiredUpdate(context.TODO(), acceptRisks,
o.Client.ConfigV1().ClusterVersions(), "version")) and replace the placeholder
context with the Run(ctx) parameter so patchDesiredUpdate receives the caller's
context; ensure any surrounding functions (e.g., Run(ctx)) keep ctx in scope and
thread it through to this call.

}
var names []string
for _, risk := range acceptRisks {
names = append(names, risk.Name)
}
_, _ = fmt.Fprintf(o.Out, "info: Accept risks are [%s]\n", strings.Join(names, ", "))
} else {
_, _ = fmt.Fprintf(o.Out, "info: Accept risks are not changed\n")
}

return nil
}

func getAcceptRisks(existing []configv1.AcceptRisk, replace, clear bool, add sets.Set[string], remove sets.Set[string]) []configv1.AcceptRisk {
var acceptRisks []configv1.AcceptRisk

if clear {
return acceptRisks
}

if !replace {
for _, risk := range existing {
if !remove.Has(risk.Name) {
acceptRisks = append(acceptRisks, *risk.DeepCopy())
}
}
}

riskNames := sets.New[string]()
for _, risk := range acceptRisks {
riskNames.Insert(risk.Name)
}

for _, name := range sets.List[string](add) {
if !riskNames.Has(name) && !remove.Has(name) {
acceptRisks = append(acceptRisks, configv1.AcceptRisk{
Name: name,
})
}
}

return acceptRisks
}

func patchDesiredUpdate(ctx context.Context, acceptRisks []configv1.AcceptRisk, client clusterVersionInterface,
clusterVersionName string) error {
acceptRisksJSON, err := json.Marshal(acceptRisks)
if err != nil {
return fmt.Errorf("marshal ClusterVersion patch: %v", err)
}
patch := []byte(fmt.Sprintf(`{"spec": {"desiredUpdate": {"acceptRisks": %s}}}`, acceptRisksJSON))
if _, err := client.Patch(ctx, clusterVersionName, types.MergePatchType, patch,
metav1.PatchOptions{}); err != nil {
return fmt.Errorf("unable to accept risks: %v", err)
}
return nil
}
60 changes: 60 additions & 0 deletions pkg/cli/admin/upgrade/accept/accept_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package accept

import (
"testing"

"github.com/google/go-cmp/cmp"
configv1 "github.com/openshift/api/config/v1"
"k8s.io/apimachinery/pkg/util/sets"
)

func Test_getAcceptRisks(t *testing.T) {
for _, testCase := range []struct {
name string
existing []configv1.AcceptRisk
replace bool
clear bool
plus sets.Set[string]
minus sets.Set[string]
expected []configv1.AcceptRisk
}{
{
name: "all zeros",
},
{
name: "riskA, riskB + riskB + riskC - riskA - riskD",
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
plus: sets.New[string]("riskB", "riskC"),
minus: sets.New[string]("riskA", "riskD"),
expected: []configv1.AcceptRisk{
{Name: "riskB"},
{Name: "riskC"},
},
},
{
name: "replace",
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
plus: sets.New[string]("riskB", "riskC"),
minus: sets.New[string]("does not matter"),
replace: true,
expected: []configv1.AcceptRisk{
{Name: "riskB"},
{Name: "riskC"},
},
},
{
name: "clear",
existing: []configv1.AcceptRisk{{Name: "riskA"}, {Name: "riskB"}},
plus: sets.New[string]("not important"),
minus: sets.New[string]("does not matter"),
clear: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
actual := getAcceptRisks(testCase.existing, testCase.replace, testCase.clear, testCase.plus, testCase.minus)
if diff := cmp.Diff(actual, testCase.expected); diff != "" {
t.Errorf("getAcceptRisks() mismatch (-want +got):\n%s", diff)
Comment on lines +55 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix cmp.Diff argument order in failure output.

At Line [55], cmp.Diff(actual, testCase.expected) conflicts with the (-want +got) label and inverts diff semantics.

Proposed fix
-			if diff := cmp.Diff(actual, testCase.expected); diff != "" {
+			if diff := cmp.Diff(testCase.expected, actual); diff != "" {
 				t.Errorf("getAcceptRisks() mismatch (-want +got):\n%s", diff)
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if diff := cmp.Diff(actual, testCase.expected); diff != "" {
t.Errorf("getAcceptRisks() mismatch (-want +got):\n%s", diff)
if diff := cmp.Diff(testCase.expected, actual); diff != "" {
t.Errorf("getAcceptRisks() mismatch (-want +got):\n%s", diff)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cli/admin/upgrade/accept/accept_test.go` around lines 55 - 56, The test's
diff argument order is inverted: change the cmp.Diff call in the accept_test.go
assertion from cmp.Diff(actual, testCase.expected) to
cmp.Diff(testCase.expected, actual) so the produced diff matches the "(-want
+got)" label; this affects the assertion around getAcceptRisks() where variables
actual and testCase.expected are compared using cmp.Diff.

}
})
}
}
5 changes: 2 additions & 3 deletions pkg/cli/admin/upgrade/rollback/rollback.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ func newOptions(streams genericiooptions.IOStreams) *options {
func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := newOptions(streams)
cmd := &cobra.Command{
Use: "rollback",
Hidden: true,
Short: "Rollback the cluster to the previous release.",
Use: "rollback",
Short: "Rollback the cluster to the previous release.",
Long: templates.LongDesc(`
Rollback the cluster to the previous release.

Expand Down
22 changes: 20 additions & 2 deletions pkg/cli/admin/upgrade/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
configv1client "github.com/openshift/client-go/config/clientset/versioned"
imagereference "github.com/openshift/library-go/pkg/image/reference"

"github.com/openshift/oc/pkg/cli/admin/upgrade/accept"
"github.com/openshift/oc/pkg/cli/admin/upgrade/channel"
"github.com/openshift/oc/pkg/cli/admin/upgrade/recommend"
"github.com/openshift/oc/pkg/cli/admin/upgrade/rollback"
Expand Down Expand Up @@ -126,6 +127,9 @@ func New(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_ROLLBACK").IsEnabled() {
cmd.AddCommand(rollback.New(f, streams))
}
if kcmdutil.FeatureGate("OC_ENABLE_CMD_UPGRADE_ACCEPT_RISKS").IsEnabled() {
cmd.AddCommand(accept.New(f, streams))
}
cmd.AddCommand(recommend.New(f, streams))

return cmd
Expand Down Expand Up @@ -218,8 +222,18 @@ func (o *Options) Run() error {
return nil
}
original := cv.Spec.DesiredUpdate
cv.Spec.DesiredUpdate = nil
updated, err := o.Client.ConfigV1().ClusterVersions().Patch(context.TODO(), cv.Name, types.MergePatchType, []byte(`{"spec":{"desiredUpdate":null}}`), metav1.PatchOptions{})
if original != nil && original.AcceptRisks != nil {
cv.Spec.DesiredUpdate = &configv1.Update{
AcceptRisks: original.AcceptRisks,
}
} else {
cv.Spec.DesiredUpdate = nil
}
bytes, err := json.Marshal(cv.Spec.DesiredUpdate)
if err != nil {
return fmt.Errorf("failed to marshal update: %v", err)
}
updated, err := o.Client.ConfigV1().ClusterVersions().Patch(context.TODO(), cv.Name, types.MergePatchType, []byte(fmt.Sprintf(`{"spec":{"desiredUpdate":%s}}`, bytes)), metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("Unable to cancel current rollout: %v", err)
}
Expand Down Expand Up @@ -252,6 +266,9 @@ func (o *Options) Run() error {
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.")
}

if update != nil {
update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
}
Comment on lines +269 to +271
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard cv.Spec.DesiredUpdate before reading AcceptRisks.

At Line [270] and Line [411], dereferencing cv.Spec.DesiredUpdate.AcceptRisks can panic when cv.Spec.DesiredUpdate == nil (valid state for clusters with no desired update set).

Proposed fix
-		if update != nil {
-			update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
-		}
+		if cv.Spec.DesiredUpdate != nil {
+			update.AcceptRisks = append([]configv1.AcceptRisk(nil), cv.Spec.DesiredUpdate.AcceptRisks...)
+		}
...
-		update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
+		if cv.Spec.DesiredUpdate != nil {
+			update.AcceptRisks = append([]configv1.AcceptRisk(nil), cv.Spec.DesiredUpdate.AcceptRisks...)
+		}

Also applies to: 411-411

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cli/admin/upgrade/upgrade.go` around lines 269 - 271, The code
dereferences cv.Spec.DesiredUpdate.AcceptRisks without guarding for nil; update
the logic around the assignment to update.AcceptRisks so you first check that
cv.Spec != nil and cv.Spec.DesiredUpdate != nil before reading AcceptRisks (and
fall back to a safe default, e.g. false, when DesiredUpdate is nil). Apply the
same nil-check fix at the other occurrence referencing
cv.Spec.DesiredUpdate.AcceptRisks (the second instance noted) so both reads are
protected and cannot panic.

if err := patchDesiredUpdate(ctx, update, o.Client, cv.Name); err != nil {

return err
Expand Down Expand Up @@ -391,6 +408,7 @@ func (o *Options) Run() error {
fmt.Fprintf(o.ErrOut, "warning: --allow-upgrade-with-warnings is bypassing: %s\n", err)
}

update.AcceptRisks = cv.Spec.DesiredUpdate.AcceptRisks
if err := patchDesiredUpdate(ctx, update, o.Client, cv.Name); err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions pkg/helpers/describe/describer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ var MissingDescriberGroupCoverageExceptions = []schema.GroupVersion{
{Group: "machine.openshift.io", Version: "v1beta1"},
{Group: "machine.openshift.io", Version: "v1"},
{Group: "sharedresource.openshift.io", Version: "v1alpha1"},
{Group: "apiextensions.openshift.io", Version: "v1alpha1"},
}

func TestDescriberCoverage(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion vendor/github.com/openshift/api/.ci-operator.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading