Add testcase for persistentVolume (#832)
* Add testcase for IsPersistentVolumeExist, To #34515520 Signed-off-by: cheyang <cheyang@163.com> * Add testcase for IsPersistentVolumeExist, To #34515520 Signed-off-by: cheyang <cheyang@163.com> * add vendor for testing Signed-off-by: cheyang <cheyang@163.com> * Add testcase for IsPersistentVolumeExist, To #34515520 Signed-off-by: cheyang <cheyang@163.com>
This commit is contained in:
parent
ec45f935a8
commit
2f437044c1
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
- name: CI Preparation
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
|
||||
arch=$(go env GOARCH)
|
||||
curl -L https://go.kubebuilder.io/dl/2.3.1/linux/${arch} | tar -xz -C /tmp/
|
||||
sudo mv /tmp/kubebuilder_2.3.1_linux_${arch} /usr/local/kubebuilder
|
||||
|
|
|
@ -11,7 +11,7 @@ matrix:
|
|||
- CI_TEST_FLAGS='-race -coverprofile=coverage.txt -covermode=atomic'
|
||||
sudo: true
|
||||
before_script:
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0
|
||||
- arch=$(go env GOARCH)
|
||||
- curl -L https://go.kubebuilder.io/dl/2.3.1/linux/${arch} | tar -xz -C /tmp/
|
||||
- sudo mv /tmp/kubebuilder_2.3.1_linux_${arch} /usr/local/kubebuilder
|
||||
|
|
|
@ -272,7 +272,7 @@ func (e *AlluxioEngine) destroyWorkers(expectedWorkers int32) (currentWorkers in
|
|||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
node, err := kubeclient.GetNode(e.Client, nodeName)
|
||||
if err != nil {
|
||||
e.Log.Error(err, "Fail to get node","nodename",nodeName)
|
||||
e.Log.Error(err, "Fail to get node", "nodename", nodeName)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
package kubeclient
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
testScheme *runtime.Scheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
testScheme = runtime.NewScheme()
|
||||
_ = v1.AddToScheme(testScheme)
|
||||
}
|
||||
|
||||
func TestGetPVCNamesFromPod(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pod := v1.Pod{}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
|
||||
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 kubeclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fluid-cloudnative/fluid/pkg/common"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
// Use fake client because of it will be maintained in the long term
|
||||
// due to https://github.com/kubernetes-sigs/controller-runtime/pull/1101
|
||||
func TestIsPersistentVolumeClaimExist(t *testing.T) {
|
||||
|
||||
namespace := "default"
|
||||
testPVCInputs := []*v1.PersistentVolumeClaim{&v1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "notCreatedByFluid",
|
||||
Namespace: namespace},
|
||||
Spec: v1.PersistentVolumeClaimSpec{},
|
||||
}, &v1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "createdByFluid",
|
||||
Annotations: common.ExpectedFluidAnnotations,
|
||||
Namespace: namespace},
|
||||
Spec: v1.PersistentVolumeClaimSpec{},
|
||||
}}
|
||||
|
||||
testPVCs := []runtime.Object{}
|
||||
|
||||
for _, pvc := range testPVCInputs {
|
||||
testPVCs = append(testPVCs, pvc.DeepCopy())
|
||||
}
|
||||
|
||||
client := fake.NewFakeClientWithScheme(testScheme, testPVCs...)
|
||||
|
||||
type args struct {
|
||||
name string
|
||||
namespace string
|
||||
annotations map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "volume doesn't exist",
|
||||
args: args{
|
||||
name: "notExist",
|
||||
namespace: namespace,
|
||||
annotations: map[string]string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "volume is not created by fluid",
|
||||
args: args{
|
||||
name: "notCreatedByFluid",
|
||||
namespace: namespace,
|
||||
annotations: map[string]string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "volume is created by fluid",
|
||||
args: args{
|
||||
name: "createdByFluid",
|
||||
namespace: namespace,
|
||||
annotations: common.ExpectedFluidAnnotations,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, _ := IsPersistentVolumeClaimExist(client, tt.args.name, tt.args.namespace, tt.args.annotations); got != tt.want {
|
||||
t.Errorf("testcase %v IsPersistentVolumeClaimExist() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
|
||||
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 kubeclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fluid-cloudnative/fluid/pkg/common"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
// Use fake client because of it will be maintained in the long term
|
||||
// due to https://github.com/kubernetes-sigs/controller-runtime/pull/1101
|
||||
func TestIsPersistentVolumeExist(t *testing.T) {
|
||||
|
||||
testPVInputs := []*v1.PersistentVolume{&v1.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "notCreatedByFluid"},
|
||||
Spec: v1.PersistentVolumeSpec{},
|
||||
}, &v1.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "createdByFluid", Annotations: common.ExpectedFluidAnnotations},
|
||||
Spec: v1.PersistentVolumeSpec{},
|
||||
}}
|
||||
|
||||
testPVs := []runtime.Object{}
|
||||
|
||||
for _, pv := range testPVInputs {
|
||||
testPVs = append(testPVs, pv.DeepCopy())
|
||||
}
|
||||
|
||||
client := fake.NewFakeClientWithScheme(testScheme, testPVs...)
|
||||
|
||||
type args struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "volume doesn't exist",
|
||||
args: args{
|
||||
name: "notExist",
|
||||
annotations: map[string]string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "volume is not created by fluid",
|
||||
args: args{
|
||||
name: "notCreatedByFluid",
|
||||
annotations: map[string]string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "volume is created by fluid",
|
||||
args: args{
|
||||
name: "createdByFluid",
|
||||
annotations: common.ExpectedFluidAnnotations,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, _ := IsPersistentVolumeExist(client, tt.args.name, tt.args.annotations); got != tt.want {
|
||||
t.Errorf("testcase %v IsPersistentVolumeExist() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
Copyright 2015 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 rand provides utilities related to randomization.
|
||||
package rand
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var rng = struct {
|
||||
sync.Mutex
|
||||
rand *rand.Rand
|
||||
}{
|
||||
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
|
||||
// Int returns a non-negative pseudo-random int.
|
||||
func Int() int {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
return rng.rand.Int()
|
||||
}
|
||||
|
||||
// Intn generates an integer in range [0,max).
|
||||
// By design this should panic if input is invalid, <= 0.
|
||||
func Intn(max int) int {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
return rng.rand.Intn(max)
|
||||
}
|
||||
|
||||
// IntnRange generates an integer in range [min,max).
|
||||
// By design this should panic if input is invalid, <= 0.
|
||||
func IntnRange(min, max int) int {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
return rng.rand.Intn(max-min) + min
|
||||
}
|
||||
|
||||
// IntnRange generates an int64 integer in range [min,max).
|
||||
// By design this should panic if input is invalid, <= 0.
|
||||
func Int63nRange(min, max int64) int64 {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
return rng.rand.Int63n(max-min) + min
|
||||
}
|
||||
|
||||
// Seed seeds the rng with the provided seed.
|
||||
func Seed(seed int64) {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
|
||||
rng.rand = rand.New(rand.NewSource(seed))
|
||||
}
|
||||
|
||||
// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n)
|
||||
// from the default Source.
|
||||
func Perm(n int) []int {
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
return rng.rand.Perm(n)
|
||||
}
|
||||
|
||||
const (
|
||||
// We omit vowels from the set of available characters to reduce the chances
|
||||
// of "bad words" being formed.
|
||||
alphanums = "bcdfghjklmnpqrstvwxz2456789"
|
||||
// No. of bits required to index into alphanums string.
|
||||
alphanumsIdxBits = 5
|
||||
// Mask used to extract last alphanumsIdxBits of an int.
|
||||
alphanumsIdxMask = 1<<alphanumsIdxBits - 1
|
||||
// No. of random letters we can extract from a single int63.
|
||||
maxAlphanumsPerInt = 63 / alphanumsIdxBits
|
||||
)
|
||||
|
||||
// String generates a random alphanumeric string, without vowels, which is n
|
||||
// characters long. This will panic if n is less than zero.
|
||||
// How the random string is created:
|
||||
// - we generate random int63's
|
||||
// - from each int63, we are extracting multiple random letters by bit-shifting and masking
|
||||
// - if some index is out of range of alphanums we neglect it (unlikely to happen multiple times in a row)
|
||||
func String(n int) string {
|
||||
b := make([]byte, n)
|
||||
rng.Lock()
|
||||
defer rng.Unlock()
|
||||
|
||||
randomInt63 := rng.rand.Int63()
|
||||
remaining := maxAlphanumsPerInt
|
||||
for i := 0; i < n; {
|
||||
if remaining == 0 {
|
||||
randomInt63, remaining = rng.rand.Int63(), maxAlphanumsPerInt
|
||||
}
|
||||
if idx := int(randomInt63 & alphanumsIdxMask); idx < len(alphanums) {
|
||||
b[i] = alphanums[idx]
|
||||
i++
|
||||
}
|
||||
randomInt63 >>= alphanumsIdxBits
|
||||
remaining--
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and
|
||||
// ensures that strings generated from hash functions appear consistent throughout the API.
|
||||
func SafeEncodeString(s string) string {
|
||||
r := make([]byte, len(s))
|
||||
for i, b := range []rune(s) {
|
||||
r[i] = alphanums[(int(b) % len(alphanums))]
|
||||
}
|
||||
return string(r)
|
||||
}
|
|
@ -0,0 +1,681 @@
|
|||
/*
|
||||
Copyright 2015 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
func NewRootGetAction(resource schema.GroupVersionResource, name string) GetActionImpl {
|
||||
action := GetActionImpl{}
|
||||
action.Verb = "get"
|
||||
action.Resource = resource
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewGetAction(resource schema.GroupVersionResource, namespace, name string) GetActionImpl {
|
||||
action := GetActionImpl{}
|
||||
action.Verb = "get"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewGetSubresourceAction(resource schema.GroupVersionResource, namespace, subresource, name string) GetActionImpl {
|
||||
action := GetActionImpl{}
|
||||
action.Verb = "get"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootGetSubresourceAction(resource schema.GroupVersionResource, subresource, name string) GetActionImpl {
|
||||
action := GetActionImpl{}
|
||||
action.Verb = "get"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootListAction(resource schema.GroupVersionResource, kind schema.GroupVersionKind, opts interface{}) ListActionImpl {
|
||||
action := ListActionImpl{}
|
||||
action.Verb = "list"
|
||||
action.Resource = resource
|
||||
action.Kind = kind
|
||||
labelSelector, fieldSelector, _ := ExtractFromListOptions(opts)
|
||||
action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewListAction(resource schema.GroupVersionResource, kind schema.GroupVersionKind, namespace string, opts interface{}) ListActionImpl {
|
||||
action := ListActionImpl{}
|
||||
action.Verb = "list"
|
||||
action.Resource = resource
|
||||
action.Kind = kind
|
||||
action.Namespace = namespace
|
||||
labelSelector, fieldSelector, _ := ExtractFromListOptions(opts)
|
||||
action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootCreateAction(resource schema.GroupVersionResource, object runtime.Object) CreateActionImpl {
|
||||
action := CreateActionImpl{}
|
||||
action.Verb = "create"
|
||||
action.Resource = resource
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewCreateAction(resource schema.GroupVersionResource, namespace string, object runtime.Object) CreateActionImpl {
|
||||
action := CreateActionImpl{}
|
||||
action.Verb = "create"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootCreateSubresourceAction(resource schema.GroupVersionResource, name, subresource string, object runtime.Object) CreateActionImpl {
|
||||
action := CreateActionImpl{}
|
||||
action.Verb = "create"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Name = name
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewCreateSubresourceAction(resource schema.GroupVersionResource, name, subresource, namespace string, object runtime.Object) CreateActionImpl {
|
||||
action := CreateActionImpl{}
|
||||
action.Verb = "create"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Subresource = subresource
|
||||
action.Name = name
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootUpdateAction(resource schema.GroupVersionResource, object runtime.Object) UpdateActionImpl {
|
||||
action := UpdateActionImpl{}
|
||||
action.Verb = "update"
|
||||
action.Resource = resource
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewUpdateAction(resource schema.GroupVersionResource, namespace string, object runtime.Object) UpdateActionImpl {
|
||||
action := UpdateActionImpl{}
|
||||
action.Verb = "update"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootPatchAction(resource schema.GroupVersionResource, name string, pt types.PatchType, patch []byte) PatchActionImpl {
|
||||
action := PatchActionImpl{}
|
||||
action.Verb = "patch"
|
||||
action.Resource = resource
|
||||
action.Name = name
|
||||
action.PatchType = pt
|
||||
action.Patch = patch
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewPatchAction(resource schema.GroupVersionResource, namespace string, name string, pt types.PatchType, patch []byte) PatchActionImpl {
|
||||
action := PatchActionImpl{}
|
||||
action.Verb = "patch"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
action.PatchType = pt
|
||||
action.Patch = patch
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootPatchSubresourceAction(resource schema.GroupVersionResource, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl {
|
||||
action := PatchActionImpl{}
|
||||
action.Verb = "patch"
|
||||
action.Resource = resource
|
||||
action.Subresource = path.Join(subresources...)
|
||||
action.Name = name
|
||||
action.PatchType = pt
|
||||
action.Patch = patch
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewPatchSubresourceAction(resource schema.GroupVersionResource, namespace, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl {
|
||||
action := PatchActionImpl{}
|
||||
action.Verb = "patch"
|
||||
action.Resource = resource
|
||||
action.Subresource = path.Join(subresources...)
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
action.PatchType = pt
|
||||
action.Patch = patch
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootUpdateSubresourceAction(resource schema.GroupVersionResource, subresource string, object runtime.Object) UpdateActionImpl {
|
||||
action := UpdateActionImpl{}
|
||||
action.Verb = "update"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
func NewUpdateSubresourceAction(resource schema.GroupVersionResource, subresource string, namespace string, object runtime.Object) UpdateActionImpl {
|
||||
action := UpdateActionImpl{}
|
||||
action.Verb = "update"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Namespace = namespace
|
||||
action.Object = object
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootDeleteAction(resource schema.GroupVersionResource, name string) DeleteActionImpl {
|
||||
action := DeleteActionImpl{}
|
||||
action.Verb = "delete"
|
||||
action.Resource = resource
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootDeleteSubresourceAction(resource schema.GroupVersionResource, subresource string, name string) DeleteActionImpl {
|
||||
action := DeleteActionImpl{}
|
||||
action.Verb = "delete"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewDeleteAction(resource schema.GroupVersionResource, namespace, name string) DeleteActionImpl {
|
||||
action := DeleteActionImpl{}
|
||||
action.Verb = "delete"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewDeleteSubresourceAction(resource schema.GroupVersionResource, subresource, namespace, name string) DeleteActionImpl {
|
||||
action := DeleteActionImpl{}
|
||||
action.Verb = "delete"
|
||||
action.Resource = resource
|
||||
action.Subresource = subresource
|
||||
action.Namespace = namespace
|
||||
action.Name = name
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootDeleteCollectionAction(resource schema.GroupVersionResource, opts interface{}) DeleteCollectionActionImpl {
|
||||
action := DeleteCollectionActionImpl{}
|
||||
action.Verb = "delete-collection"
|
||||
action.Resource = resource
|
||||
labelSelector, fieldSelector, _ := ExtractFromListOptions(opts)
|
||||
action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewDeleteCollectionAction(resource schema.GroupVersionResource, namespace string, opts interface{}) DeleteCollectionActionImpl {
|
||||
action := DeleteCollectionActionImpl{}
|
||||
action.Verb = "delete-collection"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
labelSelector, fieldSelector, _ := ExtractFromListOptions(opts)
|
||||
action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewRootWatchAction(resource schema.GroupVersionResource, opts interface{}) WatchActionImpl {
|
||||
action := WatchActionImpl{}
|
||||
action.Verb = "watch"
|
||||
action.Resource = resource
|
||||
labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts)
|
||||
action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func ExtractFromListOptions(opts interface{}) (labelSelector labels.Selector, fieldSelector fields.Selector, resourceVersion string) {
|
||||
var err error
|
||||
switch t := opts.(type) {
|
||||
case metav1.ListOptions:
|
||||
labelSelector, err = labels.Parse(t.LabelSelector)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid selector %q: %v", t.LabelSelector, err))
|
||||
}
|
||||
fieldSelector, err = fields.ParseSelector(t.FieldSelector)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("invalid selector %q: %v", t.FieldSelector, err))
|
||||
}
|
||||
resourceVersion = t.ResourceVersion
|
||||
default:
|
||||
panic(fmt.Errorf("expect a ListOptions %T", opts))
|
||||
}
|
||||
if labelSelector == nil {
|
||||
labelSelector = labels.Everything()
|
||||
}
|
||||
if fieldSelector == nil {
|
||||
fieldSelector = fields.Everything()
|
||||
}
|
||||
return labelSelector, fieldSelector, resourceVersion
|
||||
}
|
||||
|
||||
func NewWatchAction(resource schema.GroupVersionResource, namespace string, opts interface{}) WatchActionImpl {
|
||||
action := WatchActionImpl{}
|
||||
action.Verb = "watch"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts)
|
||||
action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
func NewProxyGetAction(resource schema.GroupVersionResource, namespace, scheme, name, port, path string, params map[string]string) ProxyGetActionImpl {
|
||||
action := ProxyGetActionImpl{}
|
||||
action.Verb = "get"
|
||||
action.Resource = resource
|
||||
action.Namespace = namespace
|
||||
action.Scheme = scheme
|
||||
action.Name = name
|
||||
action.Port = port
|
||||
action.Path = path
|
||||
action.Params = params
|
||||
return action
|
||||
}
|
||||
|
||||
type ListRestrictions struct {
|
||||
Labels labels.Selector
|
||||
Fields fields.Selector
|
||||
}
|
||||
type WatchRestrictions struct {
|
||||
Labels labels.Selector
|
||||
Fields fields.Selector
|
||||
ResourceVersion string
|
||||
}
|
||||
|
||||
type Action interface {
|
||||
GetNamespace() string
|
||||
GetVerb() string
|
||||
GetResource() schema.GroupVersionResource
|
||||
GetSubresource() string
|
||||
Matches(verb, resource string) bool
|
||||
|
||||
// DeepCopy is used to copy an action to avoid any risk of accidental mutation. Most people never need to call this
|
||||
// because the invocation logic deep copies before calls to storage and reactors.
|
||||
DeepCopy() Action
|
||||
}
|
||||
|
||||
type GenericAction interface {
|
||||
Action
|
||||
GetValue() interface{}
|
||||
}
|
||||
|
||||
type GetAction interface {
|
||||
Action
|
||||
GetName() string
|
||||
}
|
||||
|
||||
type ListAction interface {
|
||||
Action
|
||||
GetListRestrictions() ListRestrictions
|
||||
}
|
||||
|
||||
type CreateAction interface {
|
||||
Action
|
||||
GetObject() runtime.Object
|
||||
}
|
||||
|
||||
type UpdateAction interface {
|
||||
Action
|
||||
GetObject() runtime.Object
|
||||
}
|
||||
|
||||
type DeleteAction interface {
|
||||
Action
|
||||
GetName() string
|
||||
}
|
||||
|
||||
type DeleteCollectionAction interface {
|
||||
Action
|
||||
GetListRestrictions() ListRestrictions
|
||||
}
|
||||
|
||||
type PatchAction interface {
|
||||
Action
|
||||
GetName() string
|
||||
GetPatchType() types.PatchType
|
||||
GetPatch() []byte
|
||||
}
|
||||
|
||||
type WatchAction interface {
|
||||
Action
|
||||
GetWatchRestrictions() WatchRestrictions
|
||||
}
|
||||
|
||||
type ProxyGetAction interface {
|
||||
Action
|
||||
GetScheme() string
|
||||
GetName() string
|
||||
GetPort() string
|
||||
GetPath() string
|
||||
GetParams() map[string]string
|
||||
}
|
||||
|
||||
type ActionImpl struct {
|
||||
Namespace string
|
||||
Verb string
|
||||
Resource schema.GroupVersionResource
|
||||
Subresource string
|
||||
}
|
||||
|
||||
func (a ActionImpl) GetNamespace() string {
|
||||
return a.Namespace
|
||||
}
|
||||
func (a ActionImpl) GetVerb() string {
|
||||
return a.Verb
|
||||
}
|
||||
func (a ActionImpl) GetResource() schema.GroupVersionResource {
|
||||
return a.Resource
|
||||
}
|
||||
func (a ActionImpl) GetSubresource() string {
|
||||
return a.Subresource
|
||||
}
|
||||
func (a ActionImpl) Matches(verb, resource string) bool {
|
||||
// Stay backwards compatible.
|
||||
if !strings.Contains(resource, "/") {
|
||||
return strings.EqualFold(verb, a.Verb) &&
|
||||
strings.EqualFold(resource, a.Resource.Resource)
|
||||
}
|
||||
|
||||
parts := strings.SplitN(resource, "/", 2)
|
||||
topresource, subresource := parts[0], parts[1]
|
||||
|
||||
return strings.EqualFold(verb, a.Verb) &&
|
||||
strings.EqualFold(topresource, a.Resource.Resource) &&
|
||||
strings.EqualFold(subresource, a.Subresource)
|
||||
}
|
||||
func (a ActionImpl) DeepCopy() Action {
|
||||
ret := a
|
||||
return ret
|
||||
}
|
||||
|
||||
type GenericActionImpl struct {
|
||||
ActionImpl
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (a GenericActionImpl) GetValue() interface{} {
|
||||
return a.Value
|
||||
}
|
||||
|
||||
func (a GenericActionImpl) DeepCopy() Action {
|
||||
return GenericActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
// TODO this is wrong, but no worse than before
|
||||
Value: a.Value,
|
||||
}
|
||||
}
|
||||
|
||||
type GetActionImpl struct {
|
||||
ActionImpl
|
||||
Name string
|
||||
}
|
||||
|
||||
func (a GetActionImpl) GetName() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (a GetActionImpl) DeepCopy() Action {
|
||||
return GetActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
type ListActionImpl struct {
|
||||
ActionImpl
|
||||
Kind schema.GroupVersionKind
|
||||
Name string
|
||||
ListRestrictions ListRestrictions
|
||||
}
|
||||
|
||||
func (a ListActionImpl) GetKind() schema.GroupVersionKind {
|
||||
return a.Kind
|
||||
}
|
||||
|
||||
func (a ListActionImpl) GetListRestrictions() ListRestrictions {
|
||||
return a.ListRestrictions
|
||||
}
|
||||
|
||||
func (a ListActionImpl) DeepCopy() Action {
|
||||
return ListActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Kind: a.Kind,
|
||||
Name: a.Name,
|
||||
ListRestrictions: ListRestrictions{
|
||||
Labels: a.ListRestrictions.Labels.DeepCopySelector(),
|
||||
Fields: a.ListRestrictions.Fields.DeepCopySelector(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type CreateActionImpl struct {
|
||||
ActionImpl
|
||||
Name string
|
||||
Object runtime.Object
|
||||
}
|
||||
|
||||
func (a CreateActionImpl) GetObject() runtime.Object {
|
||||
return a.Object
|
||||
}
|
||||
|
||||
func (a CreateActionImpl) DeepCopy() Action {
|
||||
return CreateActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Name: a.Name,
|
||||
Object: a.Object.DeepCopyObject(),
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateActionImpl struct {
|
||||
ActionImpl
|
||||
Object runtime.Object
|
||||
}
|
||||
|
||||
func (a UpdateActionImpl) GetObject() runtime.Object {
|
||||
return a.Object
|
||||
}
|
||||
|
||||
func (a UpdateActionImpl) DeepCopy() Action {
|
||||
return UpdateActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Object: a.Object.DeepCopyObject(),
|
||||
}
|
||||
}
|
||||
|
||||
type PatchActionImpl struct {
|
||||
ActionImpl
|
||||
Name string
|
||||
PatchType types.PatchType
|
||||
Patch []byte
|
||||
}
|
||||
|
||||
func (a PatchActionImpl) GetName() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (a PatchActionImpl) GetPatch() []byte {
|
||||
return a.Patch
|
||||
}
|
||||
|
||||
func (a PatchActionImpl) GetPatchType() types.PatchType {
|
||||
return a.PatchType
|
||||
}
|
||||
|
||||
func (a PatchActionImpl) DeepCopy() Action {
|
||||
patch := make([]byte, len(a.Patch))
|
||||
copy(patch, a.Patch)
|
||||
return PatchActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Name: a.Name,
|
||||
PatchType: a.PatchType,
|
||||
Patch: patch,
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteActionImpl struct {
|
||||
ActionImpl
|
||||
Name string
|
||||
}
|
||||
|
||||
func (a DeleteActionImpl) GetName() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (a DeleteActionImpl) DeepCopy() Action {
|
||||
return DeleteActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteCollectionActionImpl struct {
|
||||
ActionImpl
|
||||
ListRestrictions ListRestrictions
|
||||
}
|
||||
|
||||
func (a DeleteCollectionActionImpl) GetListRestrictions() ListRestrictions {
|
||||
return a.ListRestrictions
|
||||
}
|
||||
|
||||
func (a DeleteCollectionActionImpl) DeepCopy() Action {
|
||||
return DeleteCollectionActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
ListRestrictions: ListRestrictions{
|
||||
Labels: a.ListRestrictions.Labels.DeepCopySelector(),
|
||||
Fields: a.ListRestrictions.Fields.DeepCopySelector(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type WatchActionImpl struct {
|
||||
ActionImpl
|
||||
WatchRestrictions WatchRestrictions
|
||||
}
|
||||
|
||||
func (a WatchActionImpl) GetWatchRestrictions() WatchRestrictions {
|
||||
return a.WatchRestrictions
|
||||
}
|
||||
|
||||
func (a WatchActionImpl) DeepCopy() Action {
|
||||
return WatchActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
WatchRestrictions: WatchRestrictions{
|
||||
Labels: a.WatchRestrictions.Labels.DeepCopySelector(),
|
||||
Fields: a.WatchRestrictions.Fields.DeepCopySelector(),
|
||||
ResourceVersion: a.WatchRestrictions.ResourceVersion,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ProxyGetActionImpl struct {
|
||||
ActionImpl
|
||||
Scheme string
|
||||
Name string
|
||||
Port string
|
||||
Path string
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) GetScheme() string {
|
||||
return a.Scheme
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) GetName() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) GetPort() string {
|
||||
return a.Port
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) GetPath() string {
|
||||
return a.Path
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) GetParams() map[string]string {
|
||||
return a.Params
|
||||
}
|
||||
|
||||
func (a ProxyGetActionImpl) DeepCopy() Action {
|
||||
params := map[string]string{}
|
||||
for k, v := range a.Params {
|
||||
params[k] = v
|
||||
}
|
||||
return ProxyGetActionImpl{
|
||||
ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl),
|
||||
Scheme: a.Scheme,
|
||||
Name: a.Name,
|
||||
Port: a.Port,
|
||||
Path: a.Path,
|
||||
Params: params,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
Copyright 2016 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// Fake implements client.Interface. Meant to be embedded into a struct to get
|
||||
// a default implementation. This makes faking out just the method you want to
|
||||
// test easier.
|
||||
type Fake struct {
|
||||
sync.RWMutex
|
||||
actions []Action // these may be castable to other types, but "Action" is the minimum
|
||||
|
||||
// ReactionChain is the list of reactors that will be attempted for every
|
||||
// request in the order they are tried.
|
||||
ReactionChain []Reactor
|
||||
// WatchReactionChain is the list of watch reactors that will be attempted
|
||||
// for every request in the order they are tried.
|
||||
WatchReactionChain []WatchReactor
|
||||
// ProxyReactionChain is the list of proxy reactors that will be attempted
|
||||
// for every request in the order they are tried.
|
||||
ProxyReactionChain []ProxyReactor
|
||||
|
||||
Resources []*metav1.APIResourceList
|
||||
}
|
||||
|
||||
// Reactor is an interface to allow the composition of reaction functions.
|
||||
type Reactor interface {
|
||||
// Handles indicates whether or not this Reactor deals with a given
|
||||
// action.
|
||||
Handles(action Action) bool
|
||||
// React handles the action and returns results. It may choose to
|
||||
// delegate by indicated handled=false.
|
||||
React(action Action) (handled bool, ret runtime.Object, err error)
|
||||
}
|
||||
|
||||
// WatchReactor is an interface to allow the composition of watch functions.
|
||||
type WatchReactor interface {
|
||||
// Handles indicates whether or not this Reactor deals with a given
|
||||
// action.
|
||||
Handles(action Action) bool
|
||||
// React handles a watch action and returns results. It may choose to
|
||||
// delegate by indicating handled=false.
|
||||
React(action Action) (handled bool, ret watch.Interface, err error)
|
||||
}
|
||||
|
||||
// ProxyReactor is an interface to allow the composition of proxy get
|
||||
// functions.
|
||||
type ProxyReactor interface {
|
||||
// Handles indicates whether or not this Reactor deals with a given
|
||||
// action.
|
||||
Handles(action Action) bool
|
||||
// React handles a watch action and returns results. It may choose to
|
||||
// delegate by indicating handled=false.
|
||||
React(action Action) (handled bool, ret restclient.ResponseWrapper, err error)
|
||||
}
|
||||
|
||||
// ReactionFunc is a function that returns an object or error for a given
|
||||
// Action. If "handled" is false, then the test client will ignore the
|
||||
// results and continue to the next ReactionFunc. A ReactionFunc can describe
|
||||
// reactions on subresources by testing the result of the action's
|
||||
// GetSubresource() method.
|
||||
type ReactionFunc func(action Action) (handled bool, ret runtime.Object, err error)
|
||||
|
||||
// WatchReactionFunc is a function that returns a watch interface. If
|
||||
// "handled" is false, then the test client will ignore the results and
|
||||
// continue to the next ReactionFunc.
|
||||
type WatchReactionFunc func(action Action) (handled bool, ret watch.Interface, err error)
|
||||
|
||||
// ProxyReactionFunc is a function that returns a ResponseWrapper interface
|
||||
// for a given Action. If "handled" is false, then the test client will
|
||||
// ignore the results and continue to the next ProxyReactionFunc.
|
||||
type ProxyReactionFunc func(action Action) (handled bool, ret restclient.ResponseWrapper, err error)
|
||||
|
||||
// AddReactor appends a reactor to the end of the chain.
|
||||
func (c *Fake) AddReactor(verb, resource string, reaction ReactionFunc) {
|
||||
c.ReactionChain = append(c.ReactionChain, &SimpleReactor{verb, resource, reaction})
|
||||
}
|
||||
|
||||
// PrependReactor adds a reactor to the beginning of the chain.
|
||||
func (c *Fake) PrependReactor(verb, resource string, reaction ReactionFunc) {
|
||||
c.ReactionChain = append([]Reactor{&SimpleReactor{verb, resource, reaction}}, c.ReactionChain...)
|
||||
}
|
||||
|
||||
// AddWatchReactor appends a reactor to the end of the chain.
|
||||
func (c *Fake) AddWatchReactor(resource string, reaction WatchReactionFunc) {
|
||||
c.WatchReactionChain = append(c.WatchReactionChain, &SimpleWatchReactor{resource, reaction})
|
||||
}
|
||||
|
||||
// PrependWatchReactor adds a reactor to the beginning of the chain.
|
||||
func (c *Fake) PrependWatchReactor(resource string, reaction WatchReactionFunc) {
|
||||
c.WatchReactionChain = append([]WatchReactor{&SimpleWatchReactor{resource, reaction}}, c.WatchReactionChain...)
|
||||
}
|
||||
|
||||
// AddProxyReactor appends a reactor to the end of the chain.
|
||||
func (c *Fake) AddProxyReactor(resource string, reaction ProxyReactionFunc) {
|
||||
c.ProxyReactionChain = append(c.ProxyReactionChain, &SimpleProxyReactor{resource, reaction})
|
||||
}
|
||||
|
||||
// PrependProxyReactor adds a reactor to the beginning of the chain.
|
||||
func (c *Fake) PrependProxyReactor(resource string, reaction ProxyReactionFunc) {
|
||||
c.ProxyReactionChain = append([]ProxyReactor{&SimpleProxyReactor{resource, reaction}}, c.ProxyReactionChain...)
|
||||
}
|
||||
|
||||
// Invokes records the provided Action and then invokes the ReactionFunc that
|
||||
// handles the action if one exists. defaultReturnObj is expected to be of the
|
||||
// same type a normal call would return.
|
||||
func (c *Fake) Invokes(action Action, defaultReturnObj runtime.Object) (runtime.Object, error) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
actionCopy := action.DeepCopy()
|
||||
c.actions = append(c.actions, action.DeepCopy())
|
||||
for _, reactor := range c.ReactionChain {
|
||||
if !reactor.Handles(actionCopy) {
|
||||
continue
|
||||
}
|
||||
|
||||
handled, ret, err := reactor.React(actionCopy)
|
||||
if !handled {
|
||||
continue
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return defaultReturnObj, nil
|
||||
}
|
||||
|
||||
// InvokesWatch records the provided Action and then invokes the ReactionFunc
|
||||
// that handles the action if one exists.
|
||||
func (c *Fake) InvokesWatch(action Action) (watch.Interface, error) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
actionCopy := action.DeepCopy()
|
||||
c.actions = append(c.actions, action.DeepCopy())
|
||||
for _, reactor := range c.WatchReactionChain {
|
||||
if !reactor.Handles(actionCopy) {
|
||||
continue
|
||||
}
|
||||
|
||||
handled, ret, err := reactor.React(actionCopy)
|
||||
if !handled {
|
||||
continue
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled watch: %#v", action)
|
||||
}
|
||||
|
||||
// InvokesProxy records the provided Action and then invokes the ReactionFunc
|
||||
// that handles the action if one exists.
|
||||
func (c *Fake) InvokesProxy(action Action) restclient.ResponseWrapper {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
actionCopy := action.DeepCopy()
|
||||
c.actions = append(c.actions, action.DeepCopy())
|
||||
for _, reactor := range c.ProxyReactionChain {
|
||||
if !reactor.Handles(actionCopy) {
|
||||
continue
|
||||
}
|
||||
|
||||
handled, ret, err := reactor.React(actionCopy)
|
||||
if !handled || err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearActions clears the history of actions called on the fake client.
|
||||
func (c *Fake) ClearActions() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.actions = make([]Action, 0)
|
||||
}
|
||||
|
||||
// Actions returns a chronologically ordered slice fake actions called on the
|
||||
// fake client.
|
||||
func (c *Fake) Actions() []Action {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
fa := make([]Action, len(c.actions))
|
||||
copy(fa, c.actions)
|
||||
return fa
|
||||
}
|
|
@ -0,0 +1,577 @@
|
|||
/*
|
||||
Copyright 2015 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
|
||||
"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/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// ObjectTracker keeps track of objects. It is intended to be used to
|
||||
// fake calls to a server by returning objects based on their kind,
|
||||
// namespace and name.
|
||||
type ObjectTracker interface {
|
||||
// Add adds an object to the tracker. If object being added
|
||||
// is a list, its items are added separately.
|
||||
Add(obj runtime.Object) error
|
||||
|
||||
// Get retrieves the object by its kind, namespace and name.
|
||||
Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error)
|
||||
|
||||
// Create adds an object to the tracker in the specified namespace.
|
||||
Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error
|
||||
|
||||
// Update updates an existing object in the tracker in the specified namespace.
|
||||
Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error
|
||||
|
||||
// List retrieves all objects of a given kind in the given
|
||||
// namespace. Only non-List kinds are accepted.
|
||||
List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error)
|
||||
|
||||
// Delete deletes an existing object from the tracker. If object
|
||||
// didn't exist in the tracker prior to deletion, Delete returns
|
||||
// no error.
|
||||
Delete(gvr schema.GroupVersionResource, ns, name string) error
|
||||
|
||||
// Watch watches objects from the tracker. Watch returns a channel
|
||||
// which will push added / modified / deleted object.
|
||||
Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error)
|
||||
}
|
||||
|
||||
// ObjectScheme abstracts the implementation of common operations on objects.
|
||||
type ObjectScheme interface {
|
||||
runtime.ObjectCreater
|
||||
runtime.ObjectTyper
|
||||
}
|
||||
|
||||
// ObjectReaction returns a ReactionFunc that applies core.Action to
|
||||
// the given tracker.
|
||||
func ObjectReaction(tracker ObjectTracker) ReactionFunc {
|
||||
return func(action Action) (bool, runtime.Object, error) {
|
||||
ns := action.GetNamespace()
|
||||
gvr := action.GetResource()
|
||||
// Here and below we need to switch on implementation types,
|
||||
// not on interfaces, as some interfaces are identical
|
||||
// (e.g. UpdateAction and CreateAction), so if we use them,
|
||||
// updates and creates end up matching the same case branch.
|
||||
switch action := action.(type) {
|
||||
|
||||
case ListActionImpl:
|
||||
obj, err := tracker.List(gvr, action.GetKind(), ns)
|
||||
return true, obj, err
|
||||
|
||||
case GetActionImpl:
|
||||
obj, err := tracker.Get(gvr, ns, action.GetName())
|
||||
return true, obj, err
|
||||
|
||||
case CreateActionImpl:
|
||||
objMeta, err := meta.Accessor(action.GetObject())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
if action.GetSubresource() == "" {
|
||||
err = tracker.Create(gvr, action.GetObject(), ns)
|
||||
} else {
|
||||
// TODO: Currently we're handling subresource creation as an update
|
||||
// on the enclosing resource. This works for some subresources but
|
||||
// might not be generic enough.
|
||||
err = tracker.Update(gvr, action.GetObject(), ns)
|
||||
}
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
obj, err := tracker.Get(gvr, ns, objMeta.GetName())
|
||||
return true, obj, err
|
||||
|
||||
case UpdateActionImpl:
|
||||
objMeta, err := meta.Accessor(action.GetObject())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
err = tracker.Update(gvr, action.GetObject(), ns)
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
obj, err := tracker.Get(gvr, ns, objMeta.GetName())
|
||||
return true, obj, err
|
||||
|
||||
case DeleteActionImpl:
|
||||
err := tracker.Delete(gvr, ns, action.GetName())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
return true, nil, nil
|
||||
|
||||
case PatchActionImpl:
|
||||
obj, err := tracker.Get(gvr, ns, action.GetName())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
|
||||
old, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
|
||||
// reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields
|
||||
// in obj that are removed by patch are cleared
|
||||
value := reflect.ValueOf(obj)
|
||||
value.Elem().Set(reflect.New(value.Type().Elem()).Elem())
|
||||
|
||||
switch action.GetPatchType() {
|
||||
case types.JSONPatchType:
|
||||
patch, err := jsonpatch.DecodePatch(action.GetPatch())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
modified, err := patch.Apply(old)
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(modified, obj); err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
case types.MergePatchType:
|
||||
modified, err := jsonpatch.MergePatch(old, action.GetPatch())
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(modified, obj); err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
case types.StrategicMergePatchType:
|
||||
mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj)
|
||||
if err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
if err = json.Unmarshal(mergedByte, obj); err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
default:
|
||||
return true, nil, fmt.Errorf("PatchType is not supported")
|
||||
}
|
||||
|
||||
if err = tracker.Update(gvr, obj, ns); err != nil {
|
||||
return true, nil, err
|
||||
}
|
||||
|
||||
return true, obj, nil
|
||||
|
||||
default:
|
||||
return false, nil, fmt.Errorf("no reaction implemented for %s", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type tracker struct {
|
||||
scheme ObjectScheme
|
||||
decoder runtime.Decoder
|
||||
lock sync.RWMutex
|
||||
objects map[schema.GroupVersionResource][]runtime.Object
|
||||
// The value type of watchers is a map of which the key is either a namespace or
|
||||
// all/non namespace aka "" and its value is list of fake watchers.
|
||||
// Manipulations on resources will broadcast the notification events into the
|
||||
// watchers' channel. Note that too many unhandled events (currently 100,
|
||||
// see apimachinery/pkg/watch.DefaultChanSize) will cause a panic.
|
||||
watchers map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher
|
||||
}
|
||||
|
||||
var _ ObjectTracker = &tracker{}
|
||||
|
||||
// NewObjectTracker returns an ObjectTracker that can be used to keep track
|
||||
// of objects for the fake clientset. Mostly useful for unit tests.
|
||||
func NewObjectTracker(scheme ObjectScheme, decoder runtime.Decoder) ObjectTracker {
|
||||
return &tracker{
|
||||
scheme: scheme,
|
||||
decoder: decoder,
|
||||
objects: make(map[schema.GroupVersionResource][]runtime.Object),
|
||||
watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) {
|
||||
// Heuristic for list kind: original kind + List suffix. Might
|
||||
// not always be true but this tracker has a pretty limited
|
||||
// understanding of the actual API model.
|
||||
listGVK := gvk
|
||||
listGVK.Kind = listGVK.Kind + "List"
|
||||
// GVK does have the concept of "internal version". The scheme recognizes
|
||||
// the runtime.APIVersionInternal, but not the empty string.
|
||||
if listGVK.Version == "" {
|
||||
listGVK.Version = runtime.APIVersionInternal
|
||||
}
|
||||
|
||||
list, err := t.scheme.New(listGVK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !meta.IsListType(list) {
|
||||
return nil, fmt.Errorf("%q is not a list type", listGVK.Kind)
|
||||
}
|
||||
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
objs, ok := t.objects[gvr]
|
||||
if !ok {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
matchingObjs, err := filterByNamespace(objs, ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := meta.SetList(list, matchingObjs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.DeepCopyObject(), nil
|
||||
}
|
||||
|
||||
func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
fakewatcher := watch.NewRaceFreeFake()
|
||||
|
||||
if _, exists := t.watchers[gvr]; !exists {
|
||||
t.watchers[gvr] = make(map[string][]*watch.RaceFreeFakeWatcher)
|
||||
}
|
||||
t.watchers[gvr][ns] = append(t.watchers[gvr][ns], fakewatcher)
|
||||
return fakewatcher, nil
|
||||
}
|
||||
|
||||
func (t *tracker) Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) {
|
||||
errNotFound := errors.NewNotFound(gvr.GroupResource(), name)
|
||||
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
objs, ok := t.objects[gvr]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
var matchingObjs []runtime.Object
|
||||
for _, obj := range objs {
|
||||
acc, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if acc.GetNamespace() != ns {
|
||||
continue
|
||||
}
|
||||
if acc.GetName() != name {
|
||||
continue
|
||||
}
|
||||
matchingObjs = append(matchingObjs, obj)
|
||||
}
|
||||
if len(matchingObjs) == 0 {
|
||||
return nil, errNotFound
|
||||
}
|
||||
if len(matchingObjs) > 1 {
|
||||
return nil, fmt.Errorf("more than one object matched gvr %s, ns: %q name: %q", gvr, ns, name)
|
||||
}
|
||||
|
||||
// Only one object should match in the tracker if it works
|
||||
// correctly, as Add/Update methods enforce kind/namespace/name
|
||||
// uniqueness.
|
||||
obj := matchingObjs[0].DeepCopyObject()
|
||||
if status, ok := obj.(*metav1.Status); ok {
|
||||
if status.Status != metav1.StatusSuccess {
|
||||
return nil, &errors.StatusError{ErrStatus: *status}
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (t *tracker) Add(obj runtime.Object) error {
|
||||
if meta.IsListType(obj) {
|
||||
return t.addList(obj, false)
|
||||
}
|
||||
objMeta, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvks, _, err := t.scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if partial, ok := obj.(*metav1.PartialObjectMetadata); ok && len(partial.TypeMeta.APIVersion) > 0 {
|
||||
gvks = []schema.GroupVersionKind{partial.TypeMeta.GroupVersionKind()}
|
||||
}
|
||||
|
||||
if len(gvks) == 0 {
|
||||
return fmt.Errorf("no registered kinds for %v", obj)
|
||||
}
|
||||
for _, gvk := range gvks {
|
||||
// NOTE: UnsafeGuessKindToResource is a heuristic and default match. The
|
||||
// actual registration in apiserver can specify arbitrary route for a
|
||||
// gvk. If a test uses such objects, it cannot preset the tracker with
|
||||
// objects via Add(). Instead, it should trigger the Create() function
|
||||
// of the tracker, where an arbitrary gvr can be specified.
|
||||
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
|
||||
// Resource doesn't have the concept of "__internal" version, just set it to "".
|
||||
if gvr.Version == runtime.APIVersionInternal {
|
||||
gvr.Version = ""
|
||||
}
|
||||
|
||||
err := t.add(gvr, obj, objMeta.GetNamespace(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
|
||||
return t.add(gvr, obj, ns, false)
|
||||
}
|
||||
|
||||
func (t *tracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
|
||||
return t.add(gvr, obj, ns, true)
|
||||
}
|
||||
|
||||
func (t *tracker) getWatches(gvr schema.GroupVersionResource, ns string) []*watch.RaceFreeFakeWatcher {
|
||||
watches := []*watch.RaceFreeFakeWatcher{}
|
||||
if t.watchers[gvr] != nil {
|
||||
if w := t.watchers[gvr][ns]; w != nil {
|
||||
watches = append(watches, w...)
|
||||
}
|
||||
if ns != metav1.NamespaceAll {
|
||||
if w := t.watchers[gvr][metav1.NamespaceAll]; w != nil {
|
||||
watches = append(watches, w...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return watches
|
||||
}
|
||||
|
||||
func (t *tracker) add(gvr schema.GroupVersionResource, obj runtime.Object, ns string, replaceExisting bool) error {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
gr := gvr.GroupResource()
|
||||
|
||||
// To avoid the object from being accidentally modified by caller
|
||||
// after it's been added to the tracker, we always store the deep
|
||||
// copy.
|
||||
obj = obj.DeepCopyObject()
|
||||
|
||||
newMeta, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Propagate namespace to the new object if hasn't already been set.
|
||||
if len(newMeta.GetNamespace()) == 0 {
|
||||
newMeta.SetNamespace(ns)
|
||||
}
|
||||
|
||||
if ns != newMeta.GetNamespace() {
|
||||
msg := fmt.Sprintf("request namespace does not match object namespace, request: %q object: %q", ns, newMeta.GetNamespace())
|
||||
return errors.NewBadRequest(msg)
|
||||
}
|
||||
|
||||
for i, existingObj := range t.objects[gvr] {
|
||||
oldMeta, err := meta.Accessor(existingObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if oldMeta.GetNamespace() == newMeta.GetNamespace() && oldMeta.GetName() == newMeta.GetName() {
|
||||
if replaceExisting {
|
||||
for _, w := range t.getWatches(gvr, ns) {
|
||||
w.Modify(obj)
|
||||
}
|
||||
t.objects[gvr][i] = obj
|
||||
return nil
|
||||
}
|
||||
return errors.NewAlreadyExists(gr, newMeta.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
if replaceExisting {
|
||||
// Tried to update but no matching object was found.
|
||||
return errors.NewNotFound(gr, newMeta.GetName())
|
||||
}
|
||||
|
||||
t.objects[gvr] = append(t.objects[gvr], obj)
|
||||
|
||||
for _, w := range t.getWatches(gvr, ns) {
|
||||
w.Add(obj)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tracker) addList(obj runtime.Object, replaceExisting bool) error {
|
||||
list, err := meta.ExtractList(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errs := runtime.DecodeList(list, t.decoder)
|
||||
if len(errs) > 0 {
|
||||
return errs[0]
|
||||
}
|
||||
for _, obj := range list {
|
||||
if err := t.Add(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tracker) Delete(gvr schema.GroupVersionResource, ns, name string) error {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
found := false
|
||||
|
||||
for i, existingObj := range t.objects[gvr] {
|
||||
objMeta, err := meta.Accessor(existingObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if objMeta.GetNamespace() == ns && objMeta.GetName() == name {
|
||||
obj := t.objects[gvr][i]
|
||||
t.objects[gvr] = append(t.objects[gvr][:i], t.objects[gvr][i+1:]...)
|
||||
for _, w := range t.getWatches(gvr, ns) {
|
||||
w.Delete(obj)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewNotFound(gvr.GroupResource(), name)
|
||||
}
|
||||
|
||||
// filterByNamespace returns all objects in the collection that
|
||||
// match provided namespace. Empty namespace matches
|
||||
// non-namespaced objects.
|
||||
func filterByNamespace(objs []runtime.Object, ns string) ([]runtime.Object, error) {
|
||||
var res []runtime.Object
|
||||
|
||||
for _, obj := range objs {
|
||||
acc, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ns != "" && acc.GetNamespace() != ns {
|
||||
continue
|
||||
}
|
||||
res = append(res, obj)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DefaultWatchReactor(watchInterface watch.Interface, err error) WatchReactionFunc {
|
||||
return func(action Action) (bool, watch.Interface, error) {
|
||||
return true, watchInterface, err
|
||||
}
|
||||
}
|
||||
|
||||
// SimpleReactor is a Reactor. Each reaction function is attached to a given verb,resource tuple. "*" in either field matches everything for that value.
|
||||
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions
|
||||
type SimpleReactor struct {
|
||||
Verb string
|
||||
Resource string
|
||||
|
||||
Reaction ReactionFunc
|
||||
}
|
||||
|
||||
func (r *SimpleReactor) Handles(action Action) bool {
|
||||
verbCovers := r.Verb == "*" || r.Verb == action.GetVerb()
|
||||
if !verbCovers {
|
||||
return false
|
||||
}
|
||||
resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource
|
||||
if !resourceCovers {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *SimpleReactor) React(action Action) (bool, runtime.Object, error) {
|
||||
return r.Reaction(action)
|
||||
}
|
||||
|
||||
// SimpleWatchReactor is a WatchReactor. Each reaction function is attached to a given resource. "*" matches everything for that value.
|
||||
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions
|
||||
type SimpleWatchReactor struct {
|
||||
Resource string
|
||||
|
||||
Reaction WatchReactionFunc
|
||||
}
|
||||
|
||||
func (r *SimpleWatchReactor) Handles(action Action) bool {
|
||||
resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource
|
||||
if !resourceCovers {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *SimpleWatchReactor) React(action Action) (bool, watch.Interface, error) {
|
||||
return r.Reaction(action)
|
||||
}
|
||||
|
||||
// SimpleProxyReactor is a ProxyReactor. Each reaction function is attached to a given resource. "*" matches everything for that value.
|
||||
// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions.
|
||||
type SimpleProxyReactor struct {
|
||||
Resource string
|
||||
|
||||
Reaction ProxyReactionFunc
|
||||
}
|
||||
|
||||
func (r *SimpleProxyReactor) Handles(action Action) bool {
|
||||
resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource
|
||||
if !resourceCovers {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *SimpleProxyReactor) React(action Action) (bool, restclient.ResponseWrapper, error) {
|
||||
return r.Reaction(action)
|
||||
}
|
|
@ -356,6 +356,7 @@ k8s.io/apimachinery/pkg/util/json
|
|||
k8s.io/apimachinery/pkg/util/mergepatch
|
||||
k8s.io/apimachinery/pkg/util/naming
|
||||
k8s.io/apimachinery/pkg/util/net
|
||||
k8s.io/apimachinery/pkg/util/rand
|
||||
k8s.io/apimachinery/pkg/util/remotecommand
|
||||
k8s.io/apimachinery/pkg/util/runtime
|
||||
k8s.io/apimachinery/pkg/util/sets
|
||||
|
@ -424,6 +425,7 @@ k8s.io/client-go/plugin/pkg/client/auth/gcp
|
|||
k8s.io/client-go/rest
|
||||
k8s.io/client-go/rest/watch
|
||||
k8s.io/client-go/restmapper
|
||||
k8s.io/client-go/testing
|
||||
k8s.io/client-go/third_party/forked/golang/template
|
||||
k8s.io/client-go/tools/auth
|
||||
k8s.io/client-go/tools/cache
|
||||
|
@ -479,6 +481,7 @@ sigs.k8s.io/controller-runtime/pkg/cache/internal
|
|||
sigs.k8s.io/controller-runtime/pkg/client
|
||||
sigs.k8s.io/controller-runtime/pkg/client/apiutil
|
||||
sigs.k8s.io/controller-runtime/pkg/client/config
|
||||
sigs.k8s.io/controller-runtime/pkg/client/fake
|
||||
sigs.k8s.io/controller-runtime/pkg/controller
|
||||
sigs.k8s.io/controller-runtime/pkg/controller/controllerutil
|
||||
sigs.k8s.io/controller-runtime/pkg/conversion
|
||||
|
@ -490,6 +493,7 @@ sigs.k8s.io/controller-runtime/pkg/healthz
|
|||
sigs.k8s.io/controller-runtime/pkg/internal/controller
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/log
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/objectutil
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/recorder
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/testing/integration
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/testing/integration/addr
|
||||
|
|
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
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 fake
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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/runtime/schema"
|
||||
utilrand "k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/testing"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
|
||||
)
|
||||
|
||||
type versionedTracker struct {
|
||||
testing.ObjectTracker
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
tracker versionedTracker
|
||||
scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
var _ client.Client = &fakeClient{}
|
||||
|
||||
const (
|
||||
maxNameLength = 63
|
||||
randomLength = 5
|
||||
maxGeneratedNameLength = maxNameLength - randomLength
|
||||
)
|
||||
|
||||
// NewFakeClient creates a new fake client for testing.
|
||||
// You can choose to initialize it with a slice of runtime.Object.
|
||||
// Deprecated: use NewFakeClientWithScheme. You should always be
|
||||
// passing an explicit Scheme.
|
||||
func NewFakeClient(initObjs ...runtime.Object) client.Client {
|
||||
return NewFakeClientWithScheme(scheme.Scheme, initObjs...)
|
||||
}
|
||||
|
||||
// NewFakeClientWithScheme creates a new fake client with the given scheme
|
||||
// for testing.
|
||||
// You can choose to initialize it with a slice of runtime.Object.
|
||||
func NewFakeClientWithScheme(clientScheme *runtime.Scheme, initObjs ...runtime.Object) client.Client {
|
||||
tracker := testing.NewObjectTracker(clientScheme, scheme.Codecs.UniversalDecoder())
|
||||
for _, obj := range initObjs {
|
||||
err := tracker.Add(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err))
|
||||
}
|
||||
}
|
||||
return &fakeClient{
|
||||
tracker: versionedTracker{tracker},
|
||||
scheme: clientScheme,
|
||||
}
|
||||
}
|
||||
|
||||
func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if accessor.GetName() == "" {
|
||||
return apierrors.NewInvalid(
|
||||
obj.GetObjectKind().GroupVersionKind().GroupKind(),
|
||||
accessor.GetName(),
|
||||
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
|
||||
}
|
||||
if accessor.GetResourceVersion() != "" {
|
||||
return apierrors.NewBadRequest("resourceVersion can not be set for Create requests")
|
||||
}
|
||||
accessor.SetResourceVersion("1")
|
||||
return t.ObjectTracker.Create(gvr, obj, ns)
|
||||
}
|
||||
|
||||
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get accessor for object: %v", err)
|
||||
}
|
||||
if accessor.GetName() == "" {
|
||||
return apierrors.NewInvalid(
|
||||
obj.GetObjectKind().GroupVersionKind().GroupKind(),
|
||||
accessor.GetName(),
|
||||
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
|
||||
}
|
||||
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldAccessor, err := meta.Accessor(oldObject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
|
||||
return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
|
||||
}
|
||||
if oldAccessor.GetResourceVersion() == "" {
|
||||
oldAccessor.SetResourceVersion("0")
|
||||
}
|
||||
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can not convert resourceVersion %q to int: %v", oldAccessor.GetResourceVersion(), err)
|
||||
}
|
||||
intResourceVersion++
|
||||
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
|
||||
return t.ObjectTracker.Update(gvr, obj, ns)
|
||||
}
|
||||
|
||||
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error {
|
||||
gvr, err := getGVRFromObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o, err := c.tracker.Get(gvr, key.Namespace, key.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *fakeClient) List(ctx context.Context, obj runtime.Object, opts ...client.ListOption) error {
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
OriginalKind := gvk.Kind
|
||||
|
||||
if !strings.HasSuffix(gvk.Kind, "List") {
|
||||
return fmt.Errorf("non-list type %T (kind %q) passed as output", obj, gvk)
|
||||
}
|
||||
// we need the non-list GVK, so chop off the "List" from the end of the kind
|
||||
gvk.Kind = gvk.Kind[:len(gvk.Kind)-4]
|
||||
|
||||
listOpts := client.ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
|
||||
o, err := c.tracker.List(gvr, gvk, listOpts.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(OriginalKind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if listOpts.LabelSelector != nil {
|
||||
objs, err := meta.ExtractList(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filteredObjs, err := objectutil.FilterWithLabels(objs, listOpts.LabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = meta.SetList(obj, filteredObjs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) Create(ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error {
|
||||
createOptions := &client.CreateOptions{}
|
||||
createOptions.ApplyOptions(opts)
|
||||
|
||||
for _, dryRunOpt := range createOptions.DryRun {
|
||||
if dryRunOpt == metav1.DryRunAll {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
gvr, err := getGVRFromObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if accessor.GetName() == "" && accessor.GetGenerateName() != "" {
|
||||
base := accessor.GetGenerateName()
|
||||
if len(base) > maxGeneratedNameLength {
|
||||
base = base[:maxGeneratedNameLength]
|
||||
}
|
||||
accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength)))
|
||||
}
|
||||
|
||||
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
|
||||
}
|
||||
|
||||
func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error {
|
||||
gvr, err := getGVRFromObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delOptions := client.DeleteOptions{}
|
||||
delOptions.ApplyOptions(opts)
|
||||
|
||||
//TODO: implement propagation
|
||||
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
|
||||
}
|
||||
|
||||
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error {
|
||||
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dcOptions := client.DeleteAllOfOptions{}
|
||||
dcOptions.ApplyOptions(opts)
|
||||
|
||||
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
|
||||
o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objs, err := meta.ExtractList(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.LabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, o := range filteredObjs {
|
||||
accessor, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
|
||||
updateOptions := &client.UpdateOptions{}
|
||||
updateOptions.ApplyOptions(opts)
|
||||
|
||||
for _, dryRunOpt := range updateOptions.DryRun {
|
||||
if dryRunOpt == metav1.DryRunAll {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
gvr, err := getGVRFromObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
|
||||
}
|
||||
|
||||
func (c *fakeClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
patchOptions := &client.PatchOptions{}
|
||||
patchOptions.ApplyOptions(opts)
|
||||
|
||||
for _, dryRunOpt := range patchOptions.DryRun {
|
||||
if dryRunOpt == metav1.DryRunAll {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
gvr, err := getGVRFromObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := patch.Data(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reaction := testing.ObjectReaction(c.tracker)
|
||||
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !handled {
|
||||
panic("tracker could not handle patch method")
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ta.SetKind(gvk.Kind)
|
||||
ta.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
j, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *fakeClient) Status() client.StatusWriter {
|
||||
return &fakeStatusWriter{client: c}
|
||||
}
|
||||
|
||||
func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) {
|
||||
gvk, err := apiutil.GVKForObject(obj, scheme)
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, err
|
||||
}
|
||||
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
|
||||
return gvr, nil
|
||||
}
|
||||
|
||||
type fakeStatusWriter struct {
|
||||
client *fakeClient
|
||||
}
|
||||
|
||||
func (sw *fakeStatusWriter) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
|
||||
// TODO(droot): This results in full update of the obj (spec + status). Need
|
||||
// a way to update status field only.
|
||||
return sw.client.Update(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
func (sw *fakeStatusWriter) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
// TODO(droot): This results in full update of the obj (spec + status). Need
|
||||
// a way to update status field only.
|
||||
return sw.client.Patch(ctx, obj, patch, opts...)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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 fake provides a fake client for testing.
|
||||
|
||||
Deprecated: please use pkg/envtest for testing. This package will be dropped
|
||||
before the v1.0.0 release.
|
||||
|
||||
An fake client is backed by its simple object store indexed by GroupVersionResource.
|
||||
You can create a fake client with optional objects.
|
||||
|
||||
client := NewFakeClient(initObjs...) // initObjs is a slice of runtime.Object
|
||||
|
||||
You can invoke the methods defined in the Client interface.
|
||||
|
||||
When it doubt, it's almost always better not to use this package and instead use
|
||||
envtest.Environment with a real client and API server.
|
||||
*/
|
||||
package fake
|
42
vendor/sigs.k8s.io/controller-runtime/pkg/internal/objectutil/filter.go
generated
vendored
Normal file
42
vendor/sigs.k8s.io/controller-runtime/pkg/internal/objectutil/filter.go
generated
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 objectutil
|
||||
|
||||
import (
|
||||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// FilterWithLabels returns a copy of the items in objs matching labelSel
|
||||
func FilterWithLabels(objs []runtime.Object, labelSel labels.Selector) ([]runtime.Object, error) {
|
||||
outItems := make([]runtime.Object, 0, len(objs))
|
||||
for _, obj := range objs {
|
||||
meta, err := apimeta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if labelSel != nil {
|
||||
lbls := labels.Set(meta.GetLabels())
|
||||
if !labelSel.Matches(lbls) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
outItems = append(outItems, obj.DeepCopyObject())
|
||||
}
|
||||
return outItems, nil
|
||||
}
|
Loading…
Reference in New Issue