k8s.io/kubernetes@v1.29.3/test/integration/apiserver/apply/scale_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apiserver
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"path"
    24  	"strings"
    25  	"testing"
    26  
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/managedfields"
    33  	clientset "k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/kubernetes/scheme"
    35  	deploymentstorage "k8s.io/kubernetes/pkg/registry/apps/deployment/storage"
    36  	replicasetstorage "k8s.io/kubernetes/pkg/registry/apps/replicaset/storage"
    37  	statefulsetstorage "k8s.io/kubernetes/pkg/registry/apps/statefulset/storage"
    38  	replicationcontrollerstorage "k8s.io/kubernetes/pkg/registry/core/replicationcontroller/storage"
    39  )
    40  
    41  type scaleTest struct {
    42  	kind     string
    43  	resource string
    44  	path     string
    45  	validObj string
    46  }
    47  
    48  func TestScaleAllResources(t *testing.T) {
    49  	client, closeFn := setup(t)
    50  	defer closeFn()
    51  
    52  	tests := []scaleTest{
    53  		{
    54  			kind:     "Deployment",
    55  			resource: "deployments",
    56  			path:     "/apis/apps/v1",
    57  			validObj: validAppsV1("Deployment"),
    58  		},
    59  		{
    60  			kind:     "StatefulSet",
    61  			resource: "statefulsets",
    62  			path:     "/apis/apps/v1",
    63  			validObj: validAppsV1("StatefulSet"),
    64  		},
    65  		{
    66  			kind:     "ReplicaSet",
    67  			resource: "replicasets",
    68  			path:     "/apis/apps/v1",
    69  			validObj: validAppsV1("ReplicaSet"),
    70  		},
    71  		{
    72  			kind:     "ReplicationController",
    73  			resource: "replicationcontrollers",
    74  			path:     "/api/v1",
    75  			validObj: validV1ReplicationController(),
    76  		},
    77  	}
    78  
    79  	for _, test := range tests {
    80  		t.Run(test.kind, func(t *testing.T) {
    81  			validObject := []byte(test.validObj)
    82  
    83  			// Create the object
    84  			_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
    85  				AbsPath(test.path).
    86  				Namespace("default").
    87  				Resource(test.resource).
    88  				Name("test").
    89  				Param("fieldManager", "apply_test").
    90  				Body(validObject).
    91  				Do(context.TODO()).Get()
    92  			if err != nil {
    93  				t.Fatalf("Failed to create object using apply: %v", err)
    94  			}
    95  			obj := retrieveObject(t, client, test.path, test.resource)
    96  			assertReplicasValue(t, obj, 1)
    97  			assertReplicasOwnership(t, obj, "apply_test")
    98  
    99  			// Call scale subresource to update replicas
   100  			_, err = client.CoreV1().RESTClient().
   101  				Patch(types.MergePatchType).
   102  				AbsPath(test.path).
   103  				Namespace("default").
   104  				Resource(test.resource).
   105  				Name("test").
   106  				SubResource("scale").
   107  				Param("fieldManager", "scale_test").
   108  				Body([]byte(`{"spec":{"replicas": 5}}`)).
   109  				Do(context.TODO()).Get()
   110  			if err != nil {
   111  				t.Fatalf("Failed to scale object: %v", err)
   112  			}
   113  			obj = retrieveObject(t, client, test.path, test.resource)
   114  			assertReplicasValue(t, obj, 5)
   115  			assertReplicasOwnership(t, obj, "scale_test")
   116  
   117  			// Re-apply the original object, it should fail with conflict because replicas have changed
   118  			_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   119  				AbsPath(test.path).
   120  				Namespace("default").
   121  				Resource(test.resource).
   122  				Name("test").
   123  				Param("fieldManager", "apply_test").
   124  				Body(validObject).
   125  				Do(context.TODO()).Get()
   126  			if !apierrors.IsConflict(err) {
   127  				t.Fatalf("Expected conflict when re-applying the original object, but got: %v", err)
   128  			}
   129  
   130  			// Re-apply forcing the changes should succeed
   131  			_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   132  				AbsPath(test.path).
   133  				Namespace("default").
   134  				Resource(test.resource).
   135  				Name("test").
   136  				Param("fieldManager", "apply_test").
   137  				Param("force", "true").
   138  				Body(validObject).
   139  				Do(context.TODO()).Get()
   140  			if err != nil {
   141  				t.Fatalf("Error force-updating: %v", err)
   142  			}
   143  			obj = retrieveObject(t, client, test.path, test.resource)
   144  			assertReplicasValue(t, obj, 1)
   145  			assertReplicasOwnership(t, obj, "apply_test")
   146  
   147  			// Run "Apply" with a scale object with a different number of replicas. It should generate a conflict.
   148  			_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   149  				AbsPath(test.path).
   150  				Namespace("default").
   151  				Resource(test.resource).
   152  				SubResource("scale").
   153  				Name("test").
   154  				Param("fieldManager", "apply_scale").
   155  				Body([]byte(`{"kind":"Scale","apiVersion":"autoscaling/v1","metadata":{"name":"test","namespace":"default"},"spec":{"replicas":17}}`)).
   156  				Do(context.TODO()).Get()
   157  			if !apierrors.IsConflict(err) {
   158  				t.Fatalf("Expected conflict error but got: %v", err)
   159  			}
   160  			if !strings.Contains(err.Error(), "apply_test") {
   161  				t.Fatalf("Expected conflict with `apply_test` manager when but got: %v", err)
   162  			}
   163  
   164  			// Same as before but force. Only the new manager should own .spec.replicas
   165  			_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   166  				AbsPath(test.path).
   167  				Namespace("default").
   168  				Resource(test.resource).
   169  				SubResource("scale").
   170  				Name("test").
   171  				Param("fieldManager", "apply_scale").
   172  				Param("force", "true").
   173  				Body([]byte(`{"kind":"Scale","apiVersion":"autoscaling/v1","metadata":{"name":"test","namespace":"default"},"spec":{"replicas":17}}`)).
   174  				Do(context.TODO()).Get()
   175  			if err != nil {
   176  				t.Fatalf("Error updating object by applying scale and forcing: %v ", err)
   177  			}
   178  			obj = retrieveObject(t, client, test.path, test.resource)
   179  			assertReplicasValue(t, obj, 17)
   180  			assertReplicasOwnership(t, obj, "apply_scale")
   181  
   182  			// Replace scale object
   183  			_, err = client.CoreV1().RESTClient().Put().
   184  				AbsPath(test.path).
   185  				Namespace("default").
   186  				Resource(test.resource).
   187  				SubResource("scale").
   188  				Name("test").
   189  				Param("fieldManager", "replace_test").
   190  				Body([]byte(`{"kind":"Scale","apiVersion":"autoscaling/v1","metadata":{"name":"test","namespace":"default"},"spec":{"replicas":7}}`)).
   191  				Do(context.TODO()).Get()
   192  			if err != nil {
   193  				t.Fatalf("Error replacing object: %v", err)
   194  			}
   195  			obj = retrieveObject(t, client, test.path, test.resource)
   196  			assertReplicasValue(t, obj, 7)
   197  			assertReplicasOwnership(t, obj, "replace_test")
   198  
   199  			// Apply the same number of replicas, both managers should own the field
   200  			_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   201  				AbsPath(test.path).
   202  				Namespace("default").
   203  				Resource(test.resource).
   204  				SubResource("scale").
   205  				Name("test").
   206  				Param("fieldManager", "co_owning_test").
   207  				Body([]byte(`{"kind":"Scale","apiVersion":"autoscaling/v1","metadata":{"name":"test","namespace":"default"},"spec":{"replicas":7}}`)).
   208  				Do(context.TODO()).Get()
   209  			if err != nil {
   210  				t.Fatalf("Error updating object: %v", err)
   211  			}
   212  			obj = retrieveObject(t, client, test.path, test.resource)
   213  			assertReplicasValue(t, obj, 7)
   214  			assertReplicasOwnership(t, obj, "replace_test", "co_owning_test")
   215  
   216  			// Scaling again should make this manager the only owner of replicas
   217  			_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
   218  				AbsPath(test.path).
   219  				Namespace("default").
   220  				Resource(test.resource).
   221  				SubResource("scale").
   222  				Name("test").
   223  				Param("fieldManager", "scale_test").
   224  				Body([]byte(`{"spec":{"replicas": 5}}`)).
   225  				Do(context.TODO()).Get()
   226  			if err != nil {
   227  				t.Fatalf("Error scaling object: %v", err)
   228  			}
   229  			obj = retrieveObject(t, client, test.path, test.resource)
   230  			assertReplicasValue(t, obj, 5)
   231  			assertReplicasOwnership(t, obj, "scale_test")
   232  		})
   233  	}
   234  }
   235  
   236  func TestScaleUpdateOnlyStatus(t *testing.T) {
   237  	client, closeFn := setup(t)
   238  	defer closeFn()
   239  
   240  	resource := "deployments"
   241  	path := "/apis/apps/v1"
   242  	validObject := []byte(validAppsV1("Deployment"))
   243  
   244  	// Create the object
   245  	_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
   246  		AbsPath(path).
   247  		Namespace("default").
   248  		Resource(resource).
   249  		Name("test").
   250  		Param("fieldManager", "apply_test").
   251  		Body(validObject).
   252  		Do(context.TODO()).Get()
   253  	if err != nil {
   254  		t.Fatalf("Failed to create object using apply: %v", err)
   255  	}
   256  	obj := retrieveObject(t, client, path, resource)
   257  	assertReplicasValue(t, obj, 1)
   258  	assertReplicasOwnership(t, obj, "apply_test")
   259  
   260  	// Call scale subresource to update replicas
   261  	_, err = client.CoreV1().RESTClient().
   262  		Patch(types.MergePatchType).
   263  		AbsPath(path).
   264  		Namespace("default").
   265  		Resource(resource).
   266  		Name("test").
   267  		SubResource("scale").
   268  		Param("fieldManager", "scale_test").
   269  		Body([]byte(`{"status":{"replicas": 42}}`)).
   270  		Do(context.TODO()).Get()
   271  	if err != nil {
   272  		t.Fatalf("Failed to scale object: %v", err)
   273  	}
   274  	obj = retrieveObject(t, client, path, resource)
   275  	assertReplicasValue(t, obj, 1)
   276  	assertReplicasOwnership(t, obj, "apply_test")
   277  }
   278  
   279  func TestAllKnownVersionsAreInMappings(t *testing.T) {
   280  	cases := []struct {
   281  		groupKind schema.GroupKind
   282  		mappings  managedfields.ResourcePathMappings
   283  	}{
   284  		{
   285  			groupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
   286  			mappings:  replicasetstorage.ReplicasPathMappings(),
   287  		},
   288  		{
   289  			groupKind: schema.GroupKind{Group: "apps", Kind: "StatefulSet"},
   290  			mappings:  statefulsetstorage.ReplicasPathMappings(),
   291  		},
   292  		{
   293  			groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
   294  			mappings:  deploymentstorage.ReplicasPathMappings(),
   295  		},
   296  		{
   297  			groupKind: schema.GroupKind{Group: "", Kind: "ReplicationController"},
   298  			mappings:  replicationcontrollerstorage.ReplicasPathMappings(),
   299  		},
   300  	}
   301  	for _, c := range cases {
   302  		knownVersions := scheme.Scheme.VersionsForGroupKind(c.groupKind)
   303  		for _, version := range knownVersions {
   304  			if _, ok := c.mappings[version.String()]; !ok {
   305  				t.Errorf("missing version %v for %v mappings", version, c.groupKind)
   306  			}
   307  		}
   308  
   309  		if len(knownVersions) != len(c.mappings) {
   310  			t.Errorf("%v mappings has extra items: %v vs %v", c.groupKind, c.mappings, knownVersions)
   311  		}
   312  	}
   313  }
   314  
   315  func validAppsV1(kind string) string {
   316  	return fmt.Sprintf(`{
   317  	    "apiVersion": "apps/v1",
   318  	    "kind": "%s",
   319  	    "metadata": {
   320  	      "name": "test"
   321  	    },
   322  	    "spec": {
   323  	      "replicas": 1,
   324  	      "selector": {
   325  	        "matchLabels": {
   326  	           "app": "nginx"
   327  	        }
   328  	      },
   329  	      "template": {
   330  	        "metadata": {
   331  	          "labels": {
   332  	            "app": "nginx"
   333  	          }
   334  	        },
   335  	        "spec": {
   336  	          "containers": [{
   337  	            "name":  "nginx",
   338  	            "image": "nginx:latest"
   339  	          }]
   340  	        }
   341  	      }
   342  	    }
   343  	  }`, kind)
   344  }
   345  
   346  func validV1ReplicationController() string {
   347  	return `{
   348  	    "apiVersion": "v1",
   349  	    "kind": "ReplicationController",
   350  	    "metadata": {
   351  	      "name": "test"
   352  	    },
   353  	    "spec": {
   354  	      "replicas": 1,
   355  	      "selector": {
   356            "app": "nginx"
   357  	      },
   358  	      "template": {
   359  	        "metadata": {
   360  	          "labels": {
   361  	            "app": "nginx"
   362  	          }
   363  	        },
   364  	        "spec": {
   365  	          "containers": [{
   366  	            "name":  "nginx",
   367  	            "image": "nginx:latest"
   368  	          }]
   369  	        }
   370  	      }
   371  	    }
   372  	  }`
   373  }
   374  
   375  func retrieveObject(t *testing.T, client clientset.Interface, prefix, resource string) *unstructured.Unstructured {
   376  	t.Helper()
   377  
   378  	urlPath := path.Join(prefix, "namespaces", "default", resource, "test")
   379  	bytes, err := client.CoreV1().RESTClient().Get().AbsPath(urlPath).DoRaw(context.TODO())
   380  	if err != nil {
   381  		t.Fatalf("Failed to retrieve object: %v", err)
   382  	}
   383  	obj := &unstructured.Unstructured{}
   384  	if err := json.Unmarshal(bytes, obj); err != nil {
   385  		t.Fatalf("Error unmarshalling the retrieved object: %v", err)
   386  	}
   387  	return obj
   388  }
   389  
   390  func assertReplicasValue(t *testing.T, obj *unstructured.Unstructured, value int) {
   391  	actualValue, found, err := unstructured.NestedInt64(obj.Object, "spec", "replicas")
   392  
   393  	if err != nil {
   394  		t.Fatalf("Error when retrieving replicas field: %v", err)
   395  	}
   396  	if !found {
   397  		t.Fatalf("Replicas field not found")
   398  	}
   399  
   400  	if int(actualValue) != value {
   401  		t.Fatalf("Expected replicas field value to be %d but got %d", value, actualValue)
   402  	}
   403  }
   404  
   405  func assertReplicasOwnership(t *testing.T, obj *unstructured.Unstructured, fieldManagers ...string) {
   406  	t.Helper()
   407  
   408  	accessor, err := meta.Accessor(obj)
   409  	if err != nil {
   410  		t.Fatalf("Failed to get meta accessor for object: %v", err)
   411  	}
   412  
   413  	seen := make(map[string]bool)
   414  	for _, m := range fieldManagers {
   415  		seen[m] = false
   416  	}
   417  
   418  	for _, managedField := range accessor.GetManagedFields() {
   419  		var entryJSON map[string]interface{}
   420  		if err := json.Unmarshal(managedField.FieldsV1.Raw, &entryJSON); err != nil {
   421  			t.Fatalf("failed to read into json")
   422  		}
   423  
   424  		spec, ok := entryJSON["f:spec"].(map[string]interface{})
   425  		if !ok {
   426  			// continue with the next managedField, as we this field does not hold the spec entry
   427  			continue
   428  		}
   429  
   430  		if _, ok := spec["f:replicas"]; !ok {
   431  			// continue with the next managedField, as we this field does not hold the spec.replicas entry
   432  			continue
   433  		}
   434  
   435  		// check if the manager is one of the ones we expect
   436  		if _, ok := seen[managedField.Manager]; !ok {
   437  			t.Fatalf("Unexpected field manager, found %q, expected to be in: %v", managedField.Manager, seen)
   438  		}
   439  
   440  		seen[managedField.Manager] = true
   441  	}
   442  
   443  	var missingManagers []string
   444  	for manager, managerSeen := range seen {
   445  		if !managerSeen {
   446  			missingManagers = append(missingManagers, manager)
   447  		}
   448  	}
   449  	if len(missingManagers) > 0 {
   450  		t.Fatalf("replicas fields should be owned by %v", missingManagers)
   451  	}
   452  }