mirror of
https://github.com/intel/intel-device-plugins-for-kubernetes.git
synced 2025-06-03 03:59:37 +00:00
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.
This commit is contained in:
parent
42689adc21
commit
2cb914225c
@ -80,6 +80,11 @@ type Patcher struct {
|
|||||||
afMap map[string]*fpgav2.AcceleratorFunction
|
afMap map[string]*fpgav2.AcceleratorFunction
|
||||||
resourceMap map[string]string
|
resourceMap map[string]string
|
||||||
resourceModeMap map[string]string
|
resourceModeMap map[string]string
|
||||||
|
|
||||||
|
// This set is needed to maintain the webhook's idempotence: it must be possible to
|
||||||
|
// resolve actual resources (AFs and regions) to themselves. For this we build
|
||||||
|
// a set of identities which must be accepted by the webhook without any transformation.
|
||||||
|
identitySet map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPatcher(log logr.Logger) *Patcher {
|
func newPatcher(log logr.Logger) *Patcher {
|
||||||
@ -88,6 +93,21 @@ func newPatcher(log logr.Logger) *Patcher {
|
|||||||
afMap: make(map[string]*fpgav2.AcceleratorFunction),
|
afMap: make(map[string]*fpgav2.AcceleratorFunction),
|
||||||
resourceMap: make(map[string]string),
|
resourceMap: make(map[string]string),
|
||||||
resourceModeMap: make(map[string]string),
|
resourceModeMap: make(map[string]string),
|
||||||
|
identitySet: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patcher) incIdentity(id string) {
|
||||||
|
// Initialize to 1 or increment by 1.
|
||||||
|
p.identitySet[id] = p.identitySet[id] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Patcher) decIdentity(id string) {
|
||||||
|
counter := p.identitySet[id]
|
||||||
|
if counter > 1 {
|
||||||
|
p.identitySet[id] = counter - 1
|
||||||
|
} else {
|
||||||
|
delete(p.identitySet, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,9 +123,13 @@ func (p *Patcher) AddAf(accfunc *fpgav2.AcceleratorFunction) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.resourceMap[namespace+"/"+accfunc.Name] = rfc6901Escaper.Replace(namespace + "/" + devtype)
|
mapping := rfc6901Escaper.Replace(namespace + "/" + devtype)
|
||||||
|
p.resourceMap[namespace+"/"+accfunc.Name] = mapping
|
||||||
|
p.incIdentity(mapping)
|
||||||
} else {
|
} else {
|
||||||
p.resourceMap[namespace+"/"+accfunc.Name] = rfc6901Escaper.Replace(namespace + "/region-" + accfunc.Spec.InterfaceID)
|
mapping := rfc6901Escaper.Replace(namespace + "/region-" + accfunc.Spec.InterfaceID)
|
||||||
|
p.resourceMap[namespace+"/"+accfunc.Name] = mapping
|
||||||
|
p.incIdentity(mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.resourceModeMap[namespace+"/"+accfunc.Name] = accfunc.Spec.Mode
|
p.resourceModeMap[namespace+"/"+accfunc.Name] = accfunc.Spec.Mode
|
||||||
@ -118,24 +142,32 @@ func (p *Patcher) AddRegion(region *fpgav2.FpgaRegion) {
|
|||||||
p.Lock()
|
p.Lock()
|
||||||
|
|
||||||
p.resourceModeMap[namespace+"/"+region.Name] = regiondevel
|
p.resourceModeMap[namespace+"/"+region.Name] = regiondevel
|
||||||
p.resourceMap[namespace+"/"+region.Name] = rfc6901Escaper.Replace(namespace + "/region-" + region.Spec.InterfaceID)
|
mapping := rfc6901Escaper.Replace(namespace + "/region-" + region.Spec.InterfaceID)
|
||||||
|
p.resourceMap[namespace+"/"+region.Name] = mapping
|
||||||
|
p.incIdentity(mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Patcher) RemoveAf(name string) {
|
func (p *Patcher) RemoveAf(name string) {
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
p.Lock()
|
p.Lock()
|
||||||
|
|
||||||
delete(p.afMap, namespace+"/"+name)
|
nname := namespace + "/" + name
|
||||||
delete(p.resourceMap, namespace+"/"+name)
|
|
||||||
delete(p.resourceModeMap, namespace+"/"+name)
|
p.decIdentity(p.resourceMap[nname])
|
||||||
|
delete(p.afMap, nname)
|
||||||
|
delete(p.resourceMap, nname)
|
||||||
|
delete(p.resourceModeMap, nname)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Patcher) RemoveRegion(name string) {
|
func (p *Patcher) RemoveRegion(name string) {
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
p.Lock()
|
p.Lock()
|
||||||
|
|
||||||
delete(p.resourceMap, namespace+"/"+name)
|
nname := namespace + "/" + name
|
||||||
delete(p.resourceModeMap, namespace+"/"+name)
|
|
||||||
|
p.decIdentity(p.resourceMap[nname])
|
||||||
|
delete(p.resourceMap, nname)
|
||||||
|
delete(p.resourceModeMap, nname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeContainer filters out env variables reserved for CRI hook.
|
// sanitizeContainer filters out env variables reserved for CRI hook.
|
||||||
@ -160,6 +192,16 @@ func sanitizeContainer(container corev1.Container) corev1.Container {
|
|||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Patcher) getNoopsOrError(name string) ([]string, error) {
|
||||||
|
if _, isVirtual := p.identitySet[rfc6901Escaper.Replace(name)]; isVirtual {
|
||||||
|
// `name` is not a real mapping, but a virtual one for an actual resource which
|
||||||
|
// needs to be resolved to itself with no transformations.
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Errorf("no such resource: %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Patcher) getPatchOps(containerIdx int, container corev1.Container) ([]string, error) {
|
func (p *Patcher) getPatchOps(containerIdx int, container corev1.Container) ([]string, error) {
|
||||||
container = sanitizeContainer(container)
|
container = sanitizeContainer(container)
|
||||||
|
|
||||||
@ -180,7 +222,7 @@ func (p *Patcher) getPatchOps(containerIdx int, container corev1.Container) ([]s
|
|||||||
for rname, quantity := range requestedResources {
|
for rname, quantity := range requestedResources {
|
||||||
mode, found := p.resourceModeMap[rname]
|
mode, found := p.resourceModeMap[rname]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errors.Errorf("no such resource: %q", rname)
|
return p.getNoopsOrError(rname)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -31,6 +31,18 @@ func init() {
|
|||||||
_ = flag.Set("v", "4")
|
_ = flag.Set("v", "4")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkExpectedError(t *testing.T, expectedErr bool, err error, testName string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if expectedErr && err == nil {
|
||||||
|
t.Errorf("Test case '%s': no error returned", testName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !expectedErr && err != nil {
|
||||||
|
t.Errorf("Test case '%s': unexpected error: %+v", testName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPatcherStorageFunctions(t *testing.T) {
|
func TestPatcherStorageFunctions(t *testing.T) {
|
||||||
goodAf := &fpgav2.AcceleratorFunction{
|
goodAf := &fpgav2.AcceleratorFunction{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -38,6 +50,8 @@ func TestPatcherStorageFunctions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Spec: fpgav2.AcceleratorFunctionSpec{
|
Spec: fpgav2.AcceleratorFunctionSpec{
|
||||||
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
|
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
|
||||||
|
InterfaceID: "ce48969398f05f33946d560708be108a",
|
||||||
|
Mode: af,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
brokenAf := &fpgav2.AcceleratorFunction{
|
brokenAf := &fpgav2.AcceleratorFunction{
|
||||||
@ -58,37 +72,103 @@ func TestPatcherStorageFunctions(t *testing.T) {
|
|||||||
InterfaceID: "ce48969398f05f33946d560708be108a",
|
InterfaceID: "ce48969398f05f33946d560708be108a",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
regionAlias := &fpgav2.FpgaRegion{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "arria10-alias",
|
||||||
|
},
|
||||||
|
Spec: fpgav2.FpgaRegionSpec{
|
||||||
|
InterfaceID: "ce48969398f05f33946d560708be108a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tcases := []struct {
|
||||||
|
name string
|
||||||
|
afsToAdd []*fpgav2.AcceleratorFunction
|
||||||
|
afsToRemove []*fpgav2.AcceleratorFunction
|
||||||
|
regionsToAdd []*fpgav2.FpgaRegion
|
||||||
|
regionsToRemove []*fpgav2.FpgaRegion
|
||||||
|
expectedResourceModeMapSize int
|
||||||
|
expectedResourceMapSize int
|
||||||
|
expectedIdentitySetSize int
|
||||||
|
expectedErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Add one good af",
|
||||||
|
afsToAdd: []*fpgav2.AcceleratorFunction{goodAf},
|
||||||
|
expectedResourceModeMapSize: 1,
|
||||||
|
expectedResourceMapSize: 1,
|
||||||
|
expectedIdentitySetSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one broken af",
|
||||||
|
afsToAdd: []*fpgav2.AcceleratorFunction{brokenAf},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one and remove one good af",
|
||||||
|
afsToAdd: []*fpgav2.AcceleratorFunction{goodAf},
|
||||||
|
afsToRemove: []*fpgav2.AcceleratorFunction{goodAf},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one good region",
|
||||||
|
regionsToAdd: []*fpgav2.FpgaRegion{region},
|
||||||
|
expectedResourceModeMapSize: 1,
|
||||||
|
expectedResourceMapSize: 1,
|
||||||
|
expectedIdentitySetSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one and remove one good region",
|
||||||
|
regionsToAdd: []*fpgav2.FpgaRegion{region},
|
||||||
|
regionsToRemove: []*fpgav2.FpgaRegion{region},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one region and one alias then remove one alias",
|
||||||
|
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
|
||||||
|
regionsToRemove: []*fpgav2.FpgaRegion{region},
|
||||||
|
expectedResourceModeMapSize: 1,
|
||||||
|
expectedResourceMapSize: 1,
|
||||||
|
expectedIdentitySetSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one region and one alias",
|
||||||
|
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
|
||||||
|
expectedResourceModeMapSize: 2,
|
||||||
|
expectedResourceMapSize: 2,
|
||||||
|
expectedIdentitySetSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add one region and one alias then remove all",
|
||||||
|
regionsToAdd: []*fpgav2.FpgaRegion{region, regionAlias},
|
||||||
|
regionsToRemove: []*fpgav2.FpgaRegion{region, regionAlias},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tcases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
p := newPatcher(ctrl.Log.WithName("test"))
|
p := newPatcher(ctrl.Log.WithName("test"))
|
||||||
|
for _, af := range tt.afsToAdd {
|
||||||
if err := p.AddAf(goodAf); err != nil {
|
err := p.AddAf(af)
|
||||||
t.Error("unexpected error")
|
checkExpectedError(t, tt.expectedErr, err, tt.name)
|
||||||
}
|
}
|
||||||
|
for _, reg := range tt.regionsToAdd {
|
||||||
if len(p.resourceModeMap) != 1 || len(p.afMap) != 1 || len(p.resourceMap) != 1 {
|
p.AddRegion(reg)
|
||||||
t.Error("Failed to add AF to patcher")
|
|
||||||
}
|
}
|
||||||
|
for _, af := range tt.afsToRemove {
|
||||||
if err := p.AddAf(brokenAf); err == nil {
|
p.RemoveAf(af.Name)
|
||||||
t.Error("AddAf() must fail")
|
|
||||||
}
|
}
|
||||||
|
for _, reg := range tt.regionsToRemove {
|
||||||
p.RemoveAf(goodAf.Name)
|
p.RemoveRegion(reg.Name)
|
||||||
|
|
||||||
if len(p.resourceModeMap) != 0 || len(p.afMap) != 0 || len(p.resourceMap) != 0 {
|
|
||||||
t.Error("Failed to remove AF from patcher")
|
|
||||||
}
|
}
|
||||||
|
if tt.expectedResourceModeMapSize != len(p.resourceModeMap) {
|
||||||
p.AddRegion(region)
|
t.Errorf("wrong size of resourceModeMap. Expected %d, but got %d", tt.expectedResourceModeMapSize, len(p.resourceModeMap))
|
||||||
|
|
||||||
if len(p.resourceModeMap) != 1 || len(p.resourceMap) != 1 {
|
|
||||||
t.Error("Failed to add fpga region to patcher")
|
|
||||||
}
|
}
|
||||||
|
if tt.expectedResourceMapSize != len(p.resourceMap) {
|
||||||
p.RemoveRegion(region.Name)
|
t.Errorf("wrong size of resourceMap. Expected %d, but got %d", tt.expectedResourceMapSize, len(p.resourceMap))
|
||||||
|
}
|
||||||
if len(p.resourceModeMap) != 0 || len(p.resourceMap) != 0 {
|
if tt.expectedIdentitySetSize != len(p.identitySet) {
|
||||||
t.Error("Failed to remove fpga region from patcher")
|
t.Errorf("wrong size of identitySet. Expected %d, but got %d", tt.expectedIdentitySetSize, len(p.identitySet))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,6 +325,41 @@ func TestGetPatchOps(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedOps: 4,
|
expectedOps: 4,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Skip handling for an identity mapping",
|
||||||
|
container: corev1.Container{
|
||||||
|
Resources: corev1.ResourceRequirements{
|
||||||
|
Limits: corev1.ResourceList{
|
||||||
|
"fpga.intel.com/af-ce4.d84.zkiWk5jwXzOUbVYHCL4QithCTcSko8QT-J5DNoP5BAs": resource.MustParse("1"),
|
||||||
|
},
|
||||||
|
Requests: corev1.ResourceList{
|
||||||
|
"fpga.intel.com/af-ce4.d84.zkiWk5jwXzOUbVYHCL4QithCTcSko8QT-J5DNoP5BAs": resource.MustParse("1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afs: []*fpgav2.AcceleratorFunction{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "arria10-nlb0",
|
||||||
|
},
|
||||||
|
Spec: fpgav2.AcceleratorFunctionSpec{
|
||||||
|
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
|
||||||
|
InterfaceID: "ce48969398f05f33946d560708be108a",
|
||||||
|
Mode: af,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "arria10-nlb0-alias",
|
||||||
|
},
|
||||||
|
Spec: fpgav2.AcceleratorFunctionSpec{
|
||||||
|
AfuID: "d8424dc4a4a3c413f89e433683f9040b",
|
||||||
|
InterfaceID: "ce48969398f05f33946d560708be108a",
|
||||||
|
Mode: af,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Successful handling for regiondevel mode",
|
name: "Successful handling for regiondevel mode",
|
||||||
container: corev1.Container{
|
container: corev1.Container{
|
||||||
@ -399,12 +514,7 @@ func TestGetPatchOps(t *testing.T) {
|
|||||||
p.AddRegion(region)
|
p.AddRegion(region)
|
||||||
}
|
}
|
||||||
ops, err := p.getPatchOps(0, tt.container)
|
ops, err := p.getPatchOps(0, tt.container)
|
||||||
if tt.expectedErr && err == nil {
|
checkExpectedError(t, tt.expectedErr, err, tt.name)
|
||||||
t.Errorf("Test case '%s': no error returned", tt.name)
|
|
||||||
}
|
|
||||||
if !tt.expectedErr && err != nil {
|
|
||||||
t.Errorf("Test case '%s': unexpected error: %+v", tt.name, err)
|
|
||||||
}
|
|
||||||
if len(ops) != tt.expectedOps {
|
if len(ops) != tt.expectedOps {
|
||||||
t.Errorf("test case '%s': expected %d ops, but got %d\n%v", tt.name, tt.expectedOps, len(ops), ops)
|
t.Errorf("test case '%s': expected %d ops, but got %d\n%v", tt.name, tt.expectedOps, len(ops), ops)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func GetRequestedResources(container corev1.Container, ns string) (map[string]in
|
|||||||
// Container may happen to have Requests, but not Limits. Check Requests first,
|
// Container may happen to have Requests, but not Limits. Check Requests first,
|
||||||
// then in the next loop iterate over Limits.
|
// then in the next loop iterate over Limits.
|
||||||
for resourceName, resourceQuantity := range container.Resources.Requests {
|
for resourceName, resourceQuantity := range container.Resources.Requests {
|
||||||
rname := strings.ToLower(string(resourceName))
|
rname := string(resourceName)
|
||||||
if !strings.HasPrefix(rname, ns) {
|
if !strings.HasPrefix(rname, ns) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ func GetRequestedResources(container corev1.Container, ns string) (map[string]in
|
|||||||
resources := make(map[string]int64)
|
resources := make(map[string]int64)
|
||||||
|
|
||||||
for resourceName, resourceQuantity := range container.Resources.Limits {
|
for resourceName, resourceQuantity := range container.Resources.Limits {
|
||||||
rname := strings.ToLower(string(resourceName))
|
rname := string(resourceName)
|
||||||
if !strings.HasPrefix(rname, ns) {
|
if !strings.HasPrefix(rname, ns) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user