Skip to content

Commit cc34cc0

Browse files
Merge pull request #130916 from richabanker/oidc-flags-v3
Remove mutation of authn options by binding flag setters to a tracking bool in options Kubernetes-commit: ce87977639233e45bdef18e5f94c5144f35a7ee9
2 parents 4906cf7 + 4ee7f8c commit cc34cc0

File tree

2 files changed

+367
-0
lines changed

2 files changed

+367
-0
lines changed

cli/flag/tracker_flag.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package flag
18+
19+
import (
20+
"github.com/spf13/pflag"
21+
)
22+
23+
// TrackerValue wraps a non-boolean value and stores true in the provided boolean when it is set.
24+
type TrackerValue struct {
25+
value pflag.Value
26+
provided *bool
27+
}
28+
29+
// BoolTrackerValue wraps a boolean value and stores true in the provided boolean when it is set.
30+
type BoolTrackerValue struct {
31+
boolValue
32+
provided *bool
33+
}
34+
35+
type boolValue interface {
36+
pflag.Value
37+
IsBoolFlag() bool
38+
}
39+
40+
var _ pflag.Value = &TrackerValue{}
41+
var _ boolValue = &BoolTrackerValue{}
42+
43+
// NewTracker returns a Value wrapping the given value which stores true in the provided boolean when it is set.
44+
func NewTracker(value pflag.Value, provided *bool) pflag.Value {
45+
if value == nil {
46+
panic("value must not be nil")
47+
}
48+
49+
if provided == nil {
50+
panic("provided boolean must not be nil")
51+
}
52+
53+
if boolValue, ok := value.(boolValue); ok {
54+
return &BoolTrackerValue{boolValue: boolValue, provided: provided}
55+
}
56+
return &TrackerValue{value: value, provided: provided}
57+
}
58+
59+
func (f *TrackerValue) String() string {
60+
return f.value.String()
61+
}
62+
63+
func (f *TrackerValue) Set(value string) error {
64+
err := f.value.Set(value)
65+
if err == nil {
66+
*f.provided = true
67+
}
68+
return err
69+
}
70+
71+
func (f *TrackerValue) Type() string {
72+
return f.value.Type()
73+
}
74+
75+
func (f *BoolTrackerValue) Set(value string) error {
76+
err := f.boolValue.Set(value)
77+
if err == nil {
78+
*f.provided = true
79+
}
80+
81+
return err
82+
}

cli/flag/tracker_flag_test.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package flag
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/spf13/pflag"
24+
)
25+
26+
func TestNewTracker(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
value pflag.Value
30+
provided *bool
31+
wantType string
32+
}{
33+
{
34+
name: "non-bool-tracker",
35+
value: &nonBoolFlagMockValue{val: "initial", typ: "string"},
36+
provided: new(bool),
37+
wantType: "string",
38+
},
39+
{
40+
name: "bool-tracker",
41+
value: &boolFlagMockValue{val: "false", typ: "bool", isBool: true},
42+
provided: new(bool),
43+
wantType: "bool",
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
tracker := NewTracker(tt.value, tt.provided)
50+
51+
if tracker.Type() != tt.wantType {
52+
t.Errorf("Want type %s, got %s", tt.wantType, tracker.Type())
53+
}
54+
55+
if trackerValue, ok := tracker.(*TrackerValue); ok {
56+
if trackerValue.provided != tt.provided {
57+
t.Errorf("Provided pointer not stored correctly in TrackerValue")
58+
}
59+
} else if boolTrackerValue, ok := tracker.(*BoolTrackerValue); ok {
60+
if boolTrackerValue.provided != tt.provided {
61+
t.Errorf("Provided pointer not stored correctly in BoolTrackerValue")
62+
}
63+
}
64+
})
65+
}
66+
}
67+
68+
func TestNewTrackerPanics(t *testing.T) {
69+
tests := []struct {
70+
name string
71+
value pflag.Value
72+
provided *bool
73+
panicMsg string
74+
}{
75+
{
76+
name: "nil-value",
77+
value: nil,
78+
provided: new(bool),
79+
panicMsg: "value must not be nil",
80+
},
81+
{
82+
name: "nil-provided",
83+
value: &boolFlagMockValue{val: "test"},
84+
provided: nil,
85+
panicMsg: "provided boolean must not be nil",
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
defer func() {
92+
if r := recover(); r == nil {
93+
t.Errorf("expected panic, but did not panic")
94+
} else if r != tt.panicMsg {
95+
t.Errorf("expected panic message %q, got %q", tt.panicMsg, r)
96+
}
97+
}()
98+
NewTracker(tt.value, tt.provided)
99+
})
100+
}
101+
}
102+
103+
func TestTrackerValue_String(t *testing.T) {
104+
testCases := []struct {
105+
name string
106+
mockValue pflag.Value
107+
want string
108+
}{
109+
{
110+
name: "bool-flag",
111+
mockValue: &boolFlagMockValue{val: "bool-test"},
112+
want: "bool-test",
113+
},
114+
{
115+
name: "non-bool-flag",
116+
mockValue: &nonBoolFlagMockValue{val: "non-bool-test"},
117+
want: "non-bool-test",
118+
},
119+
}
120+
121+
for _, tc := range testCases {
122+
t.Run(tc.name, func(t *testing.T) {
123+
tracker := NewTracker(tc.mockValue, new(bool))
124+
result := tracker.String()
125+
if result != tc.want {
126+
t.Errorf("Want %q, but got %q", tc.want, result)
127+
}
128+
})
129+
}
130+
}
131+
132+
func TestTrackerValue_Set(t *testing.T) {
133+
testCases := []struct {
134+
name string
135+
mockValue pflag.Value
136+
provided *bool
137+
mockErr error
138+
wantProvided bool
139+
wantErr bool
140+
}{
141+
{
142+
name: "success-bool-flag",
143+
mockValue: &boolFlagMockValue{val: "bool-test"},
144+
provided: new(bool),
145+
wantProvided: true,
146+
wantErr: false,
147+
},
148+
{
149+
name: "success-non-bool-flag",
150+
mockValue: &nonBoolFlagMockValue{val: "bool-test"},
151+
provided: new(bool),
152+
wantProvided: true,
153+
wantErr: false,
154+
},
155+
{
156+
name: "error-bool-flag",
157+
mockValue: &boolFlagMockValue{val: "bool-test", err: fmt.Errorf("set error")},
158+
provided: new(bool),
159+
wantProvided: false,
160+
wantErr: true,
161+
},
162+
{
163+
name: "error-non-bool-flag",
164+
mockValue: &nonBoolFlagMockValue{val: "bool-test", err: fmt.Errorf("set error")},
165+
provided: new(bool),
166+
wantProvided: false,
167+
wantErr: true,
168+
},
169+
}
170+
171+
for _, tc := range testCases {
172+
t.Run(tc.name, func(t *testing.T) {
173+
tracker := NewTracker(tc.mockValue, tc.provided)
174+
err := tracker.Set("new value")
175+
176+
if (err != nil) != tc.wantErr {
177+
t.Errorf("Want error: %v, got: %v", tc.wantErr, err != nil)
178+
}
179+
180+
if *tc.provided != tc.wantProvided {
181+
t.Errorf("Want provided to be %v, got: %v", tc.wantProvided, *tc.provided)
182+
}
183+
})
184+
}
185+
}
186+
187+
func TestTrackerValue_MultipleSetCalls(t *testing.T) {
188+
provided := false
189+
mock := &boolFlagMockValue{val: "initial"}
190+
tracker := NewTracker(mock, &provided)
191+
192+
err := tracker.Set("new value")
193+
if err != nil {
194+
t.Errorf("Unexpected error: %v", err)
195+
}
196+
if mock.val != "new value" {
197+
t.Errorf("Expected mock value to be 'new value', got '%s'", mock.val)
198+
}
199+
if !provided {
200+
t.Error("Expected 'provided' to be true, got false")
201+
}
202+
203+
provided = false // reset
204+
mock.err = fmt.Errorf("set error")
205+
err = tracker.Set("failed set")
206+
207+
if err == nil {
208+
t.Errorf("Expected an error, got nil")
209+
}
210+
if provided {
211+
t.Error("Expected 'provided' to be false after error, got true")
212+
}
213+
}
214+
215+
func TestTrackerValue_Type(t *testing.T) {
216+
testCases := []struct {
217+
name string
218+
mockValue pflag.Value
219+
want string
220+
}{
221+
{
222+
name: "success-bool-flag",
223+
mockValue: &boolFlagMockValue{typ: "mockBoolType"},
224+
want: "mockBoolType",
225+
},
226+
{
227+
name: "success-non-bool-flag",
228+
mockValue: &nonBoolFlagMockValue{typ: "mockNonBoolType"},
229+
want: "mockNonBoolType",
230+
},
231+
}
232+
233+
for _, tc := range testCases {
234+
t.Run(tc.name, func(t *testing.T) {
235+
tracker := NewTracker(tc.mockValue, new(bool))
236+
result := tracker.Type()
237+
if result != tc.want {
238+
t.Errorf("Want %q, but got %q", tc.want, result)
239+
}
240+
})
241+
}
242+
}
243+
244+
type boolFlagMockValue struct {
245+
val string
246+
typ string
247+
isBool bool
248+
err error
249+
}
250+
251+
func (m *boolFlagMockValue) String() string {
252+
return m.val
253+
}
254+
255+
func (m *boolFlagMockValue) Set(value string) error {
256+
m.val = value
257+
return m.err
258+
}
259+
260+
func (m *boolFlagMockValue) Type() string {
261+
return m.typ
262+
}
263+
264+
func (m *boolFlagMockValue) IsBoolFlag() bool {
265+
return m.isBool
266+
}
267+
268+
type nonBoolFlagMockValue struct {
269+
val string
270+
typ string
271+
err error
272+
}
273+
274+
func (m *nonBoolFlagMockValue) String() string {
275+
return m.val
276+
}
277+
278+
func (m *nonBoolFlagMockValue) Set(value string) error {
279+
m.val = value
280+
return m.err
281+
}
282+
283+
func (m *nonBoolFlagMockValue) Type() string {
284+
return m.typ
285+
}

0 commit comments

Comments
 (0)