-
Adam Reese authoredUnverified7423eddf
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest/fake"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/apis/core"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions/resource"
"k8s.io/kubernetes/pkg/kubectl/scheme"
)
var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer
func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
}
func newPod(name string) core.Pod {
return newPodWithStatus(name, core.PodStatus{}, "")
}
func newPodWithStatus(name string, status core.PodStatus, namespace string) core.Pod {
ns := core.NamespaceDefault
if namespace != "" {
ns = namespace
}
return core.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
SelfLink: "/api/v1/namespaces/default/pods/" + name,
},
Spec: core.PodSpec{
Containers: []core.Container{{
Name: "app:v4",
Image: "abc/app:v4",
Ports: []core.ContainerPort{{Name: "http", ContainerPort: 80}},
}},
},
Status: status,
}
}
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
func newPodList(names ...string) core.PodList {
var list core.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 := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Default.Codec(), obj))))
return &http.Response{StatusCode: code, Header: header, Body: body}, nil
}
type testClient struct {
*Client
*cmdtesting.TestFactory
}
func newTestClient() *testClient {
tf := cmdtesting.NewTestFactory()
c := &Client{Factory: tf, Log: nopLogger}
return &testClient{Client: c, TestFactory: tf}
}
func TestUpdate(t *testing.T) {
listA := newPodList("starfish", "otter", "squid")
listB := newPodList("starfish", "otter", "dolphin")
listC := newPodList("starfish", "otter", "dolphin")
listB.Items[0].Spec.Containers[0].Ports = []core.ContainerPort{{Name: "https", ContainerPort: 443}}
listC.Items[0].Spec.Containers[0].Ports = []core.ContainerPort{{Name: "https", ContainerPort: 443}}
var actions []string
tf := cmdtesting.NewTestFactory()
defer tf.Cleanup()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
actions = append(actions, p+":"+m)
t.Logf("got request %s %s", p, m)
switch {
case p == "/namespaces/default/pods/starfish" && m == "GET":
return newResponse(200, &listA.Items[0])
case p == "/namespaces/default/pods/otter" && m == "GET":
return newResponse(200, &listA.Items[1])
case p == "/namespaces/default/pods/dolphin" && m == "GET":
return newResponse(404, notFoundBody())
case p == "/namespaces/default/pods/starfish" && m == "PATCH":
data, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatalf("could not dump request: %s", err)
}
req.Body.Close()
expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
if string(data) != expected {
t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
}
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
return newResponse(200, &listB.Items[0])
case p == "/namespaces/default/pods" && m == "POST":
return newResponse(200, &listB.Items[1])
case p == "/namespaces/default/pods/squid" && m == "DELETE":
return newResponse(200, &listB.Items[1])
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
c := &Client{
Factory: tf,
Log: nopLogger,
}
codec := legacyscheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
if err := c.Update(core.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil {
t.Fatal(err)
}
// TODO: Find a way to test methods that use Client Set
// Test with a wait
// if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil {
// t.Fatal(err)
// }
// Test with a wait should fail
// TODO: A way to make this not based off of an extremely short timeout?
// if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil {
// t.Fatal(err)
// }
expectedActions := []string{
"/namespaces/default/pods/starfish:GET",
"/namespaces/default/pods/starfish:PATCH",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/dolphin:GET",
"/namespaces/default/pods:POST",
"/namespaces/default/pods/squid:DELETE",
}
if len(expectedActions) != len(actions) {
t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
return
}
for k, v := range expectedActions {
if actions[k] != v {
t.Errorf("expected %s request got %s", v, actions[k])
}
}
}
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: "Invalid schema",
namespace: "test",
reader: strings.NewReader(testInvalidServiceManifest),
err: true,
},
}
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
c := newTestClient()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c.Cleanup()
// Test for an invalid manifest
infos, err := c.Build(tt.namespace, tt.reader)
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 TestGet(t *testing.T) {
list := newPodList("starfish", "otter")
c := newTestClient()
defer c.Cleanup()
c.TestFactory.UnstructuredClient = &fake.RESTClient{
GroupVersion: schema.GroupVersion{Version: "v1"},
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 == "GET":
return newResponse(404, notFoundBody())
case p == "/namespaces/default/pods/otter" && m == "GET":
return newResponse(200, &list.Items[1])
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
// Test Success
data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter")
o, err := c.Get("default", data)
if err != nil {
t.Errorf("Expected missing results, got %q", err)
}
if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") {
t.Errorf("Expected v1/Pod otter, got %s", o)
}
// Test failure
data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish")
o, err = c.Get("default", data)
if err != nil {
t.Errorf("Expected missing results, got %q", err)
}
if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") {
t.Errorf("Expected missing starfish, got %s", o)
}
}
func TestPerform(t *testing.T) {
tests := []struct {
name string
reader io.Reader
count int
err bool
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
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()
defer c.Cleanup()
infos, err := c.Build("default", tt.reader)
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 TestReal(t *testing.T) {
t.Skip("This is a live test, comment this line to run")
c := New(nil)
if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil {
t.Fatal(err)
}
testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
c = New(nil)
if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil {
t.Fatal(err)
}
if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil {
t.Fatal(err)
}
// ensures that delete does not fail if a resource is not found
if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil {
t.Fatal(err)
}
}
const testServiceManifest = `
kind: Service
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: myapp
ports:
- port: 80
protocol: TCP
targetPort: 9376
`
const testInvalidServiceManifest = `
kind: Service
apiVersion: v1
spec:
ports:
- port: "80"
`
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: k8s.gcr.io/redis:e2e # or just image: redis
resources:
requests:
cpu: 100m
memory: 100Mi
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-slave
labels:
app: redis
tier: backend
role: slave
spec:
ports:
# the port that this service should serve on
- port: 6379
selector:
app: redis
tier: backend
role: slave
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: redis-slave
spec:
replicas: 2
template:
metadata:
labels:
app: redis
role: slave
tier: backend
spec:
containers:
- name: slave
image: gcr.io/google_samples/gb-redisslave: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
491492493494495496497498499500501502503504505506
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
`