Skip to content

Commit 2cb9142

Browse files
committed
fpga_webhook: never reject already mutated CRs
For some reason the API server may want to pass an already mutated CR through the webhook once again. The webhook must accept such CR with no additional transformations. This patch adds support for such idempotence by maintaining a set of identity mappings which effectively resolve to themselves. No patching is applied to them.
1 parent 42689ad commit 2cb9142

File tree

3 files changed

+197
-45
lines changed

3 files changed

+197
-45
lines changed

pkg/fpgacontroller/patcher/patcher.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ type Patcher struct {
8080
afMap map[string]*fpgav2.AcceleratorFunction
8181
resourceMap map[string]string
8282
resourceModeMap map[string]string
83+
84+
// This set is needed to maintain the webhook's idempotence: it must be possible to
85+
// resolve actual resources (AFs and regions) to themselves. For this we build
86+
// a set of identities which must be accepted by the webhook without any transformation.
87+
identitySet map[string]int
8388
}
8489

8590
func newPatcher(log logr.Logger) *Patcher {
@@ -88,6 +93,21 @@ func newPatcher(log logr.Logger) *Patcher {
8893
afMap: make(map[string]*fpgav2.AcceleratorFunction),
8994
resourceMap: make(map[string]string),
9095
resourceModeMap: make(map[string]string),
96+
identitySet: make(map[string]int),
97+
}
98+
}
99+
100+
func (p *Patcher) incIdentity(id string) {
101+
// Initialize to 1 or increment by 1.
102+
p.identitySet[id] = p.identitySet[id] + 1
103+
}
104+
105+
func (p *Patcher) decIdentity(id string) {
106+
counter := p.identitySet[id]
107+
if counter > 1 {
108+
p.identitySet[id] = counter - 1
109+
} else {
110+
delete(p.identitySet, id)
91111
}
92112
}
93113

@@ -103,9 +123,13 @@ func (p *Patcher) AddAf(accfunc *fpgav2.AcceleratorFunction) error {
103123
return err
104124
}
105125

106-
p.resourceMap[namespace+"/"+accfunc.Name] = rfc6901Escaper.Replace(namespace + "/" + devtype)
126+
mapping := rfc6901Escaper.Replace(namespace + "/" + devtype)
127+
p.resourceMap[namespace+"/"+accfunc.Name] = mapping
128+
p.incIdentity(mapping)
107129
} else {
108-
p.resourceMap[namespace+"/"+accfunc.Name] = rfc6901Escaper.Replace(namespace + "/region-" + accfunc.Spec.InterfaceID)
130+
mapping := rfc6901Escaper.Replace(namespace + "/region-" + accfunc.Spec.InterfaceID)
131+
p.resourceMap[namespace+"/"+accfunc.Name] = mapping
132+
p.incIdentity(mapping)
109133
}
110134

111135
p.resourceModeMap[namespace+"/"+accfunc.Name] = accfunc.Spec.Mode
@@ -118,24 +142,32 @@ func (p *Patcher) AddRegion(region *fpgav2.FpgaRegion) {
118142
p.Lock()
119143

120144
p.resourceModeMap[namespace+"/"+region.Name] = regiondevel
121-
p.resourceMap[namespace+"/"+region.Name] = rfc6901Escaper.Replace(namespace + "/region-" + region.Spec.InterfaceID)
145+
mapping := rfc6901Escaper.Replace(namespace + "/region-" + region.Spec.InterfaceID)
146+
p.resourceMap[namespace+"/"+region.Name] = mapping
147+
p.incIdentity(mapping)
122148
}
123149

124150
func (p *Patcher) RemoveAf(name string) {
125151
defer p.Unlock()
126152
p.Lock()
127153

128-
delete(p.afMap, namespace+"/"+name)
129-
delete(p.resourceMap, namespace+"/"+name)
130-
delete(p.resourceModeMap, namespace+"/"+name)
154+
nname := namespace + "/" + name
155+
156+
p.decIdentity(p.resourceMap[nname])
157+
delete(p.afMap, nname)
158+
delete(p.resourceMap, nname)
159+
delete(p.resourceModeMap, nname)
131160
}
132161

133162
func (p *Patcher) RemoveRegion(name string) {
134163
defer p.Unlock()
135164
p.Lock()
136165

137-
delete(p.resourceMap, namespace+"/"+name)
138-
delete(p.resourceModeMap, namespace+"/"+name)
166+
nname := namespace + "/" + name
167+
168+
p.decIdentity(p.resourceMap[nname])
169+
delete(p.resourceMap, nname)
170+
delete(p.resourceModeMap, nname)
139171
}
140172

141173
// sanitizeContainer filters out env variables reserved for CRI hook.
@@ -160,6 +192,16 @@ func sanitizeContainer(container corev1.Container) corev1.Container {
160192
return container
161193
}
162194

195+
func (p *Patcher) getNoopsOrError(name string) ([]string, error) {
196+
if _, isVirtual := p.identitySet[rfc6901Escaper.Replace(name)]; isVirtual {
197+
// `name` is not a real mapping, but a virtual one for an actual resource which
198+
// needs to be resolved to itself with no transformations.
199+
return []string{}, nil
200+
}
201+
202+
return nil, errors.Errorf("no such resource: %q", name)
203+
}
204+
163205
func (p *Patcher) getPatchOps(containerIdx int, container corev1.Container) ([]string, error) {
164206
container = sanitizeContainer(container)
165207

@@ -180,7 +222,7 @@ func (p *Patcher) getPatchOps(containerIdx int, container corev1.Container) ([]s
180222
for rname, quantity := range requestedResources {
181223
mode, found := p.resourceModeMap[rname]
182224
if !found {
183-
return nil, errors.Errorf("no such resource: %q", rname)
225+
return p.getNoopsOrError(rname)
184226
}
185227

186228
switch mode {

pkg/fpgacontroller/patcher/patcher_test.go

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,27 @@ func init() {
3131
_ = flag.Set("v", "4")
3232
}
3333

34+
func checkExpectedError(t *testing.T, expectedErr bool, err error, testName string) {
35+
t.Helper()
36+
37+
if expectedErr && err == nil {
38+
t.Errorf("Test case '%s': no error returned", testName)
39+
}
40+
41+
if !expectedErr && err != nil {
42+
t.Errorf("Test case '%s': unexpected error: %+v", testName, err)
43+
}
44+
}
45+
3446
func TestPatcherStorageFunctions(t *testing.T) {
3547
goodAf := &fpgav2.AcceleratorFunction{
3648
ObjectMeta: metav1.ObjectMeta{
3749
Name: "arria10-nlb0",
3850
},
3951
Spec: fpgav2.AcceleratorFunctionSpec{
40-
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
52+
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
53+
InterfaceID: "ce48969398f05f33946d560708be108a",
54+
Mode: af,
4155
},
4256
}
4357
brokenAf := &fpgav2.AcceleratorFunction{
@@ -58,37 +72,103 @@ func TestPatcherStorageFunctions(t *testing.T) {
5872
InterfaceID: "ce48969398f05f33946d560708be108a",
5973
},
6074
}
61-
62-
p := newPatcher(ctrl.Log.WithName("test"))
63-
64-
if err := p.AddAf(goodAf); err != nil {
65-
t.Error("unexpected error")
66-
}
67-
68-
if len(p.resourceModeMap) != 1 || len(p.afMap) != 1 || len(p.resourceMap) != 1 {
69-
t.Error("Failed to add AF to patcher")
70-
}
71-
72-
if err := p.AddAf(brokenAf); err == nil {
73-
t.Error("AddAf() must fail")
74-
}
75-
76-
p.RemoveAf(goodAf.Name)
77-
78-
if len(p.resourceModeMap) != 0 || len(p.afMap) != 0 || len(p.resourceMap) != 0 {
79-
t.Error("Failed to remove AF from patcher")
75+
regionAlias := &fpgav2.FpgaRegion{
76+
ObjectMeta: metav1.ObjectMeta{
77+
Name: "arria10-alias",
78+
},
79+
Spec: fpgav2.FpgaRegionSpec{
80+
InterfaceID: "ce48969398f05f33946d560708be108a",
81+
},
8082
}
8183

82-
p.AddRegion(region)
83-
84-
if len(p.resourceModeMap) != 1 || len(p.resourceMap) != 1 {
85-
t.Error("Failed to add fpga region to patcher")
84+
tcases := []struct {
85+
name string
86+
afsToAdd []*fpgav2.AcceleratorFunction
87+
afsToRemove []*fpgav2.AcceleratorFunction
88+
regionsToAdd []*fpgav2.FpgaRegion
89+
regionsToRemove []*fpgav2.FpgaRegion
90+
expectedResourceModeMapSize int
91+
expectedResourceMapSize int
92+
expectedIdentitySetSize int
93+
expectedErr bool
94+
}{
95+
{
96+
name: "Add one good af",
97+
afsToAdd: []*fpgav2.AcceleratorFunction{goodAf},
98+
expectedResourceModeMapSize: 1,
99+
expectedResourceMapSize: 1,
100+
expectedIdentitySetSize: 1,
101+
},
102+
{
103+
name: "Add one broken af",
104+
afsToAdd: []*fpgav2.AcceleratorFunction{brokenAf},
105+
expectedErr: true,
106+
},
107+
{
108+
name: "Add one and remove one good af",
109+
afsToAdd: []*fpgav2.AcceleratorFunction{goodAf},
110+
afsToRemove: []*fpgav2.AcceleratorFunction{goodAf},
111+
},
112+
{
113+
name: "Add one good region",
114+
regionsToAdd: []*fpgav2.FpgaRegion{region},
115+
expectedResourceModeMapSize: 1,
116+
expectedResourceMapSize: 1,
117+
expectedIdentitySetSize: 1,
118+
},
119+
{
120+
name: "Add one and remove one good region",
121+
regionsToAdd: []*fpgav2.FpgaRegion{region},
122+
regionsToRemove: []*fpgav2.FpgaRegion{region},
123+
},
124+
{
125+
name: "Add one region and one alias then remove one alias",
126+
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
127+
regionsToRemove: []*fpgav2.FpgaRegion{region},
128+
expectedResourceModeMapSize: 1,
129+
expectedResourceMapSize: 1,
130+
expectedIdentitySetSize: 1,
131+
},
132+
{
133+
name: "Add one region and one alias",
134+
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
135+
expectedResourceModeMapSize: 2,
136+
expectedResourceMapSize: 2,
137+
expectedIdentitySetSize: 1,
138+
},
139+
{
140+
name: "Add one region and one alias then remove all",
141+
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
142+
regionsToRemove: []*fpgav2.FpgaRegion{region, regionAlias},
143+
},
86144
}
87145

88-
p.RemoveRegion(region.Name)
89-
90-
if len(p.resourceModeMap) != 0 || len(p.resourceMap) != 0 {
91-
t.Error("Failed to remove fpga region from patcher")
146+
for _, tt := range tcases {
147+
t.Run(tt.name, func(t *testing.T) {
148+
p := newPatcher(ctrl.Log.WithName("test"))
149+
for _, af := range tt.afsToAdd {
150+
err := p.AddAf(af)
151+
checkExpectedError(t, tt.expectedErr, err, tt.name)
152+
}
153+
for _, reg := range tt.regionsToAdd {
154+
p.AddRegion(reg)
155+
}
156+
for _, af := range tt.afsToRemove {
157+
p.RemoveAf(af.Name)
158+
}
159+
for _, reg := range tt.regionsToRemove {
160+
p.RemoveRegion(reg.Name)
161+
}
162+
if tt.expectedResourceModeMapSize != len(p.resourceModeMap) {
163+
t.Errorf("wrong size of resourceModeMap. Expected %d, but got %d", tt.expectedResourceModeMapSize, len(p.resourceModeMap))
164+
}
165+
if tt.expectedResourceMapSize != len(p.resourceMap) {
166+
t.Errorf("wrong size of resourceMap. Expected %d, but got %d", tt.expectedResourceMapSize, len(p.resourceMap))
167+
}
168+
if tt.expectedIdentitySetSize != len(p.identitySet) {
169+
t.Errorf("wrong size of identitySet. Expected %d, but got %d", tt.expectedIdentitySetSize, len(p.identitySet))
170+
}
171+
})
92172
}
93173
}
94174

@@ -245,6 +325,41 @@ func TestGetPatchOps(t *testing.T) {
245325
},
246326
expectedOps: 4,
247327
},
328+
{
329+
name: "Skip handling for an identity mapping",
330+
container: corev1.Container{
331+
Resources: corev1.ResourceRequirements{
332+
Limits: corev1.ResourceList{
333+
"fpga.intel.com/af-ce4.d84.zkiWk5jwXzOUbVYHCL4QithCTcSko8QT-J5DNoP5BAs": resource.MustParse("1"),
334+
},
335+
Requests: corev1.ResourceList{
336+
"fpga.intel.com/af-ce4.d84.zkiWk5jwXzOUbVYHCL4QithCTcSko8QT-J5DNoP5BAs": resource.MustParse("1"),
337+
},
338+
},
339+
},
340+
afs: []*fpgav2.AcceleratorFunction{
341+
{
342+
ObjectMeta: metav1.ObjectMeta{
343+
Name: "arria10-nlb0",
344+
},
345+
Spec: fpgav2.AcceleratorFunctionSpec{
346+
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
347+
InterfaceID: "ce48969398f05f33946d560708be108a",
348+
Mode: af,
349+
},
350+
},
351+
{
352+
ObjectMeta: metav1.ObjectMeta{
353+
Name: "arria10-nlb0-alias",
354+
},
355+
Spec: fpgav2.AcceleratorFunctionSpec{
356+
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
357+
InterfaceID: "ce48969398f05f33946d560708be108a",
358+
Mode: af,
359+
},
360+
},
361+
},
362+
},
248363
{
249364
name: "Successful handling for regiondevel mode",
250365
container: corev1.Container{
@@ -399,12 +514,7 @@ func TestGetPatchOps(t *testing.T) {
399514
p.AddRegion(region)
400515
}
401516
ops, err := p.getPatchOps(0, tt.container)
402-
if tt.expectedErr && err == nil {
403-
t.Errorf("Test case '%s': no error returned", tt.name)
404-
}
405-
if !tt.expectedErr && err != nil {
406-
t.Errorf("Test case '%s': unexpected error: %+v", tt.name, err)
407-
}
517+
checkExpectedError(t, tt.expectedErr, err, tt.name)
408518
if len(ops) != tt.expectedOps {
409519
t.Errorf("test case '%s': expected %d ops, but got %d\n%v", tt.name, tt.expectedOps, len(ops), ops)
410520
}

pkg/internal/containers/containers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func GetRequestedResources(container corev1.Container, ns string) (map[string]in
2727
// Container may happen to have Requests, but not Limits. Check Requests first,
2828
// then in the next loop iterate over Limits.
2929
for resourceName, resourceQuantity := range container.Resources.Requests {
30-
rname := strings.ToLower(string(resourceName))
30+
rname := string(resourceName)
3131
if !strings.HasPrefix(rname, ns) {
3232
continue
3333
}
@@ -42,7 +42,7 @@ func GetRequestedResources(container corev1.Container, ns string) (map[string]in
4242
resources := make(map[string]int64)
4343

4444
for resourceName, resourceQuantity := range container.Resources.Limits {
45-
rname := strings.ToLower(string(resourceName))
45+
rname := string(resourceName)
4646
if !strings.HasPrefix(rname, ns) {
4747
continue
4848
}

0 commit comments

Comments
 (0)