Skip to content

Commit a2f47e6

Browse files
jpbetzthockinaaron-prindleyongruilin
committed
Add validators: immutable
Co-authored-by: Tim Hockin <[email protected]> Co-authored-by: Aaron Prindle <[email protected]> Co-authored-by: Yongrui Lin <[email protected]>
1 parent 6305055 commit a2f47e6

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 validate
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/api/equality"
23+
"k8s.io/apimachinery/pkg/api/operation"
24+
"k8s.io/apimachinery/pkg/util/validation/field"
25+
)
26+
27+
// Immutable verifies that the specified value has not changed in the course of
28+
// an update operation. It does nothing if the old value is not provided. If
29+
// the caller needs to compare types that are not trivially comparable, they
30+
// should use ImmutableNonComparable instead.
31+
func Immutable[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T) field.ErrorList {
32+
if op.Type != operation.Update {
33+
return nil
34+
}
35+
if value == nil && oldValue == nil {
36+
return nil
37+
}
38+
if value == nil || oldValue == nil || *value != *oldValue {
39+
return field.ErrorList{
40+
field.Forbidden(fldPath, "field is immutable"),
41+
}
42+
}
43+
return nil
44+
}
45+
46+
// ImmutableNonComparable verifies that the specified value has not changed in
47+
// the course of an update operation. It does nothing if the old value is not
48+
// provided. Unlike Immutable, this function can be used with types that are
49+
// not directly comparable, at the cost of performance.
50+
func ImmutableNonComparable[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
51+
if op.Type != operation.Update {
52+
return nil
53+
}
54+
if !equality.Semantic.DeepEqual(value, oldValue) {
55+
return field.ErrorList{
56+
field.Forbidden(fldPath, "field is immutable"),
57+
}
58+
}
59+
return nil
60+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 validate
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
"k8s.io/apimachinery/pkg/api/operation"
24+
"k8s.io/apimachinery/pkg/util/validation/field"
25+
"k8s.io/utils/ptr"
26+
)
27+
28+
type Struct struct {
29+
S string
30+
I int
31+
B bool
32+
}
33+
34+
func TestImmutable(t *testing.T) {
35+
structA := Struct{"abc", 123, true}
36+
structB := Struct{"xyz", 456, false}
37+
38+
for _, tc := range []struct {
39+
name string
40+
fn func(operation.Operation, *field.Path) field.ErrorList
41+
fail bool
42+
}{{
43+
name: "nil both values",
44+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
45+
return Immutable[int](context.Background(), op, fld, nil, nil)
46+
},
47+
}, {
48+
name: "nil value",
49+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
50+
return Immutable(context.Background(), op, fld, nil, ptr.To(123))
51+
},
52+
fail: true,
53+
}, {
54+
name: "nil oldValue",
55+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
56+
return Immutable(context.Background(), op, fld, ptr.To(123), nil)
57+
},
58+
fail: true,
59+
}, {
60+
name: "int",
61+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
62+
return Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(123))
63+
},
64+
}, {
65+
name: "int fail",
66+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
67+
return Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(456))
68+
},
69+
fail: true,
70+
}, {
71+
name: "string",
72+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
73+
return Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
74+
},
75+
}, {
76+
name: "string fail",
77+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
78+
return Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
79+
},
80+
fail: true,
81+
}, {
82+
name: "bool",
83+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
84+
return Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(true))
85+
},
86+
}, {
87+
name: "bool fail",
88+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
89+
return Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(false))
90+
},
91+
fail: true,
92+
}, {
93+
name: "struct",
94+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
95+
return Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
96+
},
97+
}, {
98+
name: "struct fail",
99+
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
100+
return Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
101+
},
102+
fail: true,
103+
}} {
104+
t.Run(tc.name, func(t *testing.T) {
105+
errs := tc.fn(operation.Operation{Type: operation.Create}, field.NewPath(""))
106+
if len(errs) != 0 { // Create should always succeed
107+
t.Errorf("case %q (create): expected success: %v", tc.name, errs)
108+
}
109+
errs = tc.fn(operation.Operation{Type: operation.Update}, field.NewPath(""))
110+
if tc.fail && len(errs) == 0 {
111+
t.Errorf("case %q (update): expected failure", tc.name)
112+
} else if !tc.fail && len(errs) != 0 {
113+
t.Errorf("case %q (update): expected success: %v", tc.name, errs)
114+
}
115+
})
116+
}
117+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 validators
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/util/sets"
21+
"k8s.io/gengo/v2/types"
22+
)
23+
24+
const (
25+
immutableTagName = "k8s:immutable"
26+
)
27+
28+
func init() {
29+
RegisterTagValidator(immutableTagValidator{})
30+
}
31+
32+
type immutableTagValidator struct{}
33+
34+
func (immutableTagValidator) Init(_ Config) {}
35+
36+
func (immutableTagValidator) TagName() string {
37+
return immutableTagName
38+
}
39+
40+
var immutableTagValidScopes = sets.New(ScopeField, ScopeType, ScopeMapVal, ScopeListVal)
41+
42+
func (immutableTagValidator) ValidScopes() sets.Set[Scope] {
43+
return immutableTagValidScopes
44+
}
45+
46+
var (
47+
immutableValidator = types.Name{Package: libValidationPkg, Name: "Immutable"}
48+
immutableNonComparableValidator = types.Name{Package: libValidationPkg, Name: "ImmutableNonComparable"}
49+
)
50+
51+
func (immutableTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
52+
var result Validations
53+
54+
t := context.Type
55+
for t.Kind == types.Pointer || t.Kind == types.Alias {
56+
if t.Kind == types.Pointer {
57+
t = t.Elem
58+
} else if t.Kind == types.Alias {
59+
t = t.Underlying
60+
}
61+
}
62+
if t.IsComparable() {
63+
result.AddFunction(Function(immutableTagName, DefaultFlags, immutableValidator))
64+
} else {
65+
result.AddFunction(Function(immutableTagName, DefaultFlags, immutableNonComparableValidator))
66+
}
67+
68+
return result, nil
69+
}
70+
71+
func (itv immutableTagValidator) Docs() TagDoc {
72+
return TagDoc{
73+
Tag: itv.TagName(),
74+
Scopes: itv.ValidScopes().UnsortedList(),
75+
Description: "Indicates that a field may not be updated.",
76+
}
77+
}

0 commit comments

Comments
 (0)