github.com/caicloud/helm@v2.5.0+incompatible/pkg/kube/client_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kube
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"k8s.io/apimachinery/pkg/api/meta"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	"k8s.io/client-go/dynamic"
    35  	"k8s.io/client-go/rest/fake"
    36  	"k8s.io/kubernetes/pkg/api"
    37  	"k8s.io/kubernetes/pkg/api/testapi"
    38  	"k8s.io/kubernetes/pkg/api/validation"
    39  	"k8s.io/kubernetes/pkg/kubectl"
    40  	cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
    41  	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
    42  	"k8s.io/kubernetes/pkg/kubectl/resource"
    43  	"k8s.io/kubernetes/pkg/printers"
    44  	watchjson "k8s.io/kubernetes/pkg/watch/json"
    45  )
    46  
    47  func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
    48  	return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
    49  }
    50  
    51  func newPod(name string) api.Pod {
    52  	return newPodWithStatus(name, api.PodStatus{}, "")
    53  }
    54  
    55  func newPodWithStatus(name string, status api.PodStatus, namespace string) api.Pod {
    56  	ns := api.NamespaceDefault
    57  	if namespace != "" {
    58  		ns = namespace
    59  	}
    60  	return api.Pod{
    61  		ObjectMeta: metav1.ObjectMeta{
    62  			Name:      name,
    63  			Namespace: ns,
    64  			SelfLink:  "/api/v1/namespaces/default/pods/" + name,
    65  		},
    66  		Spec: api.PodSpec{
    67  			Containers: []api.Container{{
    68  				Name:  "app:v4",
    69  				Image: "abc/app:v4",
    70  				Ports: []api.ContainerPort{{Name: "http", ContainerPort: 80}},
    71  			}},
    72  		},
    73  		Status: status,
    74  	}
    75  }
    76  
    77  func newPodList(names ...string) api.PodList {
    78  	var list api.PodList
    79  	for _, name := range names {
    80  		list.Items = append(list.Items, newPod(name))
    81  	}
    82  	return list
    83  }
    84  
    85  func notFoundBody() *metav1.Status {
    86  	return &metav1.Status{
    87  		Code:    http.StatusNotFound,
    88  		Status:  metav1.StatusFailure,
    89  		Reason:  metav1.StatusReasonNotFound,
    90  		Message: " \"\" not found",
    91  		Details: &metav1.StatusDetails{},
    92  	}
    93  }
    94  
    95  func newResponse(code int, obj runtime.Object) (*http.Response, error) {
    96  	header := http.Header{}
    97  	header.Set("Content-Type", runtime.ContentTypeJSON)
    98  	body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Default.Codec(), obj))))
    99  	return &http.Response{StatusCode: code, Header: header, Body: body}, nil
   100  }
   101  
   102  type fakeReaper struct {
   103  	name string
   104  }
   105  
   106  func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *metav1.DeleteOptions) error {
   107  	r.name = name
   108  	return nil
   109  }
   110  
   111  type fakeReaperFactory struct {
   112  	cmdutil.Factory
   113  	reaper kubectl.Reaper
   114  }
   115  
   116  func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) {
   117  	return f.reaper, nil
   118  }
   119  
   120  func newEventResponse(code int, e *watch.Event) (*http.Response, error) {
   121  	dispatchedEvent, err := encodeAndMarshalEvent(e)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	header := http.Header{}
   127  	header.Set("Content-Type", runtime.ContentTypeJSON)
   128  	body := ioutil.NopCloser(bytes.NewReader(dispatchedEvent))
   129  	return &http.Response{StatusCode: code, Header: header, Body: body}, nil
   130  }
   131  
   132  func encodeAndMarshalEvent(e *watch.Event) ([]byte, error) {
   133  	encodedEvent, err := watchjson.Object(testapi.Default.Codec(), e)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	return json.Marshal(encodedEvent)
   139  }
   140  
   141  func newTestClient(f cmdutil.Factory) *Client {
   142  	c := New(nil)
   143  	c.Factory = f
   144  	return c
   145  }
   146  
   147  func TestUpdate(t *testing.T) {
   148  	listA := newPodList("starfish", "otter", "squid")
   149  	listB := newPodList("starfish", "otter", "dolphin")
   150  	listC := newPodList("starfish", "otter", "dolphin")
   151  	listB.Items[0].Spec.Containers[0].Ports = []api.ContainerPort{{Name: "https", ContainerPort: 443}}
   152  	listC.Items[0].Spec.Containers[0].Ports = []api.ContainerPort{{Name: "https", ContainerPort: 443}}
   153  
   154  	var actions []string
   155  
   156  	f, tf, codec, _ := cmdtesting.NewAPIFactory()
   157  	tf.UnstructuredClient = &fake.RESTClient{
   158  		APIRegistry:          api.Registry,
   159  		NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer,
   160  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   161  			p, m := req.URL.Path, req.Method
   162  			actions = append(actions, p+":"+m)
   163  			t.Logf("got request %s %s", p, m)
   164  			switch {
   165  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   166  				return newResponse(200, &listA.Items[0])
   167  			case p == "/namespaces/default/pods/otter" && m == "GET":
   168  				return newResponse(200, &listA.Items[1])
   169  			case p == "/namespaces/default/pods/squid" && m == "GET":
   170  				return newResponse(200, &listA.Items[2])
   171  			case p == "/namespaces/default/pods/dolphin" && m == "GET":
   172  				return newResponse(404, notFoundBody())
   173  			case p == "/namespaces/default/pods/starfish" && m == "PATCH":
   174  				data, err := ioutil.ReadAll(req.Body)
   175  				if err != nil {
   176  					t.Fatalf("could not dump request: %s", err)
   177  				}
   178  				req.Body.Close()
   179  				expected := `{"spec":{"containers":[{"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
   180  				if string(data) != expected {
   181  					t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
   182  				}
   183  				return newResponse(200, &listB.Items[0])
   184  			case p == "/namespaces/default/pods" && m == "POST":
   185  				return newResponse(200, &listB.Items[1])
   186  			case p == "/namespaces/default/pods/squid" && m == "DELETE":
   187  				return newResponse(200, &listB.Items[1])
   188  			default:
   189  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   190  				return nil, nil
   191  			}
   192  		}),
   193  	}
   194  
   195  	reaper := &fakeReaper{}
   196  	rf := &fakeReaperFactory{Factory: f, reaper: reaper}
   197  	c := newTestClient(rf)
   198  	if err := c.Update(api.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil {
   199  		t.Fatal(err)
   200  	}
   201  	// TODO: Find a way to test methods that use Client Set
   202  	// Test with a wait
   203  	// if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil {
   204  	// 	t.Fatal(err)
   205  	// }
   206  	// Test with a wait should fail
   207  	// TODO: A way to make this not based off of an extremely short timeout?
   208  	// if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil {
   209  	// 	t.Fatal(err)
   210  	// }
   211  	expectedActions := []string{
   212  		"/namespaces/default/pods/starfish:GET",
   213  		"/namespaces/default/pods/starfish:GET",
   214  		"/namespaces/default/pods/starfish:PATCH",
   215  		"/namespaces/default/pods/otter:GET",
   216  		"/namespaces/default/pods/otter:GET",
   217  		"/namespaces/default/pods/otter:GET",
   218  		"/namespaces/default/pods/dolphin:GET",
   219  		"/namespaces/default/pods:POST",
   220  		"/namespaces/default/pods/squid:GET",
   221  	}
   222  	if len(expectedActions) != len(actions) {
   223  		t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
   224  		return
   225  	}
   226  	for k, v := range expectedActions {
   227  		if actions[k] != v {
   228  			t.Errorf("expected %s request got %s", v, actions[k])
   229  		}
   230  	}
   231  
   232  	if reaper.name != "squid" {
   233  		t.Errorf("unexpected reaper: %#v", reaper)
   234  	}
   235  
   236  }
   237  
   238  func TestBuild(t *testing.T) {
   239  	tests := []struct {
   240  		name        string
   241  		namespace   string
   242  		reader      io.Reader
   243  		count       int
   244  		swaggerFile string
   245  		err         bool
   246  		errMessage  string
   247  	}{
   248  		{
   249  			name:      "Valid input",
   250  			namespace: "test",
   251  			reader:    strings.NewReader(guestbookManifest),
   252  			count:     6,
   253  		}, {
   254  			name:        "Invalid schema",
   255  			namespace:   "test",
   256  			reader:      strings.NewReader(testInvalidServiceManifest),
   257  			swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json",
   258  			err:         true,
   259  			errMessage:  `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`,
   260  		},
   261  	}
   262  
   263  	for _, tt := range tests {
   264  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   265  		c := newTestClient(f)
   266  		if tt.swaggerFile != "" {
   267  			data, err := ioutil.ReadFile(tt.swaggerFile)
   268  			if err != nil {
   269  				t.Fatalf("could not read swagger spec: %s", err)
   270  			}
   271  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   272  			if err != nil {
   273  				t.Fatalf("could not load swagger spec: %s", err)
   274  			}
   275  			tf.Validator = validator
   276  		}
   277  
   278  		// Test for an invalid manifest
   279  		infos, err := c.Build(tt.namespace, tt.reader)
   280  		if err != nil && err.Error() != tt.errMessage {
   281  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   282  		} else if err != nil && !tt.err {
   283  			t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err)
   284  		}
   285  
   286  		if len(infos) != tt.count {
   287  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos))
   288  		}
   289  	}
   290  }
   291  
   292  type testPrinter struct {
   293  	Objects []runtime.Object
   294  	Err     error
   295  	printers.ResourcePrinter
   296  }
   297  
   298  func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
   299  	t.Objects = append(t.Objects, obj)
   300  	fmt.Fprintf(out, "%#v", obj)
   301  	return t.Err
   302  }
   303  
   304  func (t *testPrinter) HandledResources() []string {
   305  	return []string{}
   306  }
   307  
   308  func (t *testPrinter) AfterPrint(io.Writer, string) error {
   309  	return t.Err
   310  }
   311  
   312  func TestGet(t *testing.T) {
   313  	list := newPodList("starfish", "otter")
   314  	f, tf, _, _ := cmdtesting.NewAPIFactory()
   315  	tf.Printer = &testPrinter{}
   316  	tf.UnstructuredClient = &fake.RESTClient{
   317  		APIRegistry:          api.Registry,
   318  		NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer,
   319  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   320  			p, m := req.URL.Path, req.Method
   321  			//actions = append(actions, p+":"+m)
   322  			t.Logf("got request %s %s", p, m)
   323  			switch {
   324  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   325  				return newResponse(404, notFoundBody())
   326  			case p == "/namespaces/default/pods/otter" && m == "GET":
   327  				return newResponse(200, &list.Items[1])
   328  			default:
   329  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   330  				return nil, nil
   331  			}
   332  		}),
   333  	}
   334  	c := newTestClient(f)
   335  
   336  	// Test Success
   337  	data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: otter")
   338  	o, err := c.Get("default", data)
   339  	if err != nil {
   340  		t.Errorf("Expected missing results, got %q", err)
   341  	}
   342  	if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") {
   343  		t.Errorf("Expected v1/Pod otter, got %s", o)
   344  	}
   345  
   346  	// Test failure
   347  	data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: starfish")
   348  	o, err = c.Get("default", data)
   349  	if err != nil {
   350  		t.Errorf("Expected missing results, got %q", err)
   351  	}
   352  	if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") {
   353  		t.Errorf("Expected missing starfish, got %s", o)
   354  	}
   355  }
   356  
   357  func TestPerform(t *testing.T) {
   358  	tests := []struct {
   359  		name        string
   360  		namespace   string
   361  		reader      io.Reader
   362  		count       int
   363  		swaggerFile string
   364  		err         bool
   365  		errMessage  string
   366  	}{
   367  		{
   368  			name:      "Valid input",
   369  			namespace: "test",
   370  			reader:    strings.NewReader(guestbookManifest),
   371  			count:     6,
   372  		}, {
   373  			name:       "Empty manifests",
   374  			namespace:  "test",
   375  			reader:     strings.NewReader(""),
   376  			err:        true,
   377  			errMessage: "no objects visited",
   378  		},
   379  	}
   380  
   381  	for _, tt := range tests {
   382  		results := []*resource.Info{}
   383  
   384  		fn := func(info *resource.Info) error {
   385  			results = append(results, info)
   386  
   387  			if info.Namespace != tt.namespace {
   388  				t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace)
   389  			}
   390  			return nil
   391  		}
   392  
   393  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   394  		c := newTestClient(f)
   395  		if tt.swaggerFile != "" {
   396  			data, err := ioutil.ReadFile(tt.swaggerFile)
   397  			if err != nil {
   398  				t.Fatalf("could not read swagger spec: %s", err)
   399  			}
   400  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   401  			if err != nil {
   402  				t.Fatalf("could not load swagger spec: %s", err)
   403  			}
   404  			tf.Validator = validator
   405  		}
   406  
   407  		infos, err := c.Build(tt.namespace, tt.reader)
   408  		if err != nil && err.Error() != tt.errMessage {
   409  			t.Errorf("%q. Error while building manifests: %v", tt.name, err)
   410  		}
   411  
   412  		err = perform(infos, fn)
   413  		if (err != nil) != tt.err {
   414  			t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
   415  		}
   416  		if err != nil && err.Error() != tt.errMessage {
   417  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   418  		}
   419  
   420  		if len(results) != tt.count {
   421  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results))
   422  		}
   423  	}
   424  }
   425  
   426  func TestWaitAndGetCompletedPodPhase(t *testing.T) {
   427  	tests := []struct {
   428  		podPhase      api.PodPhase
   429  		expectedPhase api.PodPhase
   430  		err           bool
   431  		errMessage    string
   432  	}{
   433  		{
   434  			podPhase:      api.PodPending,
   435  			expectedPhase: api.PodUnknown,
   436  			err:           true,
   437  			errMessage:    "watch closed before Until timeout",
   438  		}, {
   439  			podPhase:      api.PodRunning,
   440  			expectedPhase: api.PodUnknown,
   441  			err:           true,
   442  			errMessage:    "watch closed before Until timeout",
   443  		}, {
   444  			podPhase:      api.PodSucceeded,
   445  			expectedPhase: api.PodSucceeded,
   446  		}, {
   447  			podPhase:      api.PodFailed,
   448  			expectedPhase: api.PodFailed,
   449  		},
   450  	}
   451  
   452  	for _, tt := range tests {
   453  		f, tf, codec, ns := cmdtesting.NewAPIFactory()
   454  		actions := make(map[string]string)
   455  
   456  		var testPodList api.PodList
   457  		testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test"))
   458  
   459  		tf.Client = &fake.RESTClient{
   460  			APIRegistry:          api.Registry,
   461  			NegotiatedSerializer: ns,
   462  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   463  				p, m := req.URL.Path, req.Method
   464  				actions[p] = m
   465  				switch {
   466  				case p == "/namespaces/test/pods/bestpod" && m == "GET":
   467  					return newResponse(200, &testPodList.Items[0])
   468  				case p == "/namespaces/test/pods" && m == "GET":
   469  					event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]}
   470  					return newEventResponse(200, &event)
   471  				default:
   472  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   473  					return nil, nil
   474  				}
   475  			}),
   476  		}
   477  
   478  		c := newTestClient(f)
   479  
   480  		phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second)
   481  		if (err != nil) != tt.err {
   482  			t.Fatalf("Expected error but there was none.")
   483  		}
   484  		if err != nil && err.Error() != tt.errMessage {
   485  			t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error())
   486  		}
   487  		if phase != tt.expectedPhase {
   488  			t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase)
   489  		}
   490  	}
   491  }
   492  
   493  func TestReal(t *testing.T) {
   494  	t.Skip("This is a live test, comment this line to run")
   495  	c := New(nil)
   496  	if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil {
   497  		t.Fatal(err)
   498  	}
   499  
   500  	testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
   501  	c = New(nil)
   502  	if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil {
   503  		t.Fatal(err)
   504  	}
   505  
   506  	if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil {
   507  		t.Fatal(err)
   508  	}
   509  
   510  	// ensures that delete does not fail if a resource is not found
   511  	if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil {
   512  		t.Fatal(err)
   513  	}
   514  }
   515  
   516  const testServiceManifest = `
   517  kind: Service
   518  apiVersion: v1
   519  metadata:
   520    name: my-service
   521  spec:
   522    selector:
   523      app: myapp
   524    ports:
   525      - port: 80
   526        protocol: TCP
   527        targetPort: 9376
   528  `
   529  
   530  const testInvalidServiceManifest = `
   531  kind: Service
   532  apiVersion: v1
   533  spec:
   534    ports:
   535      - port: "80"
   536  `
   537  
   538  const testEndpointManifest = `
   539  kind: Endpoints
   540  apiVersion: v1
   541  metadata:
   542    name: my-service
   543  subsets:
   544    - addresses:
   545        - ip: "1.2.3.4"
   546      ports:
   547        - port: 9376
   548  `
   549  
   550  const guestbookManifest = `
   551  apiVersion: v1
   552  kind: Service
   553  metadata:
   554    name: redis-master
   555    labels:
   556      app: redis
   557      tier: backend
   558      role: master
   559  spec:
   560    ports:
   561    - port: 6379
   562      targetPort: 6379
   563    selector:
   564      app: redis
   565      tier: backend
   566      role: master
   567  ---
   568  apiVersion: extensions/v1beta1
   569  kind: Deployment
   570  metadata:
   571    name: redis-master
   572  spec:
   573    replicas: 1
   574    template:
   575      metadata:
   576        labels:
   577          app: redis
   578          role: master
   579          tier: backend
   580      spec:
   581        containers:
   582        - name: master
   583          image: gcr.io/google_containers/redis:e2e  # or just image: redis
   584          resources:
   585            requests:
   586              cpu: 100m
   587              memory: 100Mi
   588          ports:
   589          - containerPort: 6379
   590  ---
   591  apiVersion: v1
   592  kind: Service
   593  metadata:
   594    name: redis-slave
   595    labels:
   596      app: redis
   597      tier: backend
   598      role: slave
   599  spec:
   600    ports:
   601      # the port that this service should serve on
   602    - port: 6379
   603    selector:
   604      app: redis
   605      tier: backend
   606      role: slave
   607  ---
   608  apiVersion: extensions/v1beta1
   609  kind: Deployment
   610  metadata:
   611    name: redis-slave
   612  spec:
   613    replicas: 2
   614    template:
   615      metadata:
   616        labels:
   617          app: redis
   618          role: slave
   619          tier: backend
   620      spec:
   621        containers:
   622        - name: slave
   623          image: gcr.io/google_samples/gb-redisslave:v1
   624          resources:
   625            requests:
   626              cpu: 100m
   627              memory: 100Mi
   628          env:
   629          - name: GET_HOSTS_FROM
   630            value: dns
   631          ports:
   632          - containerPort: 6379
   633  ---
   634  apiVersion: v1
   635  kind: Service
   636  metadata:
   637    name: frontend
   638    labels:
   639      app: guestbook
   640      tier: frontend
   641  spec:
   642    ports:
   643    - port: 80
   644    selector:
   645      app: guestbook
   646      tier: frontend
   647  ---
   648  apiVersion: extensions/v1beta1
   649  kind: Deployment
   650  metadata:
   651    name: frontend
   652  spec:
   653    replicas: 3
   654    template:
   655      metadata:
   656        labels:
   657          app: guestbook
   658          tier: frontend
   659      spec:
   660        containers:
   661        - name: php-redis
   662          image: gcr.io/google-samples/gb-frontend:v4
   663          resources:
   664            requests:
   665              cpu: 100m
   666              memory: 100Mi
   667          env:
   668          - name: GET_HOSTS_FROM
   669            value: dns
   670          ports:
   671          - containerPort: 80
   672  `