mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-07-16 23:11:52 -04:00
1195 lines
30 KiB
Go
1195 lines
30 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
schedulingv1alpha3 "k8s.io/api/scheduling/v1alpha3"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/net"
|
|
clientsetfake "k8s.io/client-go/kubernetes/fake"
|
|
clienttesting "k8s.io/client-go/testing"
|
|
"k8s.io/client-go/util/retry"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/klog/v2/ktesting"
|
|
extenderv1 "k8s.io/kube-scheduler/extender/v1"
|
|
schedulingapi "k8s.io/kubernetes/pkg/apis/scheduling"
|
|
st "k8s.io/kubernetes/pkg/scheduler/testing"
|
|
)
|
|
|
|
func TestGetPodFullName(t *testing.T) {
|
|
pod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "test",
|
|
Name: "pod",
|
|
},
|
|
}
|
|
got := GetPodFullName(pod)
|
|
expected := fmt.Sprintf("%s_%s", pod.Name, pod.Namespace)
|
|
if got != expected {
|
|
t.Errorf("Got wrong full name, got: %s, expected: %s", got, expected)
|
|
}
|
|
}
|
|
|
|
func newPriorityPodWithStartTime(name string, priority int32, startTime time.Time) *v1.Pod {
|
|
return &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Spec: v1.PodSpec{
|
|
Priority: &priority,
|
|
},
|
|
Status: v1.PodStatus{
|
|
StartTime: &metav1.Time{Time: startTime},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGetEarliestPodStartTime(t *testing.T) {
|
|
var priority int32 = 1
|
|
currentTime := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
pods []*v1.Pod
|
|
expectedStartTime *metav1.Time
|
|
}{
|
|
{
|
|
name: "Pods length is 0",
|
|
pods: []*v1.Pod{},
|
|
expectedStartTime: nil,
|
|
},
|
|
{
|
|
name: "generate new startTime",
|
|
pods: []*v1.Pod{
|
|
newPriorityPodWithStartTime("pod1", 1, currentTime.Add(-time.Second)),
|
|
{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod2",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
Priority: &priority,
|
|
},
|
|
},
|
|
},
|
|
expectedStartTime: &metav1.Time{Time: currentTime.Add(-time.Second)},
|
|
},
|
|
{
|
|
name: "Pod with earliest start time last in the list",
|
|
pods: []*v1.Pod{
|
|
newPriorityPodWithStartTime("pod1", 1, currentTime.Add(time.Second)),
|
|
newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)),
|
|
newPriorityPodWithStartTime("pod3", 2, currentTime),
|
|
},
|
|
expectedStartTime: &metav1.Time{Time: currentTime},
|
|
},
|
|
{
|
|
name: "Pod with earliest start time first in the list",
|
|
pods: []*v1.Pod{
|
|
newPriorityPodWithStartTime("pod1", 2, currentTime),
|
|
newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)),
|
|
newPriorityPodWithStartTime("pod3", 2, currentTime.Add(2*time.Second)),
|
|
},
|
|
expectedStartTime: &metav1.Time{Time: currentTime},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
startTime := GetEarliestPodStartTime(&extenderv1.Victims{Pods: test.pods})
|
|
if !startTime.Equal(test.expectedStartTime) {
|
|
t.Errorf("startTime is not the expected result,got %v, expected %v", startTime, test.expectedStartTime)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMoreImportantPod(t *testing.T) {
|
|
currentTime := time.Now()
|
|
pod1 := newPriorityPodWithStartTime("pod1", 1, currentTime)
|
|
pod2 := newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second))
|
|
pod3 := newPriorityPodWithStartTime("pod3", 2, currentTime)
|
|
|
|
tests := map[string]struct {
|
|
p1 *v1.Pod
|
|
p2 *v1.Pod
|
|
expected bool
|
|
}{
|
|
"Pod with higher priority": {
|
|
p1: pod1,
|
|
p2: pod2,
|
|
expected: false,
|
|
},
|
|
"Pod with older created time": {
|
|
p1: pod2,
|
|
p2: pod3,
|
|
expected: false,
|
|
},
|
|
"Pods with same start time": {
|
|
p1: pod3,
|
|
p2: pod1,
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for k, v := range tests {
|
|
t.Run(k, func(t *testing.T) {
|
|
got := MoreImportantPod(v.p1, v.p2)
|
|
if got != v.expected {
|
|
t.Errorf("expected %t but got %t", v.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchPodStatus(t *testing.T) {
|
|
conflictCheckingClient := func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount > 0 {
|
|
return true, nil, errors.New("request should not be retried")
|
|
}
|
|
patch := action.(clienttesting.PatchAction)
|
|
var patchPod v1.Pod
|
|
err := json.Unmarshal(patch.GetPatch(), &patchPod)
|
|
if err != nil {
|
|
return true, nil, err
|
|
}
|
|
existing, err := client.Tracker().Get(patch.GetResource(), patch.GetNamespace(), patch.GetName())
|
|
if err != nil {
|
|
return true, nil, err
|
|
}
|
|
if existing.(metav1.Object).GetResourceVersion() != patchPod.ResourceVersion {
|
|
return true, nil, apierrors.NewConflict(patch.GetResource().GroupResource(), patch.GetName(), fmt.Errorf("object is out of date"))
|
|
}
|
|
return false, nil, nil
|
|
})
|
|
|
|
return client
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
pod v1.Pod
|
|
client *clientsetfake.Clientset
|
|
// validateErr checks if error returned from PatchPodStatus is expected one or not.
|
|
// (true means error is expected one.)
|
|
validateErr func(goterr error) bool
|
|
statusToUpdate v1.PodStatus
|
|
resourceVersion string
|
|
nilOldStatus bool
|
|
}{
|
|
{
|
|
name: "Should update pod conditions successfully",
|
|
client: clientsetfake.NewClientset(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
|
|
},
|
|
},
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// ref: #101697, #94626 - ImagePullSecrets are allowed to have empty secret names
|
|
// which would fail the 2-way merge patch generation on Pod patches
|
|
// due to the mergeKey being the name field
|
|
name: "Should update pod conditions successfully on a pod Spec with secrets with empty name",
|
|
client: clientsetfake.NewClientset(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
// this will serialize to imagePullSecrets:[{}]
|
|
ImagePullSecrets: make([]v1.LocalObjectReference, 1),
|
|
},
|
|
},
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "retry patch request when an 'connection refused' error is returned",
|
|
client: func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount == 0 {
|
|
// return an connection refused error for the first patch request.
|
|
return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
|
|
}
|
|
if reqcount == 1 {
|
|
// not return error for the second patch request.
|
|
return false, &v1.Pod{}, nil
|
|
}
|
|
|
|
// return error if requests comes in more than three times.
|
|
return true, nil, errors.New("requests comes in more than three times.")
|
|
})
|
|
|
|
return client
|
|
}(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
|
|
},
|
|
},
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "only 4 retries at most",
|
|
client: func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount >= 4 {
|
|
// return error if requests comes in more than four times.
|
|
return true, nil, errors.New("requests comes in more than four times.")
|
|
}
|
|
|
|
// return an connection refused error for the first patch request.
|
|
return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
|
|
})
|
|
|
|
return client
|
|
}(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
},
|
|
Spec: v1.PodSpec{
|
|
ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
|
|
},
|
|
},
|
|
validateErr: net.IsConnectionRefused,
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil oldStatus patches successfully",
|
|
client: clientsetfake.NewClientset(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
},
|
|
},
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
nilOldStatus: true,
|
|
},
|
|
{
|
|
name: "up to date resource version succeeds",
|
|
client: conflictCheckingClient(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
ResourceVersion: "2",
|
|
},
|
|
},
|
|
resourceVersion: "2",
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "updated resource version produces conflict",
|
|
client: conflictCheckingClient(),
|
|
pod: v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pod1",
|
|
ResourceVersion: "2",
|
|
},
|
|
},
|
|
resourceVersion: "1",
|
|
statusToUpdate: v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
},
|
|
},
|
|
},
|
|
validateErr: apierrors.IsConflict,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
client := tc.client
|
|
_, err := client.CoreV1().Pods(tc.pod.Namespace).Create(ctx, &tc.pod, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldStatus := &tc.pod.Status
|
|
if tc.nilOldStatus {
|
|
oldStatus = nil
|
|
}
|
|
err = PatchPodStatus(ctx, client, tc.pod.Name, tc.pod.Namespace, tc.resourceVersion, oldStatus, &tc.statusToUpdate)
|
|
if err != nil && tc.validateErr == nil {
|
|
// shouldn't be error
|
|
t.Fatal(err)
|
|
}
|
|
if tc.validateErr != nil {
|
|
if !tc.validateErr(err) {
|
|
t.Fatalf("Returned unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
retrievedPod, err := client.CoreV1().Pods(tc.pod.Namespace).Get(ctx, tc.pod.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if diff := cmp.Diff(tc.statusToUpdate, retrievedPod.Status); diff != "" {
|
|
t.Errorf("unexpected pod status (-want,+got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchPodGroupStatus(t *testing.T) {
|
|
now := metav1.NewTime(time.Now().Truncate(time.Second))
|
|
|
|
tests := []struct {
|
|
name string
|
|
podGroup schedulingv1alpha3.PodGroup
|
|
client *clientsetfake.Clientset
|
|
// validateErr checks if error returned from PatchPodGroupStatus is expected one or not.
|
|
// (true means error is expected one.)
|
|
validateErr func(goterr error) bool
|
|
statusToUpdate *schedulingv1alpha3.PodGroupStatus
|
|
nilOldStatus bool
|
|
}{
|
|
{
|
|
name: "Should update podgroup conditions successfully",
|
|
client: clientsetfake.NewClientset(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity for the gang",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no-op when status is unchanged",
|
|
client: clientsetfake.NewClientset(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
Status: schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil newStatus returns nil",
|
|
client: clientsetfake.NewClientset(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
statusToUpdate: nil,
|
|
},
|
|
{
|
|
name: "retry patch request when a 'connection refused' error is returned",
|
|
client: func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "podgroups", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount == 0 {
|
|
return true, &schedulingv1alpha3.PodGroup{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
|
|
}
|
|
if reqcount == 1 {
|
|
return false, &schedulingv1alpha3.PodGroup{}, nil
|
|
}
|
|
return true, nil, errors.New("requests comes in more than three times.")
|
|
})
|
|
|
|
return client
|
|
}(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity for the gang",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "only 4 retries at most",
|
|
client: func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "podgroups", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount >= 4 {
|
|
return true, nil, errors.New("requests comes in more than four times.")
|
|
}
|
|
return true, &schedulingv1alpha3.PodGroup{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
|
|
})
|
|
|
|
return client
|
|
}(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
validateErr: net.IsConnectionRefused,
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity for the gang",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "retry patch request when a conflict error is returned",
|
|
client: func() *clientsetfake.Clientset {
|
|
client := clientsetfake.NewClientset()
|
|
|
|
reqcount := 0
|
|
client.PrependReactor("patch", "podgroups", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
defer func() { reqcount++ }()
|
|
if reqcount == 0 {
|
|
return true, &schedulingv1alpha3.PodGroup{},
|
|
apierrors.NewConflict(schema.GroupResource{
|
|
Resource: "podgroups"}, "pg1",
|
|
errors.New("the object has been modified"))
|
|
}
|
|
if reqcount == 1 {
|
|
return false, &schedulingv1alpha3.PodGroup{}, nil
|
|
}
|
|
return true, nil, errors.New("requests comes in more than three times.")
|
|
})
|
|
|
|
return client
|
|
}(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity for the gang",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil oldStatus patches successfully",
|
|
client: clientsetfake.NewClientset(),
|
|
podGroup: schedulingv1alpha3.PodGroup{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "ns",
|
|
Name: "pg1",
|
|
},
|
|
},
|
|
statusToUpdate: &schedulingv1alpha3.PodGroupStatus{
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: schedulingapi.PodGroupInitiallyScheduled,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: schedulingapi.PodGroupReasonUnschedulable,
|
|
Message: "not enough capacity for the gang",
|
|
LastTransitionTime: now,
|
|
},
|
|
},
|
|
},
|
|
nilOldStatus: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
client := tc.client
|
|
_, err := client.SchedulingV1alpha3().PodGroups(tc.podGroup.Namespace).Create(ctx, &tc.podGroup, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldStatus := &tc.podGroup.Status
|
|
if tc.nilOldStatus {
|
|
oldStatus = nil
|
|
}
|
|
err = PatchPodGroupStatus(ctx, client, tc.podGroup.Name, tc.podGroup.Namespace, oldStatus, tc.statusToUpdate)
|
|
if err != nil && tc.validateErr == nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.validateErr != nil {
|
|
if !tc.validateErr(err) {
|
|
t.Fatalf("Returned unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
retrievedPG, err := client.SchedulingV1alpha3().PodGroups(tc.podGroup.Namespace).Get(ctx, tc.podGroup.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wantStatus := tc.podGroup.Status
|
|
if tc.statusToUpdate != nil {
|
|
wantStatus = *tc.statusToUpdate
|
|
}
|
|
if diff := cmp.Diff(wantStatus, retrievedPG.Status); diff != "" {
|
|
t.Errorf("unexpected podgroup status (-want,+got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBindPod(t *testing.T) {
|
|
binding := &v1.Binding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod",
|
|
Namespace: "ns",
|
|
},
|
|
Target: v1.ObjectReference{
|
|
Name: "node",
|
|
},
|
|
}
|
|
|
|
validateNoErr := func(err error) bool { return err == nil }
|
|
|
|
tests := []struct {
|
|
name string
|
|
errFunc func() error
|
|
failCalls int
|
|
expectedCalls int
|
|
validateErr func(error) bool
|
|
}{
|
|
{
|
|
name: "successful",
|
|
failCalls: 0,
|
|
expectedCalls: 1,
|
|
validateErr: validateNoErr,
|
|
},
|
|
{
|
|
name: "no retry on conflict",
|
|
errFunc: func() error {
|
|
return apierrors.NewConflict(v1.Resource("pods"), "pod", errors.New("conflict"))
|
|
},
|
|
failCalls: 1,
|
|
expectedCalls: 1,
|
|
validateErr: apierrors.IsConflict,
|
|
},
|
|
{
|
|
name: "retry on internal error and succeed",
|
|
errFunc: func() error {
|
|
return apierrors.NewInternalError(errors.New("internal"))
|
|
},
|
|
failCalls: 1,
|
|
expectedCalls: 2,
|
|
validateErr: validateNoErr,
|
|
},
|
|
{
|
|
name: "retry on service unavailable and succeed",
|
|
errFunc: func() error {
|
|
return apierrors.NewServiceUnavailable("service unavailable")
|
|
},
|
|
failCalls: 1,
|
|
expectedCalls: 2,
|
|
validateErr: validateNoErr,
|
|
},
|
|
{
|
|
name: "retry on connection refused and succeed",
|
|
errFunc: func() error {
|
|
return fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
|
|
},
|
|
failCalls: 1,
|
|
expectedCalls: 2,
|
|
validateErr: validateNoErr,
|
|
},
|
|
{
|
|
name: "no retry on not found",
|
|
errFunc: func() error {
|
|
return apierrors.NewNotFound(v1.Resource("pods"), "pod")
|
|
},
|
|
failCalls: 1,
|
|
expectedCalls: 1,
|
|
validateErr: apierrors.IsNotFound,
|
|
},
|
|
{
|
|
name: "persistent internal error fails after retries",
|
|
errFunc: func() error {
|
|
return apierrors.NewInternalError(errors.New("internal"))
|
|
},
|
|
failCalls: retry.DefaultBackoff.Steps,
|
|
expectedCalls: retry.DefaultBackoff.Steps,
|
|
validateErr: apierrors.IsInternalError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
client := clientsetfake.NewClientset()
|
|
calls := 0
|
|
client.PrependReactor("create", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
|
|
createAction := action.(clienttesting.CreateActionImpl)
|
|
if createAction.Subresource != "binding" {
|
|
return false, nil, nil
|
|
}
|
|
calls++
|
|
|
|
if calls <= tc.failCalls {
|
|
return true, nil, tc.errFunc()
|
|
}
|
|
|
|
return true, nil, nil
|
|
})
|
|
|
|
err := BindPod(ctx, client, binding)
|
|
|
|
if !tc.validateErr(err) {
|
|
t.Errorf("BindPod() returned unexpected error: %v", err)
|
|
}
|
|
|
|
if calls != tc.expectedCalls {
|
|
t.Errorf("Expected %d calls to binding API, got %d", tc.expectedCalls, calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_As tests the As function with Pod.
|
|
func Test_As_Pod(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldObj interface{}
|
|
newObj interface{}
|
|
wantOldObj *v1.Pod
|
|
wantNewObj *v1.Pod
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "nil old Pod",
|
|
oldObj: nil,
|
|
newObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantOldObj: nil,
|
|
wantNewObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
},
|
|
{
|
|
name: "nil new Pod",
|
|
oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
newObj: nil,
|
|
wantOldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantNewObj: nil,
|
|
},
|
|
{
|
|
name: "two different kinds of objects",
|
|
oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotOld, gotNew, err := As[*v1.Pod](tc.oldObj, tc.newObj)
|
|
if err != nil && !tc.wantErr {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error, but got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" {
|
|
t.Errorf("unexpected old object (-want,+got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" {
|
|
t.Errorf("unexpected new object (-want,+got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_As_Node tests the As function with Node.
|
|
func Test_As_Node(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldObj interface{}
|
|
newObj interface{}
|
|
wantOldObj *v1.Node
|
|
wantNewObj *v1.Node
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "nil old Node",
|
|
oldObj: nil,
|
|
newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantOldObj: nil,
|
|
wantNewObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
},
|
|
{
|
|
name: "nil new Node",
|
|
oldObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
newObj: nil,
|
|
wantOldObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantNewObj: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotOld, gotNew, err := As[*v1.Node](tc.oldObj, tc.newObj)
|
|
if err != nil && !tc.wantErr {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error, but got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" {
|
|
t.Errorf("unexpected old object (-want,+got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" {
|
|
t.Errorf("unexpected new object (-want,+got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_As_KMetadata tests the As function with Pod.
|
|
func Test_As_KMetadata(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
oldObj interface{}
|
|
newObj interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "nil old Pod",
|
|
oldObj: nil,
|
|
newObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "nil new Pod",
|
|
oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
newObj: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "two different kinds of objects",
|
|
oldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
newObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unknown old type",
|
|
oldObj: "unknown type",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown new type",
|
|
newObj: "unknown type",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, _, err := As[klog.KMetadata](tc.oldObj, tc.newObj)
|
|
if err != nil && !tc.wantErr {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error, but got nil")
|
|
}
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetHostPorts(t *testing.T) {
|
|
tests := []struct {
|
|
pod *v1.Pod
|
|
want []v1.ContainerPort
|
|
}{
|
|
{
|
|
pod: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
Obj(),
|
|
want: nil,
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
InitContainerPort(true /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
Obj(),
|
|
want: nil,
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
InitContainerPort(false /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
Obj(),
|
|
want: nil,
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
Obj(),
|
|
want: []v1.ContainerPort{{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}},
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
InitContainerPort(true /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
Obj(),
|
|
want: []v1.ContainerPort{{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
}},
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8002,
|
|
HostPort: 8002,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8003,
|
|
HostPort: 8003,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8004,
|
|
HostPort: 8004,
|
|
Protocol: v1.ProtocolTCP,
|
|
}}).
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8005,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).Obj(),
|
|
want: []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8002,
|
|
HostPort: 8002,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8003,
|
|
HostPort: 8003,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8004,
|
|
HostPort: 8004,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
InitContainerPort(false /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 8001,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8002,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).
|
|
InitContainerPort(false /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8003,
|
|
HostPort: 8003,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).
|
|
InitContainerPort(true /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8004,
|
|
HostPort: 8004,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8005,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8006,
|
|
HostPort: 8006,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8007,
|
|
HostPort: 8007,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8008,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).Obj(),
|
|
want: []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8004,
|
|
HostPort: 8004,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8006,
|
|
HostPort: 8006,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8007,
|
|
HostPort: 8007,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
pod: st.MakePod().
|
|
InitContainerPort(true /* sidecar */, []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 9001,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).
|
|
ContainerPort([]v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8002,
|
|
HostPort: 9002,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
}).Obj(),
|
|
want: []v1.ContainerPort{
|
|
{
|
|
ContainerPort: 8001,
|
|
HostPort: 9001,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
{
|
|
ContainerPort: 8002,
|
|
HostPort: 9002,
|
|
Protocol: v1.ProtocolTCP,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
|
|
result := GetHostPorts(test.pod)
|
|
if diff := cmp.Diff(test.want, result); diff != "" {
|
|
t.Errorf("GetHostPorts() unexpected diff (-want,+got): %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|