diff --git a/pkg/security/controller/namespace_scc_allocation_controller.go b/pkg/security/controller/namespace_scc_allocation_controller.go index 7b4160991..faf89cf68 100644 --- a/pkg/security/controller/namespace_scc_allocation_controller.go +++ b/pkg/security/controller/namespace_scc_allocation_controller.go @@ -6,19 +6,23 @@ import ( "reflect" "time" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/klog" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + runtimejson "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/apimachinery/pkg/util/wait" corev1informers "k8s.io/client-go/informers/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" + "k8s.io/klog" coreapi "k8s.io/kubernetes/pkg/apis/core" securityv1 "github.com/openshift/api/security/v1" @@ -45,6 +49,8 @@ type NamespaceSCCAllocationController struct { rangeAllocationClient securityv1client.RangeAllocationsGetter queue workqueue.RateLimitingInterface + + encoder runtime.Encoder } func NewNamespaceSCCAllocationController( @@ -54,6 +60,13 @@ func NewNamespaceSCCAllocationController( requiredUIDRange *uid.Range, mcs MCSAllocationFunc, ) *NamespaceSCCAllocationController { + + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + codecs := serializer.NewCodecFactory(scheme) + jsonSerializer := runtimejson.NewSerializer(runtimejson.DefaultMetaFactory, scheme, scheme, false) + encoder := codecs.WithoutConversion().EncoderForVersion(jsonSerializer, corev1.SchemeGroupVersion) + c := &NamespaceSCCAllocationController{ requiredUIDRange: requiredUIDRange, mcsAllocator: mcs, @@ -62,6 +75,7 @@ func NewNamespaceSCCAllocationController( nsLister: namespaceInformer.Lister(), nsListerSynced: namespaceInformer.Informer().HasSynced, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName), + encoder: encoder, } namespaceInformer.Informer().AddEventHandlerWithResyncPeriod( @@ -177,8 +191,20 @@ func (c *NamespaceSCCAllocationController) allocate(ns *corev1.Namespace) error nsCopy.Annotations[securityv1.MCSAnnotation] = label.String() } } - - _, err = c.namespaceClient.Update(nsCopy) + nsCopyBytes, err := runtime.Encode(c.encoder, nsCopy) + if err != nil { + return err + } + nsBytes, err := runtime.Encode(c.encoder, ns) + if err != nil { + return err + } + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(nsBytes, nsCopyBytes, &corev1.Namespace{}) + if err != nil { + return err + } + // use patch here not to conflict with other actors + _, err = c.namespaceClient.Patch(ns.Name, types.StrategicMergePatchType, patchBytes) if apierrors.IsNotFound(err) { return nil } diff --git a/pkg/security/controller/namespace_security_allocation_controller_test.go b/pkg/security/controller/namespace_security_allocation_controller_test.go index d196c2895..a2b7fd077 100644 --- a/pkg/security/controller/namespace_security_allocation_controller_test.go +++ b/pkg/security/controller/namespace_security_allocation_controller_test.go @@ -1,6 +1,7 @@ package controller import ( + "encoding/json" "fmt" "math/big" "strings" @@ -8,15 +9,16 @@ import ( "github.com/davecgh/go-spew/spew" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/apitesting" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + runtimejson "k8s.io/apimachinery/pkg/runtime/serializer/json" kubefakeclient "k8s.io/client-go/kubernetes/fake" corev1listers "k8s.io/client-go/listers/core/v1" clientgotesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" - kapi "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/controller" securityv1 "github.com/openshift/api/security/v1" @@ -25,6 +27,10 @@ import ( "github.com/openshift/library-go/pkg/security/uid" ) +type patchData struct { + metav1.ObjectMeta `json:"metadata,omitempty"` +} + func TestController(t *testing.T) { kubeclient := kubefakeclient.NewSimpleClientset() securityclient := securityv1fakeclient.NewSimpleClientset() @@ -32,12 +38,18 @@ func TestController(t *testing.T) { uidr, _ := uid.NewRange(10, 20, 2) mcsr, _ := mcs.NewRange("s0:", 10, 2) + + scheme, codecs := apitesting.SchemeForOrDie(corev1.AddToScheme) + jsonSerializer := runtimejson.NewSerializer(runtimejson.DefaultMetaFactory, scheme, scheme, false) + encoder := codecs.WithoutConversion().EncoderForVersion(jsonSerializer, corev1.SchemeGroupVersion) + c := &NamespaceSCCAllocationController{ requiredUIDRange: uidr, mcsAllocator: DefaultMCSAllocation(uidr, mcsr, 5), namespaceClient: kubeclient.CoreV1().Namespaces(), nsLister: corev1listers.NewNamespaceLister(indexer), rangeAllocationClient: securityclient.SecurityV1(), + encoder: encoder, } err := c.Repair() if err != nil { @@ -55,7 +67,7 @@ func TestController(t *testing.T) { } securityclient.ClearActions() - err = c.allocate(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) + err = c.allocate(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) if err != nil { t.Fatal(err) } @@ -66,7 +78,11 @@ func TestController(t *testing.T) { } createNSAction := kubeActions[0] - got := createNSAction.(clientgotesting.CreateAction).GetObject().(*v1.Namespace) + data := createNSAction.(clientgotesting.PatchAction).GetPatch() + got := patchData{} + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unexpected error parsing patch data: %v", err) + } if got.Annotations[securityv1.UIDRangeAnnotation] != "10/2" { t.Errorf("unexpected uid annotation: %#v", got) } @@ -99,7 +115,7 @@ func TestControllerError(t *testing.T) { actions int }{ "not found": { - err: func() error { return errors.NewNotFound(kapi.Resource("Namespace"), "test") }, + err: func() error { return errors.NewNotFound(corev1.Resource("Namespace"), "test") }, errFn: func(err error) bool { return err == nil }, actions: 1, }, @@ -112,9 +128,9 @@ func TestControllerError(t *testing.T) { actions: 1, reactFn: func(a clientgotesting.Action) (bool, runtime.Object, error) { if a.Matches("get", "namespaces") { - return true, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}, nil + return true, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}, nil } - return true, (*v1.Namespace)(nil), errors.NewConflict(kapi.Resource("namespace"), "test", fmt.Errorf("test conflict")) + return true, (*corev1.Namespace)(nil), errors.NewConflict(corev1.Resource("namespace"), "test", fmt.Errorf("test conflict")) }, errFn: func(err error) bool { return err != nil && strings.Contains(err.Error(), "test conflict") @@ -126,7 +142,7 @@ func TestControllerError(t *testing.T) { t.Run(s, func(t *testing.T) { if testCase.reactFn == nil { testCase.reactFn = func(a clientgotesting.Action) (bool, runtime.Object, error) { - return true, (*v1.Namespace)(nil), testCase.err() + return true, (*corev1.Namespace)(nil), testCase.err() } } kubeclient := kubefakeclient.NewSimpleClientset() @@ -137,12 +153,18 @@ func TestControllerError(t *testing.T) { uidr, _ := uid.NewRange(10, 19, 2) mcsr, _ := mcs.NewRange("s0:", 10, 2) + + scheme, codecs := apitesting.SchemeForOrDie(corev1.AddToScheme) + jsonSerializer := runtimejson.NewSerializer(runtimejson.DefaultMetaFactory, scheme, scheme, false) + encoder := codecs.WithoutConversion().EncoderForVersion(jsonSerializer, corev1.SchemeGroupVersion) + c := &NamespaceSCCAllocationController{ requiredUIDRange: uidr, mcsAllocator: DefaultMCSAllocation(uidr, mcsr, 5), namespaceClient: kubeclient.CoreV1().Namespaces(), nsLister: corev1listers.NewNamespaceLister(indexer), rangeAllocationClient: securityclient.SecurityV1(), + encoder: encoder, } err := c.Repair() @@ -151,7 +173,7 @@ func TestControllerError(t *testing.T) { } securityclient.ClearActions() - err = c.allocate(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) + err = c.allocate(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) if !testCase.errFn(err) { t.Fatal(err) } diff --git a/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go b/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go new file mode 100644 index 000000000..542b0aa27 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/api/apitesting/codec.go @@ -0,0 +1,116 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apitesting + +import ( + "fmt" + "mime" + "os" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" +) + +var ( + testCodecMediaType string + testStorageCodecMediaType string +) + +// TestCodec returns the codec for the API version to test against, as set by the +// KUBE_TEST_API_TYPE env var. +func TestCodec(codecs runtimeserializer.CodecFactory, gvs ...schema.GroupVersion) runtime.Codec { + if len(testCodecMediaType) != 0 { + serializerInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), testCodecMediaType) + if !ok { + panic(fmt.Sprintf("no serializer for %s", testCodecMediaType)) + } + return codecs.CodecForVersions(serializerInfo.Serializer, codecs.UniversalDeserializer(), schema.GroupVersions(gvs), nil) + } + return codecs.LegacyCodec(gvs...) +} + +// TestStorageCodec returns the codec for the API version to test against used in storage, as set by the +// KUBE_TEST_API_STORAGE_TYPE env var. +func TestStorageCodec(codecs runtimeserializer.CodecFactory, gvs ...schema.GroupVersion) runtime.Codec { + if len(testStorageCodecMediaType) != 0 { + serializerInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), testStorageCodecMediaType) + if !ok { + panic(fmt.Sprintf("no serializer for %s", testStorageCodecMediaType)) + } + + // etcd2 only supports string data - we must wrap any result before returning + // TODO: remove for etcd3 / make parameterizable + serializer := serializerInfo.Serializer + if !serializerInfo.EncodesAsText { + serializer = runtime.NewBase64Serializer(serializer, serializer) + } + + decoder := recognizer.NewDecoder(serializer, codecs.UniversalDeserializer()) + return codecs.CodecForVersions(serializer, decoder, schema.GroupVersions(gvs), nil) + + } + return codecs.LegacyCodec(gvs...) +} + +func init() { + var err error + if apiMediaType := os.Getenv("KUBE_TEST_API_TYPE"); len(apiMediaType) > 0 { + testCodecMediaType, _, err = mime.ParseMediaType(apiMediaType) + if err != nil { + panic(err) + } + } + + if storageMediaType := os.Getenv("KUBE_TEST_API_STORAGE_TYPE"); len(storageMediaType) > 0 { + testStorageCodecMediaType, _, err = mime.ParseMediaType(storageMediaType) + if err != nil { + panic(err) + } + } +} + +// InstallOrDieFunc mirrors install functions that require success +type InstallOrDieFunc func(scheme *runtime.Scheme) + +// SchemeForInstallOrDie builds a simple test scheme and codecfactory pair for easy unit testing from higher level install methods +func SchemeForInstallOrDie(installFns ...InstallOrDieFunc) (*runtime.Scheme, runtimeserializer.CodecFactory) { + scheme := runtime.NewScheme() + codecFactory := runtimeserializer.NewCodecFactory(scheme) + for _, installFn := range installFns { + installFn(scheme) + } + + return scheme, codecFactory +} + +// InstallFunc mirrors install functions that can return an error +type InstallFunc func(scheme *runtime.Scheme) error + +// SchemeForOrDie builds a simple test scheme and codecfactory pair for easy unit testing from the bare registration methods. +func SchemeForOrDie(installFns ...InstallFunc) (*runtime.Scheme, runtimeserializer.CodecFactory) { + scheme := runtime.NewScheme() + codecFactory := runtimeserializer.NewCodecFactory(scheme) + for _, installFn := range installFns { + if err := installFn(scheme); err != nil { + panic(err) + } + } + + return scheme, codecFactory +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1a5be371d..ae894dceb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -464,6 +464,7 @@ k8s.io/api/storage/v1beta1 # k8s.io/apiextensions-apiserver v0.17.1 => k8s.io/apiextensions-apiserver v0.17.3 k8s.io/apiextensions-apiserver/pkg/features # k8s.io/apimachinery v0.17.3 => k8s.io/apimachinery v0.17.3 +k8s.io/apimachinery/pkg/api/apitesting k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/errors k8s.io/apimachinery/pkg/api/meta