mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-17 15:13:08 +00:00
Add ResourceVersionMatch parameter to make Resource Version semantics consistent for list
This commit is contained in:
@@ -61,6 +61,7 @@ go_test(
|
||||
"//staging/src/k8s.io/client-go/discovery/cached/disk:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/apps/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/metadata:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
@@ -68,6 +69,7 @@ go_test(
|
||||
"//staging/src/k8s.io/client-go/tools/pager:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
|
||||
"//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library",
|
||||
"//test/integration:go_default_library",
|
||||
"//test/integration/framework:go_default_library",
|
||||
"//vendor/github.com/google/uuid:go_default_library",
|
||||
"//vendor/k8s.io/gengo/examples/set-gen/sets:go_default_library",
|
||||
|
@@ -54,12 +54,15 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
|
||||
"k8s.io/client-go/metadata"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/pager"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/kubernetes/pkg/master"
|
||||
"k8s.io/kubernetes/test/integration"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
@@ -86,7 +89,7 @@ func setupWithResourcesWithOptions(t *testing.T, opts *framework.MasterConfigOpt
|
||||
masterConfig.GenericConfig.OpenAPIConfig = framework.DefaultOpenAPIConfig()
|
||||
_, s, closeFn := framework.RunAMaster(masterConfig)
|
||||
|
||||
clientSet, err := clientset.NewForConfig(&restclient.Config{Host: s.URL})
|
||||
clientSet, err := clientset.NewForConfig(&restclient.Config{Host: s.URL, QPS: -1})
|
||||
if err != nil {
|
||||
t.Fatalf("Error in create clientset: %v", err)
|
||||
}
|
||||
@@ -309,6 +312,207 @@ func Test202StatusCode(t *testing.T) {
|
||||
verifyStatusCode(t, "DELETE", s.URL+path.Join("/apis/apps/v1/namespaces", ns.Name, "replicasets", rs.Name), cascDel, 202)
|
||||
}
|
||||
|
||||
var (
|
||||
invalidContinueToken = "invalidContinueToken"
|
||||
invalidResourceVersion = "invalid"
|
||||
invalidResourceVersionMatch = metav1.ResourceVersionMatch("InvalidMatch")
|
||||
)
|
||||
|
||||
// TestListOptions ensures that list works as expected for valid and invalid combinations of limit, continue,
|
||||
// resourceVersion and resourceVersionMatch.
|
||||
func TestListOptions(t *testing.T) {
|
||||
for _, watchCacheEnabled := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("watchCacheEnabled=%t", watchCacheEnabled), func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIListChunking, true)()
|
||||
etcdOptions := framework.DefaultEtcdOptions()
|
||||
etcdOptions.EnableWatchCache = watchCacheEnabled
|
||||
s, clientSet, closeFn := setupWithOptions(t, &framework.MasterConfigOptions{EtcdOptions: etcdOptions})
|
||||
defer closeFn()
|
||||
|
||||
ns := framework.CreateTestingNamespace("list-options", s, t)
|
||||
defer framework.DeleteTestingNamespace(ns, s, t)
|
||||
|
||||
rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
|
||||
|
||||
var compactedRv, oldestUncompactedRv string
|
||||
for i := 0; i < 15; i++ {
|
||||
rs := newRS(ns.Name)
|
||||
rs.Name = fmt.Sprintf("test-%d", i)
|
||||
created, err := rsClient.Create(context.Background(), rs, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if i == 0 {
|
||||
compactedRv = created.ResourceVersion // We compact this first resource version below
|
||||
}
|
||||
// delete the first 5, and then compact them
|
||||
if i < 5 {
|
||||
var zero int64
|
||||
if err := rsClient.Delete(context.Background(), rs.Name, metav1.DeleteOptions{GracePeriodSeconds: &zero}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldestUncompactedRv = created.ResourceVersion
|
||||
}
|
||||
}
|
||||
|
||||
// compact some of the revision history in etcd so we can test "too old" resource versions
|
||||
_, kvClient, err := integration.GetEtcdClients(etcdOptions.StorageConfig.Transport)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
revision, err := strconv.Atoi(oldestUncompactedRv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = kvClient.Compact(context.Background(), int64(revision))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listObj, err := rsClient.List(context.Background(), metav1.ListOptions{
|
||||
Limit: 6,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
validContinueToken := listObj.Continue
|
||||
|
||||
// test all combinations of these, for both watch cache enabled and disabled:
|
||||
limits := []int64{0, 6}
|
||||
continueTokens := []string{"", validContinueToken, invalidContinueToken}
|
||||
rvs := []string{"", "0", compactedRv, invalidResourceVersion}
|
||||
rvMatches := []metav1.ResourceVersionMatch{
|
||||
"",
|
||||
metav1.ResourceVersionMatchNotOlderThan,
|
||||
metav1.ResourceVersionMatchExact,
|
||||
invalidResourceVersionMatch,
|
||||
}
|
||||
|
||||
for _, limit := range limits {
|
||||
for _, continueToken := range continueTokens {
|
||||
for _, rv := range rvs {
|
||||
for _, rvMatch := range rvMatches {
|
||||
name := fmt.Sprintf("limit=%d continue=%s rv=%s rvMatch=%s", limit, continueToken, rv, rvMatch)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
opts := metav1.ListOptions{
|
||||
ResourceVersion: rv,
|
||||
ResourceVersionMatch: rvMatch,
|
||||
Continue: continueToken,
|
||||
Limit: limit,
|
||||
}
|
||||
testListOptionsCase(t, rsClient, watchCacheEnabled, opts, compactedRv)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testListOptionsCase(t *testing.T, rsClient appsv1.ReplicaSetInterface, watchCacheEnabled bool, opts metav1.ListOptions, compactedRv string) {
|
||||
listObj, err := rsClient.List(context.Background(), opts)
|
||||
|
||||
// check for expected validation errors
|
||||
if opts.ResourceVersion == "" && opts.ResourceVersionMatch != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch is forbidden unless resourceVersion is provided") {
|
||||
t.Fatalf("expected forbidden error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.Continue != "" && opts.ResourceVersionMatch != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch is forbidden when continue is provided") {
|
||||
t.Fatalf("expected forbidden error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.ResourceVersionMatch == invalidResourceVersionMatch {
|
||||
if err == nil || !strings.Contains(err.Error(), "supported values") {
|
||||
t.Fatalf("expected not supported error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact && opts.ResourceVersion == "0" {
|
||||
if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch \"exact\" is forbidden for resourceVersion \"0\"") {
|
||||
t.Fatalf("expected forbidden error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.ResourceVersion == invalidResourceVersion {
|
||||
if err == nil || !strings.Contains(err.Error(), "Invalid value") {
|
||||
t.Fatalf("expecting invalid value error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.Continue == invalidContinueToken {
|
||||
if err == nil || !strings.Contains(err.Error(), "continue key is not valid") {
|
||||
t.Fatalf("expected continue key not valid error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Should not be allowed for any resource version, but tightening the validation would be a breaking change
|
||||
if opts.Continue != "" && !(opts.ResourceVersion == "" || opts.ResourceVersion == "0") {
|
||||
if err == nil || !strings.Contains(err.Error(), "specifying resource version is not allowed when using continue") {
|
||||
t.Fatalf("expected not allowed error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check for too old errors
|
||||
isExact := opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
|
||||
// Legacy corner cases that can be avoided by using an explicit resourceVersionMatch value
|
||||
// see https://kubernetes.io/docs/reference/using-api/api-concepts/#the-resourceversion-parameter
|
||||
isLegacyExact := opts.Limit > 0 && opts.ResourceVersionMatch == ""
|
||||
|
||||
if opts.ResourceVersion == compactedRv && (isExact || isLegacyExact) {
|
||||
if err == nil || !strings.Contains(err.Error(), "The resourceVersion for the provided list is too old") {
|
||||
t.Fatalf("expected too old error, but got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// test successful responses
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
items, err := meta.ExtractList(listObj)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract list from %v", listObj)
|
||||
}
|
||||
count := int64(len(items))
|
||||
|
||||
// Cacher.GetToList defines this for logic to decide if the watch cache is skipped. We need to know it to know if
|
||||
// the limit is respected when testing here.
|
||||
pagingEnabled := utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
|
||||
hasContinuation := pagingEnabled && len(opts.Continue) > 0
|
||||
hasLimit := pagingEnabled && opts.Limit > 0 && opts.ResourceVersion != "0"
|
||||
skipWatchCache := opts.ResourceVersion == "" || hasContinuation || hasLimit || isExact
|
||||
usingWatchCache := watchCacheEnabled && !skipWatchCache
|
||||
|
||||
if usingWatchCache { // watch cache does not respect limit and is not used for continue
|
||||
if count != 10 {
|
||||
t.Errorf("Expected list size to be 10 but got %d", count) // limit is ignored if watch cache is hit
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Continue != "" {
|
||||
if count != 4 {
|
||||
t.Errorf("Expected list size of 4 but got %d", count)
|
||||
}
|
||||
return
|
||||
}
|
||||
if opts.Limit > 0 {
|
||||
if count != opts.Limit {
|
||||
t.Errorf("Expected list size to be limited to %d but got %d", opts.Limit, count)
|
||||
}
|
||||
return
|
||||
}
|
||||
if count != 10 {
|
||||
t.Errorf("Expected list size to be 10 but got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListResourceVersion0(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
name string
|
||||
|
Reference in New Issue
Block a user