github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+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/kubectl"
    39  	cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
    40  	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
    41  	"k8s.io/kubernetes/pkg/kubectl/resource"
    42  	"k8s.io/kubernetes/pkg/kubectl/validation"
    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/dolphin" && m == "GET":
   170  				return newResponse(404, notFoundBody())
   171  			case p == "/namespaces/default/pods/starfish" && m == "PATCH":
   172  				data, err := ioutil.ReadAll(req.Body)
   173  				if err != nil {
   174  					t.Fatalf("could not dump request: %s", err)
   175  				}
   176  				req.Body.Close()
   177  				expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
   178  				if string(data) != expected {
   179  					t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
   180  				}
   181  				return newResponse(200, &listB.Items[0])
   182  			case p == "/namespaces/default/pods" && m == "POST":
   183  				return newResponse(200, &listB.Items[1])
   184  			case p == "/namespaces/default/pods/squid" && m == "DELETE":
   185  				return newResponse(200, &listB.Items[1])
   186  			default:
   187  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   188  				return nil, nil
   189  			}
   190  		}),
   191  	}
   192  
   193  	reaper := &fakeReaper{}
   194  	rf := &fakeReaperFactory{Factory: f, reaper: reaper}
   195  	c := newTestClient(rf)
   196  	if err := c.Update(api.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil {
   197  		t.Fatal(err)
   198  	}
   199  	// TODO: Find a way to test methods that use Client Set
   200  	// Test with a wait
   201  	// if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil {
   202  	// 	t.Fatal(err)
   203  	// }
   204  	// Test with a wait should fail
   205  	// TODO: A way to make this not based off of an extremely short timeout?
   206  	// if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil {
   207  	// 	t.Fatal(err)
   208  	// }
   209  	expectedActions := []string{
   210  		"/namespaces/default/pods/starfish:GET",
   211  		"/namespaces/default/pods/starfish:PATCH",
   212  		"/namespaces/default/pods/otter:GET",
   213  		"/namespaces/default/pods/otter:GET",
   214  		"/namespaces/default/pods/dolphin:GET",
   215  		"/namespaces/default/pods:POST",
   216  	}
   217  	if len(expectedActions) != len(actions) {
   218  		t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
   219  		return
   220  	}
   221  	for k, v := range expectedActions {
   222  		if actions[k] != v {
   223  			t.Errorf("expected %s request got %s", v, actions[k])
   224  		}
   225  	}
   226  
   227  	if reaper.name != "squid" {
   228  		t.Errorf("unexpected reaper: %#v", reaper)
   229  	}
   230  
   231  }
   232  
   233  func TestBuild(t *testing.T) {
   234  	tests := []struct {
   235  		name        string
   236  		namespace   string
   237  		reader      io.Reader
   238  		count       int
   239  		swaggerFile string
   240  		err         bool
   241  		errMessage  string
   242  	}{
   243  		{
   244  			name:      "Valid input",
   245  			namespace: "test",
   246  			reader:    strings.NewReader(guestbookManifest),
   247  			count:     6,
   248  		}, {
   249  			name:        "Invalid schema",
   250  			namespace:   "test",
   251  			reader:      strings.NewReader(testInvalidServiceManifest),
   252  			swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json",
   253  			err:         true,
   254  			errMessage:  `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`,
   255  		},
   256  	}
   257  
   258  	for _, tt := range tests {
   259  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   260  		c := newTestClient(f)
   261  		if tt.swaggerFile != "" {
   262  			data, err := ioutil.ReadFile(tt.swaggerFile)
   263  			if err != nil {
   264  				t.Fatalf("could not read swagger spec: %s", err)
   265  			}
   266  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   267  			if err != nil {
   268  				t.Fatalf("could not load swagger spec: %s", err)
   269  			}
   270  			tf.Validator = validator
   271  		}
   272  
   273  		// Test for an invalid manifest
   274  		infos, err := c.Build(tt.namespace, tt.reader)
   275  		if err != nil && err.Error() != tt.errMessage {
   276  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   277  		} else if err != nil && !tt.err {
   278  			t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err)
   279  		}
   280  
   281  		if len(infos) != tt.count {
   282  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos))
   283  		}
   284  	}
   285  }
   286  
   287  type testPrinter struct {
   288  	Objects []runtime.Object
   289  	Err     error
   290  	printers.ResourcePrinter
   291  }
   292  
   293  func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
   294  	t.Objects = append(t.Objects, obj)
   295  	fmt.Fprintf(out, "%#v", obj)
   296  	return t.Err
   297  }
   298  
   299  func (t *testPrinter) HandledResources() []string {
   300  	return []string{}
   301  }
   302  
   303  func (t *testPrinter) AfterPrint(io.Writer, string) error {
   304  	return t.Err
   305  }
   306  
   307  func TestGet(t *testing.T) {
   308  	list := newPodList("starfish", "otter")
   309  	f, tf, _, _ := cmdtesting.NewAPIFactory()
   310  	tf.Printer = &testPrinter{}
   311  	tf.UnstructuredClient = &fake.RESTClient{
   312  		APIRegistry:          api.Registry,
   313  		NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer,
   314  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   315  			p, m := req.URL.Path, req.Method
   316  			//actions = append(actions, p+":"+m)
   317  			t.Logf("got request %s %s", p, m)
   318  			switch {
   319  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   320  				return newResponse(404, notFoundBody())
   321  			case p == "/namespaces/default/pods/otter" && m == "GET":
   322  				return newResponse(200, &list.Items[1])
   323  			default:
   324  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   325  				return nil, nil
   326  			}
   327  		}),
   328  	}
   329  	c := newTestClient(f)
   330  
   331  	// Test Success
   332  	data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: otter")
   333  	o, err := c.Get("default", data)
   334  	if err != nil {
   335  		t.Errorf("Expected missing results, got %q", err)
   336  	}
   337  	if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") {
   338  		t.Errorf("Expected v1/Pod otter, got %s", o)
   339  	}
   340  
   341  	// Test failure
   342  	data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: starfish")
   343  	o, err = c.Get("default", data)
   344  	if err != nil {
   345  		t.Errorf("Expected missing results, got %q", err)
   346  	}
   347  	if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") {
   348  		t.Errorf("Expected missing starfish, got %s", o)
   349  	}
   350  }
   351  
   352  func TestPerform(t *testing.T) {
   353  	tests := []struct {
   354  		name        string
   355  		namespace   string
   356  		reader      io.Reader
   357  		count       int
   358  		swaggerFile string
   359  		err         bool
   360  		errMessage  string
   361  	}{
   362  		{
   363  			name:      "Valid input",
   364  			namespace: "test",
   365  			reader:    strings.NewReader(guestbookManifest),
   366  			count:     6,
   367  		}, {
   368  			name:       "Empty manifests",
   369  			namespace:  "test",
   370  			reader:     strings.NewReader(""),
   371  			err:        true,
   372  			errMessage: "no objects visited",
   373  		},
   374  	}
   375  
   376  	for _, tt := range tests {
   377  		results := []*resource.Info{}
   378  
   379  		fn := func(info *resource.Info) error {
   380  			results = append(results, info)
   381  
   382  			if info.Namespace != tt.namespace {
   383  				t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace)
   384  			}
   385  			return nil
   386  		}
   387  
   388  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   389  		c := newTestClient(f)
   390  		if tt.swaggerFile != "" {
   391  			data, err := ioutil.ReadFile(tt.swaggerFile)
   392  			if err != nil {
   393  				t.Fatalf("could not read swagger spec: %s", err)
   394  			}
   395  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   396  			if err != nil {
   397  				t.Fatalf("could not load swagger spec: %s", err)
   398  			}
   399  			tf.Validator = validator
   400  		}
   401  
   402  		infos, err := c.Build(tt.namespace, tt.reader)
   403  		if err != nil && err.Error() != tt.errMessage {
   404  			t.Errorf("%q. Error while building manifests: %v", tt.name, err)
   405  		}
   406  
   407  		err = perform(infos, fn)
   408  		if (err != nil) != tt.err {
   409  			t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
   410  		}
   411  		if err != nil && err.Error() != tt.errMessage {
   412  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   413  		}
   414  
   415  		if len(results) != tt.count {
   416  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results))
   417  		}
   418  	}
   419  }
   420  
   421  func TestWaitAndGetCompletedPodPhase(t *testing.T) {
   422  	tests := []struct {
   423  		podPhase      api.PodPhase
   424  		expectedPhase api.PodPhase
   425  		err           bool
   426  		errMessage    string
   427  	}{
   428  		{
   429  			podPhase:      api.PodPending,
   430  			expectedPhase: api.PodUnknown,
   431  			err:           true,
   432  			errMessage:    "watch closed before Until timeout",
   433  		}, {
   434  			podPhase:      api.PodRunning,
   435  			expectedPhase: api.PodUnknown,
   436  			err:           true,
   437  			errMessage:    "watch closed before Until timeout",
   438  		}, {
   439  			podPhase:      api.PodSucceeded,
   440  			expectedPhase: api.PodSucceeded,
   441  		}, {
   442  			podPhase:      api.PodFailed,
   443  			expectedPhase: api.PodFailed,
   444  		},
   445  	}
   446  
   447  	for _, tt := range tests {
   448  		f, tf, codec, ns := cmdtesting.NewAPIFactory()
   449  		actions := make(map[string]string)
   450  
   451  		var testPodList api.PodList
   452  		testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test"))
   453  
   454  		tf.Client = &fake.RESTClient{
   455  			APIRegistry:          api.Registry,
   456  			NegotiatedSerializer: ns,
   457  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   458  				p, m := req.URL.Path, req.Method
   459  				actions[p] = m
   460  				switch {
   461  				case p == "/namespaces/test/pods/bestpod" && m == "GET":
   462  					return newResponse(200, &testPodList.Items[0])
   463  				case p == "/namespaces/test/pods" && m == "GET":
   464  					event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]}
   465  					return newEventResponse(200, &event)
   466  				default:
   467  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   468  					return nil, nil
   469  				}
   470  			}),
   471  		}
   472  
   473  		c := newTestClient(f)
   474  
   475  		phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second)
   476  		if (err != nil) != tt.err {
   477  			t.Fatalf("Expected error but there was none.")
   478  		}
   479  		if err != nil && err.Error() != tt.errMessage {
   480  			t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error())
   481  		}
   482  		if phase != tt.expectedPhase {
   483  			t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase)
   484  		}
   485  	}
   486  }
   487  
   488  func TestReal(t *testing.T) {
   489  	t.Skip("This is a live test, comment this line to run")
   490  	c := New(nil)
   491  	if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil {
   492  		t.Fatal(err)
   493  	}
   494  
   495  	testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
   496  	c = New(nil)
   497  	if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil {
   498  		t.Fatal(err)
   499  	}
   500  
   501  	if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil {
   502  		t.Fatal(err)
   503  	}
   504  
   505  	// ensures that delete does not fail if a resource is not found
   506  	if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil {
   507  		t.Fatal(err)
   508  	}
   509  }
   510  
   511  const testServiceManifest = `
   512  kind: Service
   513  apiVersion: v1
   514  metadata:
   515    name: my-service
   516  spec:
   517    selector:
   518      app: myapp
   519    ports:
   520      - port: 80
   521        protocol: TCP
   522        targetPort: 9376
   523  `
   524  
   525  const testInvalidServiceManifest = `
   526  kind: Service
   527  apiVersion: v1
   528  spec:
   529    ports:
   530      - port: "80"
   531  `
   532  
   533  const testEndpointManifest = `
   534  kind: Endpoints
   535  apiVersion: v1
   536  metadata:
   537    name: my-service
   538  subsets:
   539    - addresses:
   540        - ip: "1.2.3.4"
   541      ports:
   542        - port: 9376
   543  `
   544  
   545  const guestbookManifest = `
   546  apiVersion: v1
   547  kind: Service
   548  metadata:
   549    name: redis-master
   550    labels:
   551      app: redis
   552      tier: backend
   553      role: master
   554  spec:
   555    ports:
   556    - port: 6379
   557      targetPort: 6379
   558    selector:
   559      app: redis
   560      tier: backend
   561      role: master
   562  ---
   563  apiVersion: extensions/v1beta1
   564  kind: Deployment
   565  metadata:
   566    name: redis-master
   567  spec:
   568    replicas: 1
   569    template:
   570      metadata:
   571        labels:
   572          app: redis
   573          role: master
   574          tier: backend
   575      spec:
   576        containers:
   577        - name: master
   578          image: gcr.io/google_containers/redis:e2e  # or just image: redis
   579          resources:
   580            requests:
   581              cpu: 100m
   582              memory: 100Mi
   583          ports:
   584          - containerPort: 6379
   585  ---
   586  apiVersion: v1
   587  kind: Service
   588  metadata:
   589    name: redis-slave
   590    labels:
   591      app: redis
   592      tier: backend
   593      role: slave
   594  spec:
   595    ports:
   596      # the port that this service should serve on
   597    - port: 6379
   598    selector:
   599      app: redis
   600      tier: backend
   601      role: slave
   602  ---
   603  apiVersion: extensions/v1beta1
   604  kind: Deployment
   605  metadata:
   606    name: redis-slave
   607  spec:
   608    replicas: 2
   609    template:
   610      metadata:
   611        labels:
   612          app: redis
   613          role: slave
   614          tier: backend
   615      spec:
   616        containers:
   617        - name: slave
   618          image: gcr.io/google_samples/gb-redisslave:v1
   619          resources:
   620            requests:
   621              cpu: 100m
   622              memory: 100Mi
   623          env:
   624          - name: GET_HOSTS_FROM
   625            value: dns
   626          ports:
   627          - containerPort: 6379
   628  ---
   629  apiVersion: v1
   630  kind: Service
   631  metadata:
   632    name: frontend
   633    labels:
   634      app: guestbook
   635      tier: frontend
   636  spec:
   637    ports:
   638    - port: 80
   639    selector:
   640      app: guestbook
   641      tier: frontend
   642  ---
   643  apiVersion: extensions/v1beta1
   644  kind: Deployment
   645  metadata:
   646    name: frontend
   647  spec:
   648    replicas: 3
   649    template:
   650      metadata:
   651        labels:
   652          app: guestbook
   653          tier: frontend
   654      spec:
   655        containers:
   656        - name: php-redis
   657          image: gcr.io/google-samples/gb-frontend:v4
   658          resources:
   659            requests:
   660              cpu: 100m
   661              memory: 100Mi
   662          env:
   663          - name: GET_HOSTS_FROM
   664            value: dns
   665          ports:
   666          - containerPort: 80
   667  `