You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/kube/client_test.go

1752 lines
51 KiB

/*
Copyright The Helm 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 kube
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes"
k8sfake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
)
var (
unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer
codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
)
func objBody(obj runtime.Object) io.ReadCloser {
return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
}
func newPod(name string) v1.Pod {
return newPodWithStatus(name, v1.PodStatus{}, "")
}
func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod {
ns := v1.NamespaceDefault
if namespace != "" {
ns = namespace
}
return v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
SelfLink: "/api/v1/namespaces/default/pods/" + name,
},
Spec: v1.PodSpec{
Containers: []v1.Container{{
Name: "app:v4",
Image: "abc/app:v4",
Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}},
}},
},
Status: status,
}
}
func newPodList(names ...string) v1.PodList {
var list v1.PodList
for _, name := range names {
list.Items = append(list.Items, newPod(name))
}
return list
}
func notFoundBody() *metav1.Status {
return &metav1.Status{
Code: http.StatusNotFound,
Status: metav1.StatusFailure,
Reason: metav1.StatusReasonNotFound,
Message: " \"\" not found",
Details: &metav1.StatusDetails{},
}
}
func newResponse(code int, obj runtime.Object) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", runtime.ContentTypeJSON)
body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
return &http.Response{StatusCode: code, Header: header, Body: body}, nil
}
func newResponseJSON(code int, json []byte) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", runtime.ContentTypeJSON)
body := io.NopCloser(bytes.NewReader(json))
return &http.Response{StatusCode: code, Header: header, Body: body}, nil
}
func newTestClient(t *testing.T) *Client {
t.Helper()
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
return &Client{
Factory: testFactory.WithNamespace(v1.NamespaceDefault),
}
}
type RequestResponseAction struct {
Request http.Request
Response http.Response
Error error
}
type RoundTripperTestFunc func(previous []RequestResponseAction, req *http.Request) (*http.Response, error)
func NewRequestResponseLogClient(t *testing.T, cb RoundTripperTestFunc) RequestResponseLogClient {
t.Helper()
return RequestResponseLogClient{
t: t,
cb: cb,
}
}
// RequestResponseLogClient is a test client that logs requests and responses
// Satifying http.RoundTripper interface, it can be used to mock HTTP requests in tests.
// Forwarding requests to a callback function (cb) that can be used to simulate server responses.
type RequestResponseLogClient struct {
t *testing.T
cb RoundTripperTestFunc
actionsLock sync.Mutex
Actions []RequestResponseAction
}
func (r *RequestResponseLogClient) Do(req *http.Request) (*http.Response, error) {
t := r.t
t.Helper()
readBodyBytes := func(body io.ReadCloser) []byte {
if body == nil {
return []byte{}
}
defer body.Close()
bodyBytes, err := io.ReadAll(body)
require.NoError(t, err)
return bodyBytes
}
reqBytes := readBodyBytes(req.Body)
t.Logf("Request: %s %s %s", req.Method, req.URL.String(), reqBytes)
if req.Body != nil {
req.Body = io.NopCloser(bytes.NewReader(reqBytes))
}
resp, err := r.cb(r.Actions, req)
respBytes := readBodyBytes(resp.Body)
t.Logf("Response: %d %s", resp.StatusCode, string(respBytes))
if resp.Body != nil {
resp.Body = io.NopCloser(bytes.NewReader(respBytes))
}
r.actionsLock.Lock()
defer r.actionsLock.Unlock()
r.Actions = append(r.Actions, RequestResponseAction{
Request: *req,
Response: *resp,
Error: err,
})
return resp, err
}
func TestCreate(t *testing.T) {
// Note: c.Create with the fake client can currently only test creation of a single pod/object in the same list. When testing
// with more than one pod, c.Create will run into a data race as it calls perform->batchPerform which performs creation
// in batches. The race is something in the fake client itself in `func (c *RESTClient) do(...)`
// when it stores the req: c.Req = req and cannot (?) be fixed easily.
type testCase struct {
Name string
Pods v1.PodList
Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error)
ServerSideApply bool
ExpectedActions []string
ExpectedErrorContains string
}
testCases := map[string]testCase{
"Create success (client-side apply)": {
Pods: newPodList("starfish"),
ServerSideApply: false,
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, _ *http.Request) (*http.Response, error) {
t.Helper()
if len(previous) < 2 { // simulate a conflict
return newResponseJSON(http.StatusConflict, resourceQuotaConflict)
}
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
ExpectedActions: []string{
"/namespaces/default/pods:POST",
"/namespaces/default/pods:POST",
"/namespaces/default/pods:POST",
},
},
"Create success (server-side apply)": {
Pods: newPodList("whale"),
ServerSideApply: true,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) {
t.Helper()
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
ExpectedActions: []string{
"/namespaces/default/pods/whale:PATCH",
},
},
"Create fail: incompatible server (server-side apply)": {
Pods: newPodList("lobster"),
ServerSideApply: true,
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
return &http.Response{
StatusCode: http.StatusUnsupportedMediaType,
Request: req,
}, nil
},
ExpectedErrorContains: "server-side apply not available on the server:",
ExpectedActions: []string{
"/namespaces/default/pods/lobster:PATCH",
},
},
"Create fail: quota (server-side apply)": {
Pods: newPodList("dolphin"),
ServerSideApply: true,
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) {
t.Helper()
return newResponseJSON(http.StatusConflict, resourceQuotaConflict)
},
ExpectedErrorContains: "Operation cannot be fulfilled on resourcequotas \"quota\": the object has been modified; " +
"please apply your changes to the latest version and try again",
ExpectedActions: []string{
"/namespaces/default/pods/dolphin:PATCH",
},
},
}
c := newTestClient(t)
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
return tc.Callback(t, tc, previous, req)
})
c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(client.Do),
}
list, err := c.Build(objBody(&tc.Pods), false)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
result, err := c.Create(
list,
ClientCreateOptionServerSideApply(tc.ServerSideApply, false))
if tc.ExpectedErrorContains != "" {
require.ErrorContains(t, err, tc.ExpectedErrorContains)
} else {
require.NoError(t, err)
// See note above about limitations in supporting more than a single object
assert.Len(t, result.Created, 1, "expected 1 object created, got %d", len(result.Created))
}
actions := []string{}
for _, action := range client.Actions {
path, method := action.Request.URL.Path, action.Request.Method
actions = append(actions, path+":"+method)
}
assert.Equal(t, tc.ExpectedActions, actions)
})
}
}
func TestUpdate(t *testing.T) {
type testCase struct {
OriginalPods v1.PodList
TargetPods v1.PodList
ThreeWayMergeForUnstructured bool
ServerSideApply bool
ExpectedActions []string
}
expectedActionsClientSideApply := []string{
"/namespaces/default/pods/starfish:GET",
"/namespaces/default/pods/starfish:GET",
"/namespaces/default/pods/starfish:PATCH",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/dolphin:GET",
"/namespaces/default/pods:POST", // create dolphin
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE",
}
expectedActionsServerSideApply := []string{
"/namespaces/default/pods/starfish:GET",
"/namespaces/default/pods/starfish:PATCH",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/otter:PATCH",
"/namespaces/default/pods/dolphin:GET",
"/namespaces/default/pods:POST", // create dolphin
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE",
}
testCases := map[string]testCase{
"client-side apply": {
OriginalPods: newPodList("starfish", "otter", "squid"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return listTarget
}(),
ThreeWayMergeForUnstructured: false,
ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply,
},
"client-side apply (three-way merge for unstructured)": {
OriginalPods: newPodList("starfish", "otter", "squid"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return listTarget
}(),
ThreeWayMergeForUnstructured: true,
ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply,
},
"serverSideApply": {
OriginalPods: newPodList("starfish", "otter", "squid"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return listTarget
}(),
ThreeWayMergeForUnstructured: false,
ServerSideApply: true,
ExpectedActions: expectedActionsServerSideApply,
},
}
c := newTestClient(t)
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
listOriginal := tc.OriginalPods
listTarget := tc.TargetPods
iterationCounter := 0
cb := func(_ []RequestResponseAction, req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
switch {
case p == "/namespaces/default/pods/starfish" && m == http.MethodGet:
return newResponse(http.StatusOK, &listOriginal.Items[0])
case p == "/namespaces/default/pods/otter" && m == http.MethodGet:
return newResponse(http.StatusOK, &listOriginal.Items[1])
case p == "/namespaces/default/pods/otter" && m == http.MethodPatch:
if !tc.ServerSideApply {
defer req.Body.Close()
data, err := io.ReadAll(req.Body)
require.NoError(t, err)
assert.Equal(t, `{}`, string(data))
}
return newResponse(http.StatusOK, &listTarget.Items[0])
case p == "/namespaces/default/pods/dolphin" && m == http.MethodGet:
return newResponse(http.StatusNotFound, notFoundBody())
case p == "/namespaces/default/pods/starfish" && m == http.MethodPatch:
if !tc.ServerSideApply {
// Ensure client-side apply specifies correct patch
defer req.Body.Close()
data, err := io.ReadAll(req.Body)
require.NoError(t, err)
expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
assert.Equal(t, expected, string(data))
}
return newResponse(http.StatusOK, &listTarget.Items[0])
case p == "/namespaces/default/pods" && m == http.MethodPost:
if iterationCounter < 2 {
iterationCounter++
return newResponseJSON(http.StatusConflict, resourceQuotaConflict)
}
return newResponse(http.StatusOK, &listTarget.Items[1])
case p == "/namespaces/default/pods/squid" && m == http.MethodDelete:
return newResponse(http.StatusOK, &listTarget.Items[1])
case p == "/namespaces/default/pods/squid" && m == http.MethodGet:
return newResponse(http.StatusOK, &listTarget.Items[2])
default:
}
t.Fail()
return nil, nil
}
client := NewRequestResponseLogClient(t, cb)
c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(client.Do),
}
first, err := c.Build(objBody(&listOriginal), false)
require.NoError(t, err)
second, err := c.Build(objBody(&listTarget), false)
require.NoError(t, err)
result, err := c.Update(
first,
second,
ClientUpdateOptionThreeWayMergeForUnstructured(tc.ThreeWayMergeForUnstructured),
ClientUpdateOptionForceReplace(false),
ClientUpdateOptionServerSideApply(tc.ServerSideApply, false))
require.NoError(t, err)
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated))
assert.Len(t, result.Deleted, 1, "expected 1 resource deleted, got %d", len(result.Deleted))
actions := []string{}
for _, action := range client.Actions {
path, method := action.Request.URL.Path, action.Request.Method
actions = append(actions, path+":"+method)
}
assert.Equal(t, tc.ExpectedActions, actions)
})
}
}
func TestBuild(t *testing.T) {
tests := []struct {
name string
namespace string
reader io.Reader
count int
err bool
}{
{
name: "Valid input",
namespace: "test",
reader: strings.NewReader(guestbookManifest),
count: 6,
}, {
name: "Valid input, deploying resources into different namespaces",
namespace: "test",
reader: strings.NewReader(namespacedGuestbookManifest),
count: 1,
},
}
c := newTestClient(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test for an invalid manifest
infos, err := c.Build(tt.reader, false)
if err != nil && !tt.err {
t.Errorf("Got error message when no error should have occurred: %v", err)
} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
t.Error("error message was not scrubbed")
}
if len(infos) != tt.count {
t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
}
})
}
}
func TestBuildTable(t *testing.T) {
tests := []struct {
name string
namespace string
reader io.Reader
count int
err bool
}{
{
name: "Valid input",
namespace: "test",
reader: strings.NewReader(guestbookManifest),
count: 6,
}, {
name: "Valid input, deploying resources into different namespaces",
namespace: "test",
reader: strings.NewReader(namespacedGuestbookManifest),
count: 1,
},
}
c := newTestClient(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test for an invalid manifest
infos, err := c.BuildTable(tt.reader, false)
if err != nil && !tt.err {
t.Errorf("Got error message when no error should have occurred: %v", err)
} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
t.Error("error message was not scrubbed")
}
if len(infos) != tt.count {
t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
}
})
}
}
func TestPerform(t *testing.T) {
tests := []struct {
name string
reader io.Reader
count int
err bool
errMessage string
}{
{
name: "Valid input",
reader: strings.NewReader(guestbookManifest),
count: 6,
}, {
name: "Empty manifests",
reader: strings.NewReader(""),
err: true,
errMessage: "no objects visited",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := []*resource.Info{}
fn := func(info *resource.Info) error {
results = append(results, info)
return nil
}
c := newTestClient(t)
infos, err := c.Build(tt.reader, false)
if err != nil && err.Error() != tt.errMessage {
t.Errorf("Error while building manifests: %v", err)
}
err = perform(infos, fn)
if (err != nil) != tt.err {
t.Errorf("expected error: %v, got %v", tt.err, err)
}
if err != nil && err.Error() != tt.errMessage {
t.Errorf("expected error message: %v, got %v", tt.errMessage, err)
}
if len(results) != tt.count {
t.Errorf("expected %d result objects, got %d", tt.count, len(results))
}
})
}
}
func TestWait(t *testing.T) {
podList := newPodList("starfish", "otter", "squid")
var created *time.Time
c := newTestClient(t)
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
switch {
case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet:
pod := &podList.Items[0]
if created != nil && time.Since(*created) >= time.Second*5 {
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
},
}
}
return newResponse(http.StatusOK, pod)
case p == "/api/v1/namespaces/default/pods/otter" && m == http.MethodGet:
pod := &podList.Items[1]
if created != nil && time.Since(*created) >= time.Second*5 {
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
},
}
}
return newResponse(http.StatusOK, pod)
case p == "/api/v1/namespaces/default/pods/squid" && m == http.MethodGet:
pod := &podList.Items[2]
if created != nil && time.Since(*created) >= time.Second*5 {
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
},
}
}
return newResponse(http.StatusOK, pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
now := time.Now()
created = &now
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
if err != nil {
t.Fatal(err)
}
resources, err := c.Build(objBody(&podList), false)
if err != nil {
t.Fatal(err)
}
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
if err != nil {
t.Fatal(err)
}
if len(result.Created) != 3 {
t.Errorf("expected 3 resource created, got %d", len(result.Created))
}
if err := c.Wait(resources, time.Second*30); err != nil {
t.Errorf("expected wait without error, got %s", err)
}
if time.Since(*created) < time.Second*5 {
t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*created))
}
}
func TestWaitJob(t *testing.T) {
job := newJob("starfish", 0, intToInt32(1), 0, 0)
var created *time.Time
c := newTestClient(t)
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
switch {
case p == "/apis/batch/v1/namespaces/default/jobs/starfish" && m == http.MethodGet:
if created != nil && time.Since(*created) >= time.Second*5 {
job.Status.Succeeded = 1
}
return newResponse(http.StatusOK, job)
case p == "/namespaces/default/jobs" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
now := time.Now()
created = &now
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
if err != nil {
t.Fatal(err)
}
resources, err := c.Build(objBody(job), false)
if err != nil {
t.Fatal(err)
}
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
if err != nil {
t.Fatal(err)
}
if len(result.Created) != 1 {
t.Errorf("expected 1 resource created, got %d", len(result.Created))
}
if err := c.WaitWithJobs(resources, time.Second*30); err != nil {
t.Errorf("expected wait without error, got %s", err)
}
if time.Since(*created) < time.Second*5 {
t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*created))
}
}
func TestWaitDelete(t *testing.T) {
pod := newPod("starfish")
var deleted *time.Time
c := newTestClient(t)
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
switch {
case p == "/namespaces/default/pods/starfish" && m == http.MethodGet:
if deleted != nil && time.Since(*deleted) >= time.Second*5 {
return newResponse(http.StatusNotFound, notFoundBody())
}
return newResponse(http.StatusOK, &pod)
case p == "/namespaces/default/pods/starfish" && m == http.MethodDelete:
now := time.Now()
deleted = &now
return newResponse(http.StatusOK, &pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
if err != nil {
t.Fatal(err)
}
resources, err := c.Build(objBody(&pod), false)
if err != nil {
t.Fatal(err)
}
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
if err != nil {
t.Fatal(err)
}
if len(result.Created) != 1 {
t.Errorf("expected 1 resource created, got %d", len(result.Created))
}
if _, err := c.Delete(resources); err != nil {
t.Fatal(err)
}
if err := c.WaitForDelete(resources, time.Second*30); err != nil {
t.Errorf("expected wait without error, got %s", err)
}
if time.Since(*deleted) < time.Second*5 {
t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*deleted))
}
}
func TestReal(t *testing.T) {
t.Skip("This is a live test, comment this line to run")
c := New(nil)
resources, err := c.Build(strings.NewReader(guestbookManifest), false)
if err != nil {
t.Fatal(err)
}
if _, err := c.Create(resources); err != nil {
t.Fatal(err)
}
testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
c = New(nil)
resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
if err != nil {
t.Fatal(err)
}
if _, err := c.Create(resources); err != nil {
t.Fatal(err)
}
resources, err = c.Build(strings.NewReader(testEndpointManifest), false)
if err != nil {
t.Fatal(err)
}
if _, errs := c.Delete(resources); errs != nil {
t.Fatal(errs)
}
resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
if err != nil {
t.Fatal(err)
}
// ensures that delete does not fail if a resource is not found
if _, errs := c.Delete(resources); errs != nil {
t.Fatal(errs)
}
}
func TestGetPodList(t *testing.T) {
namespace := "some-namespace"
names := []string{"dave", "jimmy"}
var responsePodList v1.PodList
for _, name := range names {
responsePodList.Items = append(responsePodList.Items, newPodWithStatus(name, v1.PodStatus{}, namespace))
}
kubeClient := k8sfake.NewSimpleClientset(&responsePodList)
c := Client{Namespace: namespace, kubeClient: kubeClient}
podList, err := c.GetPodList(namespace, metav1.ListOptions{})
clientAssertions := assert.New(t)
clientAssertions.NoError(err)
clientAssertions.Equal(&responsePodList, podList)
}
func TestOutputContainerLogsForPodList(t *testing.T) {
namespace := "some-namespace"
somePodList := newPodList("jimmy", "three", "structs")
kubeClient := k8sfake.NewSimpleClientset(&somePodList)
c := Client{Namespace: namespace, kubeClient: kubeClient}
outBuffer := &bytes.Buffer{}
outBufferFunc := func(_, _, _ string) io.Writer { return outBuffer }
err := c.OutputContainerLogsForPodList(&somePodList, namespace, outBufferFunc)
clientAssertions := assert.New(t)
clientAssertions.NoError(err)
clientAssertions.Equal("fake logsfake logsfake logs", outBuffer.String())
}
const testServiceManifest = `
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: myapp
ports:
- port: 80
protocol: TCP
targetPort: 9376
`
const testEndpointManifest = `
kind: Endpoints
apiVersion: v1
metadata:
name: my-service
subsets:
- addresses:
- ip: "1.2.3.4"
ports:
- port: 9376
`
const guestbookManifest = `
apiVersion: v1
kind: Service
metadata:
name: redis-master
labels:
app: redis
tier: backend
role: master
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
tier: backend
role: master
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: redis-master
spec:
replicas: 1
template:
metadata:
labels:
app: redis
role: master
tier: backend
spec:
containers:
- name: master
image: registry.k8s.io/redis:e2e # or just image: redis
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-replica
labels:
app: redis
tier: backend
role: replica
spec:
ports:
# the port that this service should serve on
- port: 6379
selector:
app: redis
tier: backend
role: replica
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: redis-replica
spec:
replicas: 2
template:
metadata:
labels:
app: redis
role: replica
tier: backend
spec:
containers:
- name: replica
image: gcr.io/google_samples/gb-redisreplica:v1
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
ports:
- port: 80
selector:
app: guestbook
tier: frontend
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 3
template:
metadata:
labels:
app: guestbook
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google-samples/gb-frontend:v4
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
`
const namespacedGuestbookManifest = `
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
namespace: guestbook
spec:
replicas: 3
template:
metadata:
labels:
app: guestbook
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google-samples/gb-frontend:v4
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
`
var resourceQuotaConflict = []byte(`
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Operation cannot be fulfilled on resourcequotas \"quota\": the object has been modified; please apply your changes to the latest version and try again","reason":"Conflict","details":{"name":"quota","kind":"resourcequotas"},"code":409}`)
type createPatchTestCase struct {
name string
// The target state.
target *unstructured.Unstructured
// The state as it exists in the release.
original *unstructured.Unstructured
// The actual state as it exists in the cluster.
actual *unstructured.Unstructured
threeWayMergeForUnstructured bool
// The patch is supposed to transfer the current state to the target state,
// thereby preserving the actual state, wherever possible.
expectedPatch string
expectedPatchType types.PatchType
}
func (c createPatchTestCase) run(t *testing.T) {
scheme := runtime.NewScheme()
v1.AddToScheme(scheme)
encoder := jsonserializer.NewSerializerWithOptions(
jsonserializer.DefaultMetaFactory, scheme, scheme, jsonserializer.SerializerOptions{
Yaml: false, Pretty: false, Strict: true,
},
)
objBody := func(obj runtime.Object) io.ReadCloser {
return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, obj))))
}
header := make(http.Header)
header.Set("Content-Type", runtime.ContentTypeJSON)
restClient := &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Resp: &http.Response{
StatusCode: http.StatusOK,
Body: objBody(c.actual),
Header: header,
},
}
targetInfo := &resource.Info{
Client: restClient,
Namespace: "default",
Name: "test-obj",
Object: c.target,
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{
Group: "crd.com",
Version: "v1",
Resource: "datas",
},
Scope: meta.RESTScopeNamespace,
},
}
patch, patchType, err := createPatch(c.original, targetInfo, c.threeWayMergeForUnstructured)
if err != nil {
t.Fatalf("Failed to create patch: %v", err)
}
if c.expectedPatch != string(patch) {
t.Errorf("Unexpected patch.\nTarget:\n%s\nOriginal:\n%s\nActual:\n%s\n\nExpected:\n%s\nGot:\n%s",
c.target,
c.original,
c.actual,
c.expectedPatch,
string(patch),
)
}
if patchType != types.MergePatchType {
t.Errorf("Expected patch type %s, got %s", types.MergePatchType, patchType)
}
}
func newTestCustomResourceData(metadata map[string]string, spec map[string]interface{}) *unstructured.Unstructured {
if metadata == nil {
metadata = make(map[string]string)
}
if _, ok := metadata["name"]; !ok {
metadata["name"] = "test-obj"
}
if _, ok := metadata["namespace"]; !ok {
metadata["namespace"] = "default"
}
o := map[string]interface{}{
"apiVersion": "crd.com/v1",
"kind": "Data",
"metadata": metadata,
}
if len(spec) > 0 {
o["spec"] = spec
}
return &unstructured.Unstructured{
Object: o,
}
}
func TestCreatePatchCustomResourceMetadata(t *testing.T) {
target := newTestCustomResourceData(map[string]string{
"meta.helm.sh/release-name": "foo-simple",
"meta.helm.sh/release-namespace": "default",
"objectset.rio.cattle.io/id": "default-foo-simple",
}, nil)
testCase := createPatchTestCase{
name: "take ownership of resource",
target: target,
original: target,
actual: newTestCustomResourceData(nil, map[string]interface{}{
"color": "red",
}),
threeWayMergeForUnstructured: true,
expectedPatch: `{"metadata":{"meta.helm.sh/release-name":"foo-simple","meta.helm.sh/release-namespace":"default","objectset.rio.cattle.io/id":"default-foo-simple"}}`,
expectedPatchType: types.MergePatchType,
}
t.Run(testCase.name, testCase.run)
// Previous behavior.
testCase.threeWayMergeForUnstructured = false
testCase.expectedPatch = `{}`
t.Run(testCase.name, testCase.run)
}
func TestCreatePatchCustomResourceSpec(t *testing.T) {
target := newTestCustomResourceData(nil, map[string]interface{}{
"color": "red",
"size": "large",
})
testCase := createPatchTestCase{
name: "merge with spec of existing custom resource",
target: target,
original: target,
actual: newTestCustomResourceData(nil, map[string]interface{}{
"color": "red",
"weight": "heavy",
}),
threeWayMergeForUnstructured: true,
expectedPatch: `{"spec":{"size":"large"}}`,
expectedPatchType: types.MergePatchType,
}
t.Run(testCase.name, testCase.run)
// Previous behavior.
testCase.threeWayMergeForUnstructured = false
testCase.expectedPatch = `{}`
t.Run(testCase.name, testCase.run)
}
type errorFactory struct {
*cmdtesting.TestFactory
err error
}
func (f *errorFactory) KubernetesClientSet() (*kubernetes.Clientset, error) {
return nil, f.err
}
func newTestClientWithDiscoveryError(t *testing.T, err error) *Client {
t.Helper()
c := newTestClient(t)
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
if req.URL.Path == "/version" {
return nil, err
}
resp, respErr := newResponse(http.StatusOK, &v1.Pod{})
return resp, respErr
}),
}
return c
}
func TestIsReachable(t *testing.T) {
const (
expectedUnreachableMsg = "kubernetes cluster unreachable"
)
tests := []struct {
name string
setupClient func(*testing.T) *Client
expectError bool
errorContains string
}{
{
name: "successful reachability test",
setupClient: func(t *testing.T) *Client {
t.Helper()
client := newTestClient(t)
client.kubeClient = k8sfake.NewSimpleClientset()
return client
},
expectError: false,
},
{
name: "client creation error with ErrEmptyConfig",
setupClient: func(t *testing.T) *Client {
t.Helper()
client := newTestClient(t)
client.Factory = &errorFactory{err: genericclioptions.ErrEmptyConfig}
return client
},
expectError: true,
errorContains: expectedUnreachableMsg,
},
{
name: "client creation error with general error",
setupClient: func(t *testing.T) *Client {
t.Helper()
client := newTestClient(t)
client.Factory = &errorFactory{err: errors.New("connection refused")}
return client
},
expectError: true,
errorContains: "kubernetes cluster unreachable: connection refused",
},
{
name: "discovery error with cluster unreachable",
setupClient: func(t *testing.T) *Client {
t.Helper()
return newTestClientWithDiscoveryError(t, http.ErrServerClosed)
},
expectError: true,
errorContains: expectedUnreachableMsg,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := tt.setupClient(t)
err := client.IsReachable()
if tt.expectError {
if err == nil {
t.Error("expected error but got nil")
return
}
if !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("expected error message to contain '%s', got: %v", tt.errorContains, err)
}
} else {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
})
}
}
func TestIsIncompatibleServerError(t *testing.T) {
testCases := map[string]struct {
Err error
Want bool
}{
"Unsupported media type": {
Err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusUnsupportedMediaType}},
Want: true,
},
"Not found error": {
Err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}},
Want: false,
},
"Generic error": {
Err: fmt.Errorf("some generic error"),
Want: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
if got := isIncompatibleServerError(tc.Err); got != tc.Want {
t.Errorf("isIncompatibleServerError() = %v, want %v", got, tc.Want)
}
})
}
}
func TestReplaceResource(t *testing.T) {
type testCase struct {
Pods v1.PodList
Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error)
ExpectedErrorContains string
}
testCases := map[string]testCase{
"normal": {
Pods: newPodList("whale"),
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
switch len(previous) {
case 0:
assert.Equal(t, "GET", req.Method)
case 1:
assert.Equal(t, "PUT", req.Method)
}
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"conflict": {
Pods: newPodList("whale"),
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
return &http.Response{
StatusCode: http.StatusConflict,
Request: req,
}, nil
},
ExpectedErrorContains: "failed to replace object: the server reported a conflict",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
return tc.Callback(t, tc, previous, req)
})
testFactory.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(client.Do),
}
resourceList, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.Pods), nil)
require.NoError(t, err)
require.Len(t, resourceList, 1)
info := resourceList[0]
err = replaceResource(info, FieldValidationDirectiveStrict)
if tc.ExpectedErrorContains != "" {
require.ErrorContains(t, err, tc.ExpectedErrorContains)
} else {
require.NoError(t, err)
require.NotNil(t, info.Object)
}
})
}
}
func TestPatchResourceClientSide(t *testing.T) {
type testCase struct {
OriginalPods v1.PodList
TargetPods v1.PodList
ThreeWayMergeForUnstructured bool
Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error)
ExpectedErrorContains string
}
testCases := map[string]testCase{
"normal": {
OriginalPods: newPodList("whale"),
TargetPods: func() v1.PodList {
pods := newPodList("whale")
pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return pods
}(),
ThreeWayMergeForUnstructured: false,
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
switch len(previous) {
case 0:
assert.Equal(t, "GET", req.Method)
return newResponse(http.StatusOK, &tc.OriginalPods.Items[0])
case 1:
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/strategic-merge-patch+json", req.Header.Get("Content-Type"))
return newResponse(http.StatusOK, &tc.TargetPods.Items[0])
}
t.Fail()
return nil, nil
},
},
"three way merge for unstructured": {
OriginalPods: newPodList("whale"),
TargetPods: func() v1.PodList {
pods := newPodList("whale")
pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return pods
}(),
ThreeWayMergeForUnstructured: true,
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
switch len(previous) {
case 0:
assert.Equal(t, "GET", req.Method)
return newResponse(http.StatusOK, &tc.OriginalPods.Items[0])
case 1:
t.Logf("patcher: %+v", req.Header)
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/strategic-merge-patch+json", req.Header.Get("Content-Type"))
return newResponse(http.StatusOK, &tc.TargetPods.Items[0])
}
t.Fail()
return nil, nil
},
},
"conflict": {
OriginalPods: newPodList("whale"),
TargetPods: func() v1.PodList {
pods := newPodList("whale")
pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return pods
}(),
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
switch len(previous) {
case 0:
assert.Equal(t, "GET", req.Method)
return newResponse(http.StatusOK, &tc.OriginalPods.Items[0])
case 1:
assert.Equal(t, "PATCH", req.Method)
return &http.Response{
StatusCode: http.StatusConflict,
Request: req,
}, nil
}
t.Fail()
return nil, nil
},
ExpectedErrorContains: "cannot patch \"whale\" with kind Pod: the server reported a conflict",
},
"no patch": {
OriginalPods: newPodList("whale"),
TargetPods: newPodList("whale"),
Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
switch len(previous) {
case 0:
assert.Equal(t, "GET", req.Method)
return newResponse(http.StatusOK, &tc.OriginalPods.Items[0])
case 1:
assert.Equal(t, "GET", req.Method)
return newResponse(http.StatusOK, &tc.TargetPods.Items[0])
}
t.Fail()
return nil, nil // newResponse(http.StatusOK, &tc.TargetPods.Items[0])
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
return tc.Callback(t, tc, previous, req)
})
testFactory.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(client.Do),
}
resourceListOriginal, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.OriginalPods), nil)
require.NoError(t, err)
require.Len(t, resourceListOriginal, 1)
resourceListTarget, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.TargetPods), nil)
require.NoError(t, err)
require.Len(t, resourceListTarget, 1)
original := resourceListOriginal[0]
target := resourceListTarget[0]
err = patchResourceClientSide(original.Object, target, tc.ThreeWayMergeForUnstructured)
if tc.ExpectedErrorContains != "" {
require.ErrorContains(t, err, tc.ExpectedErrorContains)
} else {
require.NoError(t, err)
require.NotNil(t, target.Object)
}
})
}
}
func TestPatchResourceServerSide(t *testing.T) {
type testCase struct {
Pods v1.PodList
DryRun bool
ForceConflicts bool
FieldValidationDirective FieldValidationDirective
Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error)
ExpectedErrorContains string
}
testCases := map[string]testCase{
"normal": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type"))
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
assert.Equal(t, "false", req.URL.Query().Get("force"))
assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation"))
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"dry run": {
Pods: newPodList("whale"),
DryRun: true,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type"))
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
assert.Equal(t, "All", req.URL.Query().Get("dryRun"))
assert.Equal(t, "false", req.URL.Query().Get("force"))
assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation"))
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"force conflicts": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: true,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type"))
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
assert.Equal(t, "true", req.URL.Query().Get("force"))
assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation"))
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"dry run + force conflicts": {
Pods: newPodList("whale"),
DryRun: true,
ForceConflicts: true,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type"))
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
assert.Equal(t, "All", req.URL.Query().Get("dryRun"))
assert.Equal(t, "true", req.URL.Query().Get("force"))
assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation"))
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"field validation ignore": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveIgnore,
Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
assert.Equal(t, "PATCH", req.Method)
assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type"))
assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path)
assert.Equal(t, "false", req.URL.Query().Get("force"))
assert.Equal(t, "Ignore", req.URL.Query().Get("fieldValidation"))
return newResponse(http.StatusOK, &tc.Pods.Items[0])
},
},
"incompatible server": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
return &http.Response{
StatusCode: http.StatusUnsupportedMediaType,
Request: req,
}, nil
},
ExpectedErrorContains: "server-side apply not available on the server:",
},
"conflict": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) {
t.Helper()
return &http.Response{
StatusCode: http.StatusConflict,
Request: req,
}, nil
},
ExpectedErrorContains: "the server reported a conflict",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) {
return tc.Callback(t, tc, previous, req)
})
testFactory.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(client.Do),
}
resourceList, err := buildResourceList(testFactory, v1.NamespaceDefault, tc.FieldValidationDirective, objBody(&tc.Pods), nil)
require.NoError(t, err)
require.Len(t, resourceList, 1)
info := resourceList[0]
err = patchResourceServerSide(info, tc.DryRun, tc.ForceConflicts, tc.FieldValidationDirective)
if tc.ExpectedErrorContains != "" {
require.ErrorContains(t, err, tc.ExpectedErrorContains)
} else {
require.NoError(t, err)
require.NotNil(t, info.Object)
}
})
}
}
func TestDetermineFieldValidationDirective(t *testing.T) {
assert.Equal(t, FieldValidationDirectiveIgnore, determineFieldValidationDirective(false))
assert.Equal(t, FieldValidationDirectiveStrict, determineFieldValidationDirective(true))
}