mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 18:02:01 +00:00
3623 lines
92 KiB
Go
3623 lines
92 KiB
Go
/*
|
|
Copyright 2018 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 apiserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
restclient "k8s.io/client-go/rest"
|
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
|
"k8s.io/kubernetes/test/integration/framework"
|
|
)
|
|
|
|
func setup(t testing.TB) (clientset.Interface, kubeapiservertesting.TearDownFunc) {
|
|
// Disable ServiceAccount admission plugin as we don't have serviceaccount controller running.
|
|
server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd())
|
|
|
|
config := restclient.CopyConfig(server.ClientConfig)
|
|
// There are some tests (in scale_test.go) that rely on the response to be returned in JSON.
|
|
// So we overwrite it here.
|
|
config.ContentType = runtime.ContentTypeJSON
|
|
clientSet, err := clientset.NewForConfig(config)
|
|
if err != nil {
|
|
t.Fatalf("Error in create clientset: %v", err)
|
|
}
|
|
return clientSet, server.TearDownFn
|
|
}
|
|
|
|
// TestApplyAlsoCreates makes sure that PATCH requests with the apply content type
|
|
// will create the object if it doesn't already exist
|
|
// TODO: make a set of test cases in an easy-to-consume place (separate package?) so it's easy to test in both integration and e2e.
|
|
func TestApplyAlsoCreates(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
testCases := []struct {
|
|
resource string
|
|
name string
|
|
body string
|
|
}{
|
|
{
|
|
resource: "pods",
|
|
name: "test-pod",
|
|
body: `{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "test-pod"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container",
|
|
"image": "test-image"
|
|
}]
|
|
}
|
|
}`,
|
|
}, {
|
|
resource: "services",
|
|
name: "test-svc",
|
|
body: `{
|
|
"apiVersion": "v1",
|
|
"kind": "Service",
|
|
"metadata": {
|
|
"name": "test-svc"
|
|
},
|
|
"spec": {
|
|
"ports": [{
|
|
"port": 8080,
|
|
"protocol": "UDP"
|
|
}]
|
|
}
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource(tc.resource).
|
|
Name(tc.name).
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(tc.body)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Get().Namespace("default").Resource(tc.resource).Name(tc.name).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
// Test that we can re apply with a different field manager and don't get conflicts
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource(tc.resource).
|
|
Name(tc.name).
|
|
Param("fieldManager", "apply_test_2").
|
|
Body([]byte(tc.body)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to re-apply object using Apply patch: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNoOpUpdateSameResourceVersion makes sure that PUT requests which change nothing
|
|
// will not change the resource version (no write to etcd is done)
|
|
func TestNoOpUpdateSameResourceVersion(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
podName := "no-op"
|
|
podResource := "pods"
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "` + podName + `",
|
|
"labels": {
|
|
"a": "one",
|
|
"c": "two",
|
|
"b": "three"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
},{
|
|
"name": "test-container-c",
|
|
"image": "test-image-two"
|
|
},{
|
|
"name": "test-container-b",
|
|
"image": "test-image-three"
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource(podResource).
|
|
Name(podName).
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
// Sleep for one second to make sure that the times of each update operation is different.
|
|
time.Sleep(1 * time.Second)
|
|
|
|
createdObject, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource(podResource).Name(podName).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve created object: %v", err)
|
|
}
|
|
|
|
createdAccessor, err := meta.Accessor(createdObject)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for created object: %v", err)
|
|
}
|
|
|
|
createdBytes, err := json.MarshalIndent(createdObject, "\t", "\t")
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal created object: %v", err)
|
|
}
|
|
|
|
// Test that we can put the same object and don't change the RV
|
|
_, err = client.CoreV1().RESTClient().Put().
|
|
Namespace("default").
|
|
Resource(podResource).
|
|
Name(podName).
|
|
Body(createdBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply no-op update: %v", err)
|
|
}
|
|
|
|
updatedObject, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource(podResource).Name(podName).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve updated object: %v", err)
|
|
}
|
|
|
|
updatedAccessor, err := meta.Accessor(updatedObject)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for updated object: %v", err)
|
|
}
|
|
|
|
updatedBytes, err := json.MarshalIndent(updatedObject, "\t", "\t")
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal updated object: %v", err)
|
|
}
|
|
|
|
if createdAccessor.GetResourceVersion() != updatedAccessor.GetResourceVersion() {
|
|
t.Fatalf("Expected same resource version to be %v but got: %v\nold object:\n%v\nnew object:\n%v",
|
|
createdAccessor.GetResourceVersion(),
|
|
updatedAccessor.GetResourceVersion(),
|
|
string(createdBytes),
|
|
string(updatedBytes),
|
|
)
|
|
}
|
|
}
|
|
|
|
func getRV(obj runtime.Object) (string, error) {
|
|
acc, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return acc.GetResourceVersion(), nil
|
|
}
|
|
|
|
// TestNoSemanticUpdateAppleSameResourceVersion makes sure that APPLY requests which makes no semantic changes
|
|
// will not change the resource version (no write to etcd is done)
|
|
//
|
|
// Some of the non-semantic changes are:
|
|
// - Applying an atomic struct that removes a default
|
|
// - Changing Quantity or other fields that are normalized
|
|
func TestNoSemanticUpdateApplySameResourceVersion(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
ssBytes := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "StatefulSet",
|
|
"metadata": {
|
|
"name": "nginx",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"serviceName": "nginx",
|
|
"selector": { "matchLabels": {"app": "nginx"}},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx",
|
|
"resources": {
|
|
"limits": {"memory": "2048Mi"}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
"volumeClaimTemplates": [{
|
|
"metadata": {"name": "nginx"},
|
|
"spec": {
|
|
"accessModes": ["ReadWriteOnce"],
|
|
"resources": {"requests": {"storage": "1Gi"}}
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
obj, err := client.AppsV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("statefulsets").
|
|
Name("nginx").
|
|
Body(ssBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
rvCreated, err := getRV(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get RV: %v", err)
|
|
}
|
|
|
|
// Sleep for one second to make sure that the times of each update operation is different.
|
|
time.Sleep(1200 * time.Millisecond)
|
|
|
|
obj, err = client.AppsV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("statefulsets").
|
|
Name("nginx").
|
|
Body(ssBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
rvApplied, err := getRV(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get RV: %v", err)
|
|
}
|
|
if rvApplied != rvCreated {
|
|
t.Fatal("ResourceVersion changed after apply")
|
|
}
|
|
}
|
|
|
|
// TestNoSemanticUpdateAppleSameResourceVersion makes sure that PUT requests which makes no semantic changes
|
|
// will not change the resource version (no write to etcd is done)
|
|
//
|
|
// Some of the non-semantic changes are:
|
|
// - Applying an atomic struct that removes a default
|
|
// - Changing Quantity or other fields that are normalized
|
|
func TestNoSemanticUpdatePutSameResourceVersion(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
ssBytes := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "StatefulSet",
|
|
"metadata": {
|
|
"name": "nginx",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"serviceName": "nginx",
|
|
"selector": { "matchLabels": {"app": "nginx"}},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx",
|
|
"resources": {
|
|
"limits": {"memory": "2048Mi"}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
"volumeClaimTemplates": [{
|
|
"metadata": {"name": "nginx"},
|
|
"spec": {
|
|
"accessModes": ["ReadWriteOnce"],
|
|
"resources": { "requests": { "storage": "1Gi"}}
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
obj, err := client.AppsV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("statefulsets").
|
|
Body(ssBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
rvCreated, err := getRV(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get RV: %v", err)
|
|
}
|
|
|
|
// Sleep for one second to make sure that the times of each update operation is different.
|
|
time.Sleep(1200 * time.Millisecond)
|
|
|
|
obj, err = client.AppsV1().RESTClient().Put().
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("statefulsets").
|
|
Name("nginx").
|
|
Body(ssBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
rvApplied, err := getRV(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get RV: %v", err)
|
|
}
|
|
if rvApplied != rvCreated {
|
|
t.Fatal("ResourceVersion changed after similar PUT")
|
|
}
|
|
}
|
|
|
|
// TestCreateOnApplyFailsWithUID makes sure that PATCH requests with the apply content type
|
|
// will not create the object if it doesn't already exist and it specifies a UID
|
|
func TestCreateOnApplyFailsWithUID(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name("test-pod-uid").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "test-pod-uid",
|
|
"uid": "88e00824-7f0e-11e8-94a1-c8d3ffb15800"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container",
|
|
"image": "test-image"
|
|
}]
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if !apierrors.IsConflict(err) {
|
|
t.Fatalf("Expected conflict error but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyUpdateApplyConflictForced(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Body([]byte(`{"spec":{"replicas": 5}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(obj)).Do(context.TODO()).Get()
|
|
if err == nil {
|
|
t.Fatalf("Expecting to get conflicts when applying object")
|
|
}
|
|
status, ok := err.(*apierrors.StatusError)
|
|
if !ok {
|
|
t.Fatalf("Expecting to get conflicts as API error")
|
|
}
|
|
if len(status.Status().Details.Causes) < 1 {
|
|
t.Fatalf("Expecting to get at least one conflict when applying object, got: %v", status.Status().Details.Causes)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("force", "true").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(obj)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object with force: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestApplyGroupsManySeparateUpdates tests that when many different managers update the same object,
|
|
// the number of managedFields entries will only grow to a certain size.
|
|
func TestApplyGroupsManySeparateUpdates(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "admissionregistration.k8s.io/v1",
|
|
"kind": "ValidatingWebhookConfiguration",
|
|
"metadata": {
|
|
"name": "webhook",
|
|
"labels": {"applier":"true"},
|
|
},
|
|
}`)
|
|
|
|
object, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/admissionregistration.k8s.io/v1").
|
|
Resource("validatingwebhookconfigurations").
|
|
Name("webhook").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
for i := 0; i < 20; i++ {
|
|
unique := fmt.Sprintf("updater%v", i)
|
|
object, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
AbsPath("/apis/admissionregistration.k8s.io/v1").
|
|
Resource("validatingwebhookconfigurations").
|
|
Name("webhook").
|
|
Param("fieldManager", unique).
|
|
Body([]byte(`{"metadata":{"labels":{"` + unique + `":"new"}}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
// Expect 11 entries, because the cap for update entries is 10, and 1 apply entry
|
|
if actual, expected := len(accessor.GetManagedFields()), 11; actual != expected {
|
|
if b, err := json.MarshalIndent(object, "\t", "\t"); err == nil {
|
|
t.Fatalf("Object expected to contain %v entries in managedFields, but got %v:\n%v", expected, actual, string(b))
|
|
} else {
|
|
t.Fatalf("Object expected to contain %v entries in managedFields, but got %v: error marshalling object: %v", expected, actual, err)
|
|
}
|
|
}
|
|
|
|
// Expect the first entry to have the manager name "apply_test"
|
|
if actual, expected := accessor.GetManagedFields()[0].Manager, "apply_test"; actual != expected {
|
|
t.Fatalf("Expected first manager to be named %v but got %v", expected, actual)
|
|
}
|
|
|
|
// Expect the second entry to have the manager name "ancient-changes"
|
|
if actual, expected := accessor.GetManagedFields()[1].Manager, "ancient-changes"; actual != expected {
|
|
t.Fatalf("Expected first manager to be named %v but got %v", expected, actual)
|
|
}
|
|
}
|
|
|
|
// TestCreateVeryLargeObject tests that a very large object can be created without exceeding the size limit due to managedFields
|
|
func TestCreateVeryLargeObject(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
cfg := &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "large-create-test-cm",
|
|
Namespace: "default",
|
|
},
|
|
Data: map[string]string{},
|
|
}
|
|
|
|
for i := 0; i < 9999; i++ {
|
|
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
|
|
cfg.Data[unique] = "A"
|
|
}
|
|
|
|
// Should be able to create an object near the object size limit.
|
|
if _, err := client.CoreV1().ConfigMaps(cfg.Namespace).Create(context.TODO(), cfg, metav1.CreateOptions{}); err != nil {
|
|
t.Errorf("unable to create large test configMap: %v", err)
|
|
}
|
|
|
|
// Applying to the same object should cause managedFields to go over the object size limit, and fail.
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace(cfg.Namespace).
|
|
Resource("configmaps").
|
|
Name(cfg.Name).
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "large-create-test-cm",
|
|
"namespace": "default",
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err == nil {
|
|
t.Fatalf("expected to fail to update object using Apply patch, but succeeded")
|
|
}
|
|
}
|
|
|
|
// TestUpdateVeryLargeObject tests that a small object can be updated to be very large without exceeding the size limit due to managedFields
|
|
func TestUpdateVeryLargeObject(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
cfg := &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "large-update-test-cm",
|
|
Namespace: "default",
|
|
},
|
|
Data: map[string]string{"k": "v"},
|
|
}
|
|
|
|
// Create a small config map.
|
|
cfg, err := client.CoreV1().ConfigMaps(cfg.Namespace).Create(context.TODO(), cfg, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Errorf("unable to create configMap: %v", err)
|
|
}
|
|
|
|
// Should be able to update a small object to be near the object size limit.
|
|
var updateErr error
|
|
pollErr := wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) {
|
|
updateCfg, err := client.CoreV1().ConfigMaps(cfg.Namespace).Get(context.TODO(), cfg.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Apply the large update, then attempt to push it to the apiserver.
|
|
for i := 0; i < 9999; i++ {
|
|
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
|
|
updateCfg.Data[unique] = "A"
|
|
}
|
|
|
|
if _, err = client.CoreV1().ConfigMaps(cfg.Namespace).Update(context.TODO(), updateCfg, metav1.UpdateOptions{}); err == nil {
|
|
return true, nil
|
|
}
|
|
updateErr = err
|
|
return false, nil
|
|
})
|
|
if pollErr == wait.ErrWaitTimeout {
|
|
t.Errorf("unable to update configMap: %v", updateErr)
|
|
}
|
|
|
|
// Applying to the same object should cause managedFields to go over the object size limit, and fail.
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace(cfg.Namespace).
|
|
Resource("configmaps").
|
|
Name(cfg.Name).
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "large-update-test-cm",
|
|
"namespace": "default",
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err == nil {
|
|
t.Fatalf("expected to fail to update object using Apply patch, but succeeded")
|
|
}
|
|
}
|
|
|
|
// TestPatchVeryLargeObject tests that a small object can be patched to be very large without exceeding the size limit due to managedFields
|
|
func TestPatchVeryLargeObject(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
cfg := &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "large-patch-test-cm",
|
|
Namespace: "default",
|
|
},
|
|
Data: map[string]string{"k": "v"},
|
|
}
|
|
|
|
// Create a small config map.
|
|
if _, err := client.CoreV1().ConfigMaps(cfg.Namespace).Create(context.TODO(), cfg, metav1.CreateOptions{}); err != nil {
|
|
t.Errorf("unable to create configMap: %v", err)
|
|
}
|
|
|
|
patchString := `{"data":{"k":"v"`
|
|
for i := 0; i < 9999; i++ {
|
|
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
|
|
patchString = fmt.Sprintf("%s,%q:%q", patchString, unique, "A")
|
|
}
|
|
patchString = fmt.Sprintf("%s}}", patchString)
|
|
|
|
// Should be able to update a small object to be near the object size limit.
|
|
_, err := client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
AbsPath("/api/v1").
|
|
Namespace(cfg.Namespace).
|
|
Resource("configmaps").
|
|
Name(cfg.Name).
|
|
Body([]byte(patchString)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Errorf("unable to patch configMap: %v", err)
|
|
}
|
|
|
|
// Applying to the same object should cause managedFields to go over the object size limit, and fail.
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("large-patch-test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "large-patch-test-cm",
|
|
"namespace": "default",
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err == nil {
|
|
t.Fatalf("expected to fail to update object using Apply patch, but succeeded")
|
|
}
|
|
}
|
|
|
|
// TestApplyManagedFields makes sure that managedFields api does not change
|
|
func TestApplyManagedFields(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "updater").
|
|
Body([]byte(`{"data":{"new-key": "value"}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
// Sleep for one second to make sure that the times of each update operation is different.
|
|
// This will let us check that update entries with the same manager name are grouped together,
|
|
// and that the most recent update time is recorded in the grouped entry.
|
|
time.Sleep(1 * time.Second)
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "updater").
|
|
Body([]byte(`{"data":{"key": "new value"}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
actual, err := json.MarshalIndent(object, "\t", "\t")
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal object: %v", err)
|
|
}
|
|
|
|
expected := []byte(`{
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"uid": "` + string(accessor.GetUID()) + `",
|
|
"resourceVersion": "` + accessor.GetResourceVersion() + `",
|
|
"creationTimestamp": "` + accessor.GetCreationTimestamp().UTC().Format(time.RFC3339) + `",
|
|
"labels": {
|
|
"test-label": "test"
|
|
},
|
|
"managedFields": [
|
|
{
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"apiVersion": "v1",
|
|
"time": "` + accessor.GetManagedFields()[0].Time.UTC().Format(time.RFC3339) + `",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {
|
|
"f:metadata": {
|
|
"f:labels": {
|
|
"f:test-label": {}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"manager": "updater",
|
|
"operation": "Update",
|
|
"apiVersion": "v1",
|
|
"time": "` + accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) + `",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {
|
|
"f:data": {
|
|
"f:key": {},
|
|
"f:new-key": {}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"data": {
|
|
"key": "new value",
|
|
"new-key": "value"
|
|
}
|
|
}`)
|
|
|
|
if string(expected) != string(actual) {
|
|
t.Fatalf("Expected:\n%v\nGot:\n%v", string(expected), string(actual))
|
|
}
|
|
|
|
if accessor.GetManagedFields()[0].Time.UTC().Format(time.RFC3339) == accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) {
|
|
t.Fatalf("Expected times to be different but got:\n%v", string(actual))
|
|
}
|
|
}
|
|
|
|
// TestApplyRemovesEmptyManagedFields there are no empty managers in managedFields
|
|
func TestApplyRemovesEmptyManagedFields(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default"
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managed := accessor.GetManagedFields(); managed != nil {
|
|
t.Fatalf("Object contains unexpected managedFields: %v", managed)
|
|
}
|
|
}
|
|
|
|
func TestApplyRequiresFieldManager(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default"
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body(obj).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err == nil {
|
|
t.Fatalf("Apply should fail to create without fieldManager")
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Apply failed to create with fieldManager: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestApplyRemoveContainerPort removes a container port from a deployment
|
|
func TestApplyRemoveContainerPort(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
"ports": [{
|
|
"containerPort": 80,
|
|
"protocol": "TCP"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
obj = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to remove container port using Apply patch: %v", err)
|
|
}
|
|
|
|
deployment, err := client.AppsV1().Deployments("default").Get(context.TODO(), "deployment", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
if len(deployment.Spec.Template.Spec.Containers[0].Ports) > 0 {
|
|
t.Fatalf("Expected no container ports but got: %v, object: \n%#v", deployment.Spec.Template.Spec.Containers[0].Ports, deployment)
|
|
}
|
|
}
|
|
|
|
// TestApplyFailsWithVersionMismatch ensures that a version mismatch between the
|
|
// patch object and the live object will error
|
|
func TestApplyFailsWithVersionMismatch(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
obj = []byte(`{
|
|
"apiVersion": "extensions/v1beta",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 100,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(obj)).Do(context.TODO()).Get()
|
|
if err == nil {
|
|
t.Fatalf("Expecting to get version mismatch when applying object")
|
|
}
|
|
status, ok := err.(*apierrors.StatusError)
|
|
if !ok {
|
|
t.Fatalf("Expecting to get version mismatch as API error")
|
|
}
|
|
if status.Status().Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status code to be %d but was %d", http.StatusBadRequest, status.Status().Code)
|
|
}
|
|
}
|
|
|
|
// TestApplyConvertsManagedFieldsVersion checks that the apply
|
|
// converts the API group-version in the field manager
|
|
func TestApplyConvertsManagedFieldsVersion(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"},
|
|
"managedFields": [
|
|
{
|
|
"manager": "sidecar_controller",
|
|
"operation": "Apply",
|
|
"apiVersion": "extensions/v1beta1",
|
|
"fieldsV1": {
|
|
"f:metadata": {
|
|
"f:labels": {
|
|
"f:sidecar_version": {}
|
|
}
|
|
},
|
|
"f:spec": {
|
|
"f:template": {
|
|
"f: spec": {
|
|
"f:containers": {
|
|
"k:{\"name\":\"sidecar\"}": {
|
|
".": {},
|
|
"f:image": {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Body(obj).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
obj = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"sidecar_version": "release"}
|
|
},
|
|
"spec": {
|
|
"template": {
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "sidecar",
|
|
"image": "sidecar:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "sidecar_controller").
|
|
Body([]byte(obj)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
}
|
|
|
|
object, err := client.AppsV1().Deployments("default").Get(context.TODO(), "deployment", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
managed := accessor.GetManagedFields()
|
|
if len(managed) != 2 {
|
|
t.Fatalf("Expected 2 field managers, but got managed fields: %v", managed)
|
|
}
|
|
|
|
var actual *metav1.ManagedFieldsEntry
|
|
for i := range managed {
|
|
entry := &managed[i]
|
|
if entry.Manager == "sidecar_controller" && entry.APIVersion == "apps/v1" {
|
|
actual = entry
|
|
}
|
|
}
|
|
|
|
if actual == nil {
|
|
t.Fatalf("Expected managed fields to contain entry with manager '%v' with converted api version '%v', but got managed fields:\n%v", "sidecar_controller", "apps/v1", managed)
|
|
}
|
|
|
|
expected := &metav1.ManagedFieldsEntry{
|
|
Manager: "sidecar_controller",
|
|
Operation: metav1.ManagedFieldsOperationApply,
|
|
APIVersion: "apps/v1",
|
|
Time: actual.Time,
|
|
FieldsType: "FieldsV1",
|
|
FieldsV1: &metav1.FieldsV1{
|
|
Raw: []byte(`{"f:metadata":{"f:labels":{"f:sidecar_version":{}}},"f:spec":{"f:template":{"f:spec":{"f:containers":{"k:{\"name\":\"sidecar\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`),
|
|
},
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("expected:\n%v\nbut got:\n%v", expected, actual)
|
|
}
|
|
}
|
|
|
|
// TestClearManagedFieldsWithMergePatch verifies it's possible to clear the managedFields
|
|
func TestClearManagedFieldsWithMergePatch(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`{"metadata":{"managedFields": [{}]}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 {
|
|
t.Fatalf("Failed to clear managedFields, got: %v", managedFields)
|
|
}
|
|
}
|
|
|
|
// TestClearManagedFieldsWithStrategicMergePatch verifies it's possible to clear the managedFields
|
|
func TestClearManagedFieldsWithStrategicMergePatch(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`{"metadata":{"managedFields": [{}]}}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 {
|
|
t.Fatalf("Failed to clear managedFields, got: %v", managedFields)
|
|
}
|
|
|
|
if labels := accessor.GetLabels(); len(labels) < 1 {
|
|
t.Fatalf("Expected other fields to stay untouched, got: %v", object)
|
|
}
|
|
}
|
|
|
|
// TestClearManagedFieldsWithJSONPatch verifies it's possible to clear the managedFields
|
|
func TestClearManagedFieldsWithJSONPatch(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.JSONPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`[{"op": "replace", "path": "/metadata/managedFields", "value": [{}]}]`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 {
|
|
t.Fatalf("Failed to clear managedFields, got: %v", managedFields)
|
|
}
|
|
}
|
|
|
|
// TestClearManagedFieldsWithUpdate verifies it's possible to clear the managedFields
|
|
func TestClearManagedFieldsWithUpdate(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Put().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"managedFields": [{}],
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 {
|
|
t.Fatalf("Failed to clear managedFields, got: %v", managedFields)
|
|
}
|
|
|
|
if labels := accessor.GetLabels(); len(labels) < 1 {
|
|
t.Fatalf("Expected other fields to stay untouched, got: %v", object)
|
|
}
|
|
}
|
|
|
|
// TestErrorsDontFail
|
|
func TestErrorsDontFail(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Tries to create with a managed fields that has an empty `fieldsType`.
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"managedFields": [{
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"apiVersion": "v1",
|
|
"time": "2019-07-08T09:31:18Z",
|
|
"fieldsType": "",
|
|
"fieldsV1": {}
|
|
}],
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object with empty fieldsType: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestErrorsDontFailUpdate(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Put().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"managedFields": [{
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"apiVersion": "v1",
|
|
"time": "2019-07-08T09:31:18Z",
|
|
"fieldsType": "",
|
|
"fieldsV1": {}
|
|
}],
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update object with empty fieldsType: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestErrorsDontFailPatch(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.JSONPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`[{"op": "replace", "path": "/metadata/managedFields", "value": [{
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"apiVersion": "v1",
|
|
"time": "2019-07-08T09:31:18Z",
|
|
"fieldsType": "",
|
|
"fieldsV1": {}
|
|
}]}]`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object with empty FieldsType: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyDoesNotChangeManagedFieldsViaSubresources(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "just-a-pod"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
liveObj, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
updateBytes := []byte(`{
|
|
"metadata": {
|
|
"managedFields": [{
|
|
"manager":"testing",
|
|
"operation":"Update",
|
|
"apiVersion":"v1",
|
|
"fieldsType":"FieldsV1",
|
|
"fieldsV1":{
|
|
"f:spec":{
|
|
"f:containers":{
|
|
"k:{\"name\":\"testing\"}":{
|
|
".":{},
|
|
"f:image":{},
|
|
"f:name":{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
},
|
|
"status": {
|
|
"conditions": [{"type": "MyStatus", "status":"true"}]
|
|
}
|
|
}`)
|
|
|
|
updateActor := "update_managedfields_test"
|
|
newObj, err := client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", updateActor).
|
|
Name("just-a-pod").
|
|
Resource("pods").
|
|
SubResource("status").
|
|
Body(updateBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error updating subresource: %v ", err)
|
|
}
|
|
|
|
liveAccessor, err := meta.Accessor(liveObj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for live object: %v", err)
|
|
}
|
|
newAccessor, err := meta.Accessor(newObj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for new object: %v", err)
|
|
}
|
|
|
|
liveManagedFields := liveAccessor.GetManagedFields()
|
|
if len(liveManagedFields) != 1 {
|
|
t.Fatalf("Expected managedFields in the live object to have exactly one entry, got %d: %v", len(liveManagedFields), liveManagedFields)
|
|
}
|
|
|
|
newManagedFields := newAccessor.GetManagedFields()
|
|
if len(newManagedFields) != 2 {
|
|
t.Fatalf("Expected managedFields in the new object to have exactly two entries, got %d: %v", len(newManagedFields), newManagedFields)
|
|
}
|
|
|
|
if !reflect.DeepEqual(liveManagedFields[0], newManagedFields[0]) {
|
|
t.Fatalf("managedFields updated via subresource:\n\nlive managedFields: %v\nnew managedFields: %v\n\n", liveManagedFields, newManagedFields)
|
|
}
|
|
|
|
if newManagedFields[1].Manager != updateActor {
|
|
t.Fatalf(`Expected managerFields to have an entry with manager set to %q`, updateActor)
|
|
}
|
|
}
|
|
|
|
// TestClearManagedFieldsWithUpdateEmptyList verifies it's possible to clear the managedFields by sending an empty list.
|
|
func TestClearManagedFieldsWithUpdateEmptyList(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Param("fieldManager", "apply_test").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Put().
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "ConfigMap",
|
|
"metadata": {
|
|
"name": "test-cm",
|
|
"namespace": "default",
|
|
"managedFields": [],
|
|
"labels": {
|
|
"test-label": "test"
|
|
}
|
|
},
|
|
"data": {
|
|
"key": "value"
|
|
}
|
|
}`)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Resource("configmaps").
|
|
Name("test-cm").
|
|
Body([]byte(`{"metadata":{"labels": { "test-label": "v1" }}}`)).Do(context.TODO()).Get()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
|
|
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to retrieve object: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(object)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor: %v", err)
|
|
}
|
|
|
|
if managedFields := accessor.GetManagedFields(); len(managedFields) != 0 {
|
|
t.Fatalf("Failed to stop tracking managedFields, got: %v", managedFields)
|
|
}
|
|
|
|
if labels := accessor.GetLabels(); len(labels) < 1 {
|
|
t.Fatalf("Expected other fields to stay untouched, got: %v", object)
|
|
}
|
|
}
|
|
|
|
// TestApplyUnsetExclusivelyOwnedFields verifies that when owned fields are omitted from an applied
|
|
// configuration, and no other managers own the field, it is removed.
|
|
func TestApplyUnsetExclusivelyOwnedFields(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// spec.replicas is a optional, defaulted field
|
|
// spec.template.spec.hostname is an optional, non-defaulted field
|
|
apply := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-exclusive-unset",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"hostname": "test-hostname",
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-exclusive-unset").
|
|
Param("fieldManager", "apply_test").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// unset spec.replicas and spec.template.spec.hostname
|
|
apply = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-exclusive-unset",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-exclusive-unset").
|
|
Param("fieldManager", "apply_test").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
deployment, ok := patched.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
if *deployment.Spec.Replicas != 1 {
|
|
t.Errorf("Expected deployment.spec.replicas to be 1 (default value), but got %d", deployment.Spec.Replicas)
|
|
}
|
|
if len(deployment.Spec.Template.Spec.Hostname) != 0 {
|
|
t.Errorf("Expected deployment.spec.template.spec.hostname to be unset, but got %s", deployment.Spec.Template.Spec.Hostname)
|
|
}
|
|
}
|
|
|
|
// TestApplyUnsetSharedFields verifies that when owned fields are omitted from an applied
|
|
// configuration, but other managers also own the field, is it not removed.
|
|
func TestApplyUnsetSharedFields(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// spec.replicas is a optional, defaulted field
|
|
// spec.template.spec.hostname is an optional, non-defaulted field
|
|
apply := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-unset",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"hostname": "test-hostname",
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
for _, fieldManager := range []string{"shared_owner_1", "shared_owner_2"} {
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-unset").
|
|
Param("fieldManager", fieldManager).
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
}
|
|
|
|
// unset spec.replicas and spec.template.spec.hostname
|
|
apply = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-unset",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-unset").
|
|
Param("fieldManager", "shared_owner_1").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
deployment, ok := patched.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
if *deployment.Spec.Replicas != 3 {
|
|
t.Errorf("Expected deployment.spec.replicas to be 3, but got %d", deployment.Spec.Replicas)
|
|
}
|
|
if deployment.Spec.Template.Spec.Hostname != "test-hostname" {
|
|
t.Errorf("Expected deployment.spec.template.spec.hostname to be \"test-hostname\", but got %s", deployment.Spec.Template.Spec.Hostname)
|
|
}
|
|
}
|
|
|
|
// TestApplyCanTransferFieldOwnershipToController verifies that when an applier creates an
|
|
// object, a controller takes ownership of a field, and the applier
|
|
// then omits the field from its applied configuration, that the field value persists.
|
|
func TestApplyCanTransferFieldOwnershipToController(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Applier creates a deployment with replicas set to 3
|
|
apply := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
appliedObj, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// a controller takes over the replicas field
|
|
applied, ok := appliedObj.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
replicas := int32(4)
|
|
applied.Spec.Replicas = &replicas
|
|
_, err = client.AppsV1().Deployments("default").
|
|
Update(context.TODO(), applied, metav1.UpdateOptions{FieldManager: "test_updater"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// applier omits replicas
|
|
apply = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// ensure the container is deleted even though a controller updated a field of the container
|
|
deployment, ok := patched.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
if *deployment.Spec.Replicas != 4 {
|
|
t.Errorf("Expected deployment.spec.replicas to be 4, but got %d", deployment.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
// TestApplyCanRemoveMapItemsContributedToByControllers verifies that when an applier creates an
|
|
// object, a controller modifies the contents of the map item via update, and the applier
|
|
// then omits the item from its applied configuration, that the item is removed.
|
|
func TestApplyCanRemoveMapItemsContributedToByControllers(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Applier creates a deployment with a name=nginx container
|
|
apply := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
appliedObj, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// a controller sets container.workingDir of the name=nginx container via an update
|
|
applied, ok := appliedObj.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
applied.Spec.Template.Spec.Containers[0].WorkingDir = "/home/replacement"
|
|
_, err = client.AppsV1().Deployments("default").
|
|
Update(context.TODO(), applied, metav1.UpdateOptions{FieldManager: "test_updater"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// applier removes name=nginx the container
|
|
apply = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"hostname": "test-hostname",
|
|
"containers": [{
|
|
"name": "other-container",
|
|
"image": "nginx:latest",
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// ensure the container is deleted even though a controller updated a field of the container
|
|
deployment, ok := patched.(*appsv1.Deployment)
|
|
if !ok {
|
|
t.Fatalf("Failed to convert response object to Deployment")
|
|
}
|
|
if len(deployment.Spec.Template.Spec.Containers) != 1 {
|
|
t.Fatalf("Expected 1 container after apply, got %d", len(deployment.Spec.Template.Spec.Containers))
|
|
}
|
|
if deployment.Spec.Template.Spec.Containers[0].Name != "other-container" {
|
|
t.Fatalf("Expected container to be named \"other-container\" but got %s", deployment.Spec.Template.Spec.Containers[0].Name)
|
|
}
|
|
}
|
|
|
|
// TestDefaultMissingKeys makes sure that the missing keys default is used when merging.
|
|
func TestDefaultMissingKeys(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Applier creates a deployment with containerPort but no protocol
|
|
apply := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
"ports": [{
|
|
"name": "foo",
|
|
"containerPort": 80
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
|
}
|
|
|
|
// Applier updates the name, and uses the protocol, we should get a conflict.
|
|
apply = []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment-shared-map-item-removal",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest",
|
|
"ports": [{
|
|
"name": "bar",
|
|
"containerPort": 80,
|
|
"protocol": "TCP"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
patched, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment-shared-map-item-removal").
|
|
Param("fieldManager", "test_applier_conflict").
|
|
Body(apply).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err == nil {
|
|
t.Fatalf("Expecting to get conflicts when a different applier updates existing list item, got no error: %s", patched)
|
|
}
|
|
status, ok := err.(*apierrors.StatusError)
|
|
if !ok {
|
|
t.Fatalf("Expecting to get conflicts as API error")
|
|
}
|
|
if len(status.Status().Details.Causes) != 1 {
|
|
t.Fatalf("Expecting to get one conflict when a different applier updates existing list item, got: %v", status.Status().Details.Causes)
|
|
}
|
|
}
|
|
|
|
var podBytes = []byte(`
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
labels:
|
|
app: some-app
|
|
plugin1: some-value
|
|
plugin2: some-value
|
|
plugin3: some-value
|
|
plugin4: some-value
|
|
name: some-name
|
|
namespace: default
|
|
ownerReferences:
|
|
- apiVersion: apps/v1
|
|
blockOwnerDeletion: true
|
|
controller: true
|
|
kind: ReplicaSet
|
|
name: some-name
|
|
uid: 0a9d2b9e-779e-11e7-b422-42010a8001be
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- one
|
|
- two
|
|
- three
|
|
- four
|
|
- five
|
|
- six
|
|
- seven
|
|
- eight
|
|
- nine
|
|
env:
|
|
- name: VAR_3
|
|
valueFrom:
|
|
secretKeyRef:
|
|
key: some-other-key
|
|
name: some-oher-name
|
|
- name: VAR_2
|
|
valueFrom:
|
|
secretKeyRef:
|
|
key: other-key
|
|
name: other-name
|
|
- name: VAR_1
|
|
valueFrom:
|
|
secretKeyRef:
|
|
key: some-key
|
|
name: some-name
|
|
image: some-image-name
|
|
imagePullPolicy: IfNotPresent
|
|
name: some-name
|
|
resources:
|
|
requests:
|
|
cpu: "0"
|
|
terminationMessagePath: /dev/termination-log
|
|
terminationMessagePolicy: File
|
|
volumeMounts:
|
|
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
|
name: default-token-hu5jz
|
|
readOnly: true
|
|
dnsPolicy: ClusterFirst
|
|
nodeName: node-name
|
|
priority: 0
|
|
restartPolicy: Always
|
|
schedulerName: default-scheduler
|
|
securityContext: {}
|
|
serviceAccount: default
|
|
serviceAccountName: default
|
|
terminationGracePeriodSeconds: 30
|
|
tolerations:
|
|
- effect: NoExecute
|
|
key: node.kubernetes.io/not-ready
|
|
operator: Exists
|
|
tolerationSeconds: 300
|
|
- effect: NoExecute
|
|
key: node.kubernetes.io/unreachable
|
|
operator: Exists
|
|
tolerationSeconds: 300
|
|
volumes:
|
|
- name: default-token-hu5jz
|
|
secret:
|
|
defaultMode: 420
|
|
secretName: default-token-hu5jz
|
|
status:
|
|
conditions:
|
|
- lastProbeTime: null
|
|
lastTransitionTime: "2019-07-08T09:31:18Z"
|
|
status: "True"
|
|
type: Initialized
|
|
- lastProbeTime: null
|
|
lastTransitionTime: "2019-07-08T09:41:59Z"
|
|
status: "True"
|
|
type: Ready
|
|
- lastProbeTime: null
|
|
lastTransitionTime: null
|
|
status: "True"
|
|
type: ContainersReady
|
|
- lastProbeTime: null
|
|
lastTransitionTime: "2019-07-08T09:31:18Z"
|
|
status: "True"
|
|
type: PodScheduled
|
|
containerStatuses:
|
|
- containerID: docker://885e82a1ed0b7356541bb410a0126921ac42439607c09875cd8097dd5d7b5376
|
|
image: some-image-name
|
|
imageID: docker-pullable://some-image-id
|
|
lastState:
|
|
terminated:
|
|
containerID: docker://d57290f9e00fad626b20d2dd87a3cf69bbc22edae07985374f86a8b2b4e39565
|
|
exitCode: 255
|
|
finishedAt: "2019-07-08T09:39:09Z"
|
|
reason: Error
|
|
startedAt: "2019-07-08T09:38:54Z"
|
|
name: name
|
|
ready: true
|
|
restartCount: 6
|
|
state:
|
|
running:
|
|
startedAt: "2019-07-08T09:41:59Z"
|
|
hostIP: 10.0.0.1
|
|
phase: Running
|
|
podIP: 10.0.0.1
|
|
qosClass: BestEffort
|
|
startTime: "2019-07-08T09:31:18Z"
|
|
`)
|
|
|
|
func decodePod(podBytes []byte) v1.Pod {
|
|
pod := v1.Pod{}
|
|
err := yaml.Unmarshal(podBytes, &pod)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return pod
|
|
}
|
|
|
|
func encodePod(pod v1.Pod) []byte {
|
|
podBytes, err := yaml.Marshal(pod)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return podBytes
|
|
}
|
|
|
|
func getPodBytesWhenEnabled(b *testing.B, pod v1.Pod, format string) []byte {
|
|
client, closeFn := setup(b)
|
|
defer closeFn()
|
|
flag.Lookup("v").Value.Set("0")
|
|
|
|
pod.Name = "size-pod"
|
|
podB, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
Name(pod.Name).
|
|
Namespace("default").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("pods").
|
|
SetHeader("Accept", format).
|
|
Body(encodePod(pod)).DoRaw(context.TODO())
|
|
if err != nil {
|
|
b.Fatalf("Failed to create object: %#v", err)
|
|
}
|
|
return podB
|
|
}
|
|
|
|
func BenchmarkServerSideApply(b *testing.B) {
|
|
podBytesWhenEnabled := getPodBytesWhenEnabled(b, decodePod(podBytes), "application/yaml")
|
|
|
|
client, closeFn := setup(b)
|
|
defer closeFn()
|
|
flag.Lookup("v").Value.Set("0")
|
|
|
|
benchAll(b, client, decodePod(podBytesWhenEnabled))
|
|
}
|
|
|
|
func benchAll(b *testing.B, client clientset.Interface, pod v1.Pod) {
|
|
// Make sure pod is ready to post
|
|
pod.ObjectMeta.CreationTimestamp = metav1.Time{}
|
|
pod.ObjectMeta.ResourceVersion = ""
|
|
pod.ObjectMeta.UID = ""
|
|
|
|
// Create pod for repeated-updates
|
|
pod.Name = "repeated-pod"
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
SetHeader("Content-Type", "application/yaml").
|
|
Body(encodePod(pod)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
b.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
b.Run("List1", benchListPod(client, pod, 1))
|
|
b.Run("List20", benchListPod(client, pod, 20))
|
|
b.Run("List200", benchListPod(client, pod, 200))
|
|
b.Run("List2000", benchListPod(client, pod, 2000))
|
|
|
|
b.Run("RepeatedUpdates", benchRepeatedUpdate(client, "repeated-pod"))
|
|
b.Run("Post1", benchPostPod(client, pod, 1))
|
|
b.Run("Post10", benchPostPod(client, pod, 10))
|
|
b.Run("Post50", benchPostPod(client, pod, 50))
|
|
}
|
|
|
|
func benchPostPod(client clientset.Interface, pod v1.Pod, parallel int) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
c := make(chan error)
|
|
for j := 0; j < parallel; j++ {
|
|
j := j
|
|
i := i
|
|
go func(pod v1.Pod) {
|
|
pod.Name = fmt.Sprintf("post%d-%d-%d-%d", parallel, b.N, j, i)
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
SetHeader("Content-Type", "application/yaml").
|
|
Body(encodePod(pod)).Do(context.TODO()).Get()
|
|
c <- err
|
|
}(pod)
|
|
}
|
|
for j := 0; j < parallel; j++ {
|
|
err := <-c
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
close(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func createNamespace(client clientset.Interface, name string) error {
|
|
namespace := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
|
namespaceBytes, err := yaml.Marshal(namespace)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to marshal namespace: %v", err)
|
|
}
|
|
_, err = client.CoreV1().RESTClient().Get().
|
|
Resource("namespaces").
|
|
SetHeader("Content-Type", "application/yaml").
|
|
Body(namespaceBytes).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create namespace: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func benchListPod(client clientset.Interface, pod v1.Pod, num int) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
namespace := fmt.Sprintf("get-%d-%d", num, b.N)
|
|
if err := createNamespace(client, namespace); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
// Create pods
|
|
for i := 0; i < num; i++ {
|
|
pod.Name = fmt.Sprintf("get-%d-%d", b.N, i)
|
|
pod.Namespace = namespace
|
|
_, err := client.CoreV1().RESTClient().Post().
|
|
Namespace(namespace).
|
|
Resource("pods").
|
|
SetHeader("Content-Type", "application/yaml").
|
|
Body(encodePod(pod)).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
b.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := client.CoreV1().RESTClient().Get().
|
|
Namespace(namespace).
|
|
Resource("pods").
|
|
SetHeader("Accept", "application/vnd.kubernetes.protobuf").
|
|
Do(context.TODO()).Get()
|
|
if err != nil {
|
|
b.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func benchRepeatedUpdate(client clientset.Interface, podName string) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := client.CoreV1().RESTClient().Patch(types.JSONPatchType).
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name(podName).
|
|
Body([]byte(fmt.Sprintf(`[{"op": "replace", "path": "/spec/containers/0/image", "value": "image%d"}]`, i))).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
b.Fatalf("Failed to patch object: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpgradeClientSideToServerSideApply(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
obj := []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
annotations:
|
|
"kubectl.kubernetes.io/last-applied-configuration": |
|
|
{"kind":"Deployment","apiVersion":"apps/v1","metadata":{"name":"my-deployment","labels":{"app":"my-app"}},"spec":{"replicas": 3,"template":{"metadata":{"labels":{"app":"my-app"}},"spec":{"containers":[{"name":"my-c","image":"my-image"}]}}}}
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100000
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`)
|
|
|
|
deployment, err := yamlutil.ToJSON(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed marshal yaml: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Post().
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Body(deployment).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
obj = []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label
|
|
spec:
|
|
replicas: 3 # expect conflict
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`)
|
|
|
|
deployment, err = yamlutil.ToJSON(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed marshal yaml: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("my-deployment").
|
|
Param("fieldManager", "kubectl").
|
|
Body(deployment).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if !apierrors.IsConflict(err) {
|
|
t.Fatalf("Expected conflict error but got: %v", err)
|
|
}
|
|
|
|
obj = []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label
|
|
spec:
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-new
|
|
`)
|
|
|
|
deployment, err = yamlutil.ToJSON(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed marshal yaml: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("my-deployment").
|
|
Param("fieldManager", "kubectl").
|
|
Body(deployment).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
}
|
|
|
|
deploymentObj, err := client.AppsV1().Deployments("default").Get(context.TODO(), "my-deployment", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
if *deploymentObj.Spec.Replicas != 100000 {
|
|
t.Fatalf("expected to get obj with replicas %d, but got %d", 100000, *deploymentObj.Spec.Replicas)
|
|
}
|
|
if deploymentObj.Spec.Template.Spec.Containers[0].Image != "my-image-new" {
|
|
t.Fatalf("expected to get obj with image %s, but got %s", "my-image-new", deploymentObj.Spec.Template.Spec.Containers[0].Image)
|
|
}
|
|
}
|
|
|
|
func TestRenamingAppliedFieldManagers(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Creating an object
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "just-a-pod",
|
|
"labels": {
|
|
"a": "one"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
}]
|
|
}
|
|
}`)
|
|
_, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "multi_manager_one").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply: %v", err)
|
|
}
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "multi_manager_two").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"b":"two"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply: %v", err)
|
|
}
|
|
|
|
pod, err := client.CoreV1().Pods("default").Get(context.TODO(), "just-a-pod", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
expectedLabels := map[string]string{
|
|
"a": "one",
|
|
"b": "two",
|
|
}
|
|
if !reflect.DeepEqual(pod.Labels, expectedLabels) {
|
|
t.Fatalf("Expected labels to be %v, but got %v", expectedLabels, pod.Labels)
|
|
}
|
|
|
|
managedFields := pod.GetManagedFields()
|
|
for i := range managedFields {
|
|
managedFields[i].Manager = "multi_manager"
|
|
}
|
|
pod.SetManagedFields(managedFields)
|
|
|
|
obj, err := client.CoreV1().RESTClient().
|
|
Put().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(pod).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for object: %v", err)
|
|
}
|
|
managedFields = accessor.GetManagedFields()
|
|
if len(managedFields) != 1 {
|
|
t.Fatalf("Expected object to have 1 managed fields entry, got: %d", len(managedFields))
|
|
}
|
|
entry := managedFields[0]
|
|
if entry.Manager != "multi_manager" || entry.Operation != "Apply" || string(entry.FieldsV1.Raw) != `{"f:metadata":{"f:labels":{"f:b":{}}}}` {
|
|
t.Fatalf(`Unexpected entry, got: %v`, entry)
|
|
}
|
|
}
|
|
|
|
func TestRenamingUpdatedFieldManagers(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Creating an object
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "just-a-pod"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
}]
|
|
}
|
|
}`)
|
|
_, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "first").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "multi_manager_one").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"metadata":{"labels":{"a":"one"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "multi_manager_two").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"metadata":{"labels":{"b":"two"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
pod, err := client.CoreV1().Pods("default").Get(context.TODO(), "just-a-pod", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
expectedLabels := map[string]string{
|
|
"a": "one",
|
|
"b": "two",
|
|
}
|
|
if !reflect.DeepEqual(pod.Labels, expectedLabels) {
|
|
t.Fatalf("Expected labels to be %v, but got %v", expectedLabels, pod.Labels)
|
|
}
|
|
|
|
managedFields := pod.GetManagedFields()
|
|
for i := range managedFields {
|
|
managedFields[i].Manager = "multi_manager"
|
|
}
|
|
pod.SetManagedFields(managedFields)
|
|
|
|
obj, err := client.CoreV1().RESTClient().
|
|
Put().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(pod).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for object: %v", err)
|
|
}
|
|
managedFields = accessor.GetManagedFields()
|
|
if len(managedFields) != 2 {
|
|
t.Fatalf("Expected object to have 2 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
entry := managedFields[1]
|
|
if entry.Manager != "multi_manager" || entry.Operation != "Update" || string(entry.FieldsV1.Raw) != `{"f:metadata":{"f:labels":{"f:b":{}}}}` {
|
|
t.Fatalf(`Unexpected entry, got: %v`, entry)
|
|
}
|
|
}
|
|
|
|
func TestDroppingSubresourceField(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Creating an object
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "just-a-pod"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
}]
|
|
}
|
|
}`)
|
|
_, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "first").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "label_manager").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"a":"one"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply: %v", err)
|
|
}
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "label_manager").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
SubResource("status").
|
|
Body([]byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"b":"two"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply: %v", err)
|
|
}
|
|
|
|
pod, err := client.CoreV1().Pods("default").Get(context.TODO(), "just-a-pod", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
expectedLabels := map[string]string{
|
|
"a": "one",
|
|
"b": "two",
|
|
}
|
|
if !reflect.DeepEqual(pod.Labels, expectedLabels) {
|
|
t.Fatalf("Expected labels to be %v, but got %v", expectedLabels, pod.Labels)
|
|
}
|
|
|
|
managedFields := pod.GetManagedFields()
|
|
if len(managedFields) != 3 {
|
|
t.Fatalf("Expected object to have 3 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
if managedFields[1].Manager != "label_manager" || managedFields[1].Operation != "Apply" || managedFields[1].Subresource != "" {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[1])
|
|
}
|
|
if managedFields[2].Manager != "label_manager" || managedFields[2].Operation != "Apply" || managedFields[2].Subresource != "status" {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[2])
|
|
}
|
|
|
|
for i := range managedFields {
|
|
managedFields[i].Subresource = ""
|
|
}
|
|
pod.SetManagedFields(managedFields)
|
|
|
|
obj, err := client.CoreV1().RESTClient().
|
|
Put().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(pod).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for object: %v", err)
|
|
}
|
|
managedFields = accessor.GetManagedFields()
|
|
if len(managedFields) != 2 {
|
|
t.Fatalf("Expected object to have 2 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
entry := managedFields[1]
|
|
if entry.Manager != "label_manager" || entry.Operation != "Apply" || string(entry.FieldsV1.Raw) != `{"f:metadata":{"f:labels":{"f:b":{}}}}` {
|
|
t.Fatalf(`Unexpected entry, got: %v`, entry)
|
|
}
|
|
}
|
|
|
|
func TestDroppingSubresourceFromSpecField(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Creating an object
|
|
podBytes := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "Pod",
|
|
"metadata": {
|
|
"name": "just-a-pod"
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "test-container-a",
|
|
"image": "test-image-one"
|
|
}]
|
|
}
|
|
}`)
|
|
_, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "first").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(podBytes).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "manager").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"metadata":{"labels":{"a":"two"}}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
Namespace("default").
|
|
Param("fieldManager", "manager").
|
|
Resource("pods").
|
|
SubResource("status").
|
|
Name("just-a-pod").
|
|
Body([]byte(`{"status":{"phase":"Running"}}`)).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply: %v", err)
|
|
}
|
|
|
|
pod, err := client.CoreV1().Pods("default").Get(context.TODO(), "just-a-pod", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
expectedLabels := map[string]string{"a": "two"}
|
|
if !reflect.DeepEqual(pod.Labels, expectedLabels) {
|
|
t.Fatalf("Expected labels to be %v, but got %v", expectedLabels, pod.Labels)
|
|
}
|
|
if pod.Status.Phase != v1.PodRunning {
|
|
t.Fatalf("Expected phase to be %q, but got %q", v1.PodRunning, pod.Status.Phase)
|
|
}
|
|
|
|
managedFields := pod.GetManagedFields()
|
|
if len(managedFields) != 3 {
|
|
t.Fatalf("Expected object to have 3 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
if managedFields[1].Manager != "manager" || managedFields[1].Operation != "Update" || managedFields[1].Subresource != "" {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[1])
|
|
}
|
|
if managedFields[2].Manager != "manager" || managedFields[2].Operation != "Update" || managedFields[2].Subresource != "status" {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[2])
|
|
}
|
|
|
|
for i := range managedFields {
|
|
managedFields[i].Subresource = ""
|
|
}
|
|
pod.SetManagedFields(managedFields)
|
|
|
|
obj, err := client.CoreV1().RESTClient().
|
|
Put().
|
|
Namespace("default").
|
|
Resource("pods").
|
|
Name("just-a-pod").
|
|
Body(pod).
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update: %v", err)
|
|
}
|
|
|
|
accessor, err := meta.Accessor(obj)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get meta accessor for object: %v", err)
|
|
}
|
|
managedFields = accessor.GetManagedFields()
|
|
if len(managedFields) != 2 {
|
|
t.Fatalf("Expected object to have 2 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
entry := managedFields[1]
|
|
if entry.Manager != "manager" || entry.Operation != "Update" || string(entry.FieldsV1.Raw) != `{"f:status":{"f:phase":{}}}` {
|
|
t.Fatalf(`Unexpected entry, got: %v`, entry)
|
|
}
|
|
}
|
|
|
|
func TestSubresourceField(t *testing.T) {
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// Creating a deployment
|
|
deploymentBytes := []byte(`{
|
|
"apiVersion": "apps/v1",
|
|
"kind": "Deployment",
|
|
"metadata": {
|
|
"name": "deployment",
|
|
"labels": {"app": "nginx"}
|
|
},
|
|
"spec": {
|
|
"replicas": 3,
|
|
"selector": {
|
|
"matchLabels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"template": {
|
|
"metadata": {
|
|
"labels": {
|
|
"app": "nginx"
|
|
}
|
|
},
|
|
"spec": {
|
|
"containers": [{
|
|
"name": "nginx",
|
|
"image": "nginx:latest"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
_, err := client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
Name("deployment").
|
|
Param("fieldManager", "manager").
|
|
Body(deploymentBytes).Do(context.TODO()).Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
}
|
|
|
|
_, err = client.CoreV1().RESTClient().
|
|
Patch(types.MergePatchType).
|
|
AbsPath("/apis/apps/v1").
|
|
Namespace("default").
|
|
Resource("deployments").
|
|
SubResource("scale").
|
|
Name("deployment").
|
|
Body([]byte(`{"spec":{"replicas":32}}`)).
|
|
Param("fieldManager", "manager").
|
|
Do(context.TODO()).
|
|
Get()
|
|
if err != nil {
|
|
t.Fatalf("Failed to update status: %v", err)
|
|
}
|
|
|
|
deployment, err := client.AppsV1().Deployments("default").Get(context.TODO(), "deployment", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to get object: %v", err)
|
|
}
|
|
|
|
managedFields := deployment.GetManagedFields()
|
|
if len(managedFields) != 2 {
|
|
t.Fatalf("Expected object to have 2 managed fields entries, got: %d", len(managedFields))
|
|
}
|
|
if managedFields[0].Manager != "manager" || managedFields[0].Operation != "Apply" || managedFields[0].Subresource != "" {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[0])
|
|
}
|
|
if managedFields[1].Manager != "manager" ||
|
|
managedFields[1].Operation != "Update" ||
|
|
managedFields[1].Subresource != "scale" ||
|
|
string(managedFields[1].FieldsV1.Raw) != `{"f:spec":{"f:replicas":{}}}` {
|
|
t.Fatalf(`Unexpected entry, got: %v`, managedFields[1])
|
|
}
|
|
}
|
|
|
|
// K8s has a bug introduced in vX.XX.X which changed the treatment of
|
|
// ObjectReferences from granular to atomic. This means that only one manager
|
|
// may own all fields of the ObjectReference. This resulted in a regression
|
|
// for the common use case of user-specified GVK, and machine-populated UID fields.
|
|
//
|
|
// This is a test to show that clusters affected by this bug before it was fixed
|
|
// do not experience any friction when updating to a version of k8s which marks
|
|
// the fields' management again as granular.
|
|
func TestApplyFormerlyAtomicFields(t *testing.T) {
|
|
// Start server with our populated ObjectReference. Since it is atomic its
|
|
// ownership changed when XX popualted the UID after the user specified the
|
|
// GVKN.
|
|
|
|
// 1. Create PersistentVolume with its claimRef owned by
|
|
// kube-controller-manager as in v1.22 - 1.24
|
|
// 2. Attempt to re-apply the original PersistentVolume which does not
|
|
// include uid.
|
|
// 3. Check that:
|
|
// a.) The operaiton was successfu;
|
|
// b.) The uid is unchanged
|
|
|
|
client, closeFn := setup(t)
|
|
defer closeFn()
|
|
|
|
// old PersistentVolume from last version of k8s with its claimRef owned
|
|
// atomically
|
|
oldPersistentVolume := []byte(`
|
|
{
|
|
"apiVersion": "v1",
|
|
"kind": "PersistentVolume",
|
|
"metadata": {
|
|
"creationTimestamp": "2022-06-08T23:46:32Z",
|
|
"finalizers": [
|
|
"kubernetes.io/pv-protection"
|
|
],
|
|
"labels": {
|
|
"type": "local"
|
|
},
|
|
"name": "pv-storage",
|
|
"uid": "112b18f7-fde6-4e48-aa61-f5168bd576b8"
|
|
},
|
|
"spec": {
|
|
"accessModes": [
|
|
"ReadWriteOnce"
|
|
],
|
|
"capacity": {
|
|
"storage": "16Mi"
|
|
},
|
|
"claimRef": {
|
|
"apiVersion": "v1",
|
|
"kind": "PersistentVolumeClaim",
|
|
"name": "pvc-storage",
|
|
"namespace": "default",
|
|
"resourceVersion": "15499",
|
|
"uid": "2018e302-7b12-406c-9fa2-e52535d29e48"
|
|
},
|
|
"hostPath": {
|
|
"path": "“/tmp/mydata",
|
|
"type": ""
|
|
},
|
|
"persistentVolumeReclaimPolicy": "Retain",
|
|
"volumeMode": "Filesystem"
|
|
},
|
|
"status": {
|
|
"phase": "Bound"
|
|
}
|
|
}`)
|
|
|
|
managedFieldsUpdate := []byte(`{
|
|
"apiVersion": "v1",
|
|
"kind": "PersistentVolume",
|
|
"metadata": {
|
|
"name": "pv-storage",
|
|
"managedFields": [
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {
|
|
"f:metadata": {
|
|
"f:labels": {
|
|
"f:type": {}
|
|
}
|
|
},
|
|
"f:spec": {
|
|
"f:accessModes": {},
|
|
"f:capacity": {
|
|
"f:storage": {}
|
|
},
|
|
"f:hostPath": {
|
|
"f:path": {}
|
|
},
|
|
"f:storageClassName": {}
|
|
}
|
|
},
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"time": "2022-06-08T23:46:32Z"
|
|
},
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {
|
|
"f:status": {
|
|
"f:phase": {}
|
|
}
|
|
},
|
|
"manager": "kube-controller-manager",
|
|
"operation": "Update",
|
|
"subresource": "status",
|
|
"time": "2022-06-08T23:46:32Z"
|
|
},
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {
|
|
"f:spec": {
|
|
"f:claimRef": {}
|
|
}
|
|
},
|
|
"manager": "kube-controller-manager",
|
|
"operation": "Update",
|
|
"time": "2022-06-08T23:46:37Z"
|
|
}
|
|
]
|
|
}
|
|
}`)
|
|
|
|
// Re-applies name and namespace
|
|
originalPV := []byte(`{
|
|
"kind": "PersistentVolume",
|
|
"apiVersion": "v1",
|
|
"metadata": {
|
|
"labels": {
|
|
"type": "local"
|
|
},
|
|
"name": "pv-storage",
|
|
},
|
|
"spec": {
|
|
"storageClassName": "",
|
|
"capacity": {
|
|
"storage": "16Mi"
|
|
},
|
|
"accessModes": [
|
|
"ReadWriteOnce"
|
|
],
|
|
"hostPath": {
|
|
"path": "“/tmp/mydata"
|
|
},
|
|
"claimRef": {
|
|
"name": "pvc-storage",
|
|
"namespace": "default"
|
|
}
|
|
}
|
|
}`)
|
|
|
|
// Create PV
|
|
originalObj, err := client.CoreV1().RESTClient().
|
|
Post().
|
|
Param("fieldManager", "apply_test").
|
|
Resource("persistentvolumes").
|
|
Body(oldPersistentVolume).
|
|
Do(context.TODO()).
|
|
Get()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
} else if _, ok := originalObj.(*v1.PersistentVolume); !ok {
|
|
t.Fatalf("returned object is incorrect type: %t", originalObj)
|
|
}
|
|
|
|
// Directly set managed fields to object
|
|
newObj, err := client.CoreV1().RESTClient().
|
|
Patch(types.StrategicMergePatchType).
|
|
Name("pv-storage").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("persistentvolumes").
|
|
Body(managedFieldsUpdate).
|
|
Do(context.TODO()).
|
|
Get()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
} else if _, ok := newObj.(*v1.PersistentVolume); !ok {
|
|
t.Fatalf("returned object is incorrect type: %t", newObj)
|
|
}
|
|
|
|
// Is initialized, attempt to write to fields underneath
|
|
// claimRef ObjectReference.
|
|
newObj, err = client.CoreV1().RESTClient().
|
|
Patch(types.ApplyPatchType).
|
|
Name("pv-storage").
|
|
Param("fieldManager", "apply_test").
|
|
Resource("persistentvolumes").
|
|
Body(originalPV).
|
|
Do(context.TODO()).
|
|
Get()
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to apply object: %v", err)
|
|
} else if _, ok := newObj.(*v1.PersistentVolume); !ok {
|
|
t.Fatalf("returned object is incorrect type: %t", newObj)
|
|
}
|
|
|
|
// Test that bug is fixed by showing no error and that uid is not cleared.
|
|
if !reflect.DeepEqual(originalObj.(*v1.PersistentVolume).Spec.ClaimRef, newObj.(*v1.PersistentVolume).Spec.ClaimRef) {
|
|
t.Fatalf("claimRef changed unexpectedly")
|
|
}
|
|
|
|
// Expect that we know own name/namespace fields
|
|
// All other fields unowned
|
|
// Make sure apply_test now owns claimRef.UID and that kube-controller-manager owns
|
|
// claimRef (but its ownership is not respected due to new granular structType)
|
|
managedFields := newObj.(*v1.PersistentVolume).ManagedFields
|
|
var expectedManagedFields []metav1.ManagedFieldsEntry
|
|
expectedManagedFieldsString := []byte(`[
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {"f:metadata":{"f:labels":{"f:type":{}}},"f:spec":{"f:accessModes":{},"f:capacity":{"f:storage":{}},"f:claimRef":{"f:name":{},"f:namespace":{}},"f:hostPath":{"f:path":{}},"f:storageClassName":{}}},
|
|
"manager": "apply_test",
|
|
"operation": "Apply",
|
|
"time": "2022-06-08T23:46:32Z"
|
|
},
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {"f:status":{"f:phase":{}}},
|
|
"manager": "kube-controller-manager",
|
|
"operation": "Update",
|
|
"subresource": "status",
|
|
"time": "2022-06-08T23:46:32Z"
|
|
},
|
|
{
|
|
"apiVersion": "v1",
|
|
"fieldsType": "FieldsV1",
|
|
"fieldsV1": {"f:spec":{"f:claimRef":{}}},
|
|
"manager": "kube-controller-manager",
|
|
"operation": "Update",
|
|
"time": "2022-06-08T23:46:37Z"
|
|
}
|
|
]`)
|
|
|
|
err = json.Unmarshal(expectedManagedFieldsString, &expectedManagedFields)
|
|
if err != nil {
|
|
t.Fatalf("unexpectly failed to decode expected managed fields")
|
|
}
|
|
|
|
// Wipe timestamps before comparison
|
|
for i := range expectedManagedFields {
|
|
expectedManagedFields[i].Time = nil
|
|
}
|
|
|
|
for i := range managedFields {
|
|
managedFields[i].Time = nil
|
|
}
|
|
|
|
if !reflect.DeepEqual(expectedManagedFields, managedFields) {
|
|
t.Fatalf("unexpected managed fields: %v", cmp.Diff(expectedManagedFields, managedFields))
|
|
}
|
|
}
|