sigs.k8s.io/gateway-api@v1.0.0/conformance/utils/echo/pod.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 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 echo 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "strings" 24 "testing" 25 "time" 26 27 v1 "k8s.io/api/core/v1" 28 klabels "k8s.io/apimachinery/pkg/labels" 29 "k8s.io/client-go/kubernetes/scheme" 30 "k8s.io/client-go/rest" 31 "k8s.io/client-go/tools/remotecommand" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 34 "sigs.k8s.io/gateway-api/conformance/utils/config" 35 "sigs.k8s.io/gateway-api/conformance/utils/http" 36 "sigs.k8s.io/gateway-api/conformance/utils/suite" 37 ) 38 39 // MeshPod represents a connection to a specific pod running in the mesh. 40 // This can be used to trigger requests *from* that pod. 41 type MeshPod struct { 42 Name string 43 Namespace string 44 Address string 45 rc rest.Interface 46 rcfg *rest.Config 47 } 48 49 type MeshApplication string 50 51 const ( 52 MeshAppEchoV1 MeshApplication = "app=echo,version=v1" 53 MeshAppEchoV2 MeshApplication = "app=echo,version=v2" 54 ) 55 56 func (m *MeshPod) MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, exp http.ExpectedResponse, timeoutConfig config.TimeoutConfig) { 57 t.Helper() 58 59 http.AwaitConvergence(t, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, func(elapsed time.Duration) bool { 60 req := makeRequest(t, exp.Request) 61 62 resp, err := m.request(req) 63 if err != nil { 64 t.Logf("Request %v failed, not ready yet: %v (after %v)", req, err.Error(), elapsed) 65 return false 66 } 67 t.Logf("Got resp %v", resp) 68 if err := compareRequest(exp, resp); err != nil { 69 t.Logf("Response expectation failed for request: %v not ready yet: %v (after %v)", req, err, elapsed) 70 return false 71 } 72 return true 73 }) 74 75 t.Logf("Request passed") 76 } 77 78 func makeRequest(t *testing.T, r http.Request) []string { 79 protocol := strings.ToLower(r.Protocol) 80 if protocol == "" { 81 protocol = "http" 82 } 83 host := http.CalculateHost(t, r.Host, protocol) 84 args := []string{"client", fmt.Sprintf("%s://%s%s", protocol, host, r.Path)} 85 if r.Method != "" { 86 args = append(args, "--method="+r.Method) 87 } 88 for k, v := range r.Headers { 89 args = append(args, "-H", fmt.Sprintf("%v: %v", k, v)) 90 } 91 return args 92 } 93 94 func compareRequest(exp http.ExpectedResponse, resp Response) error { 95 want := exp.Response 96 if fmt.Sprint(want.StatusCode) != resp.Code { 97 return fmt.Errorf("wanted status code %v, got %v", want.StatusCode, resp.Code) 98 } 99 for _, name := range want.AbsentHeaders { 100 if v := resp.ResponseHeaders.Values(name); len(v) != 0 { 101 return fmt.Errorf("expected no header %q, got %v", name, v) 102 } 103 } 104 for k, v := range want.Headers { 105 if got := resp.ResponseHeaders.Get(k); got != v { 106 return fmt.Errorf("expected header %v=%v, got %v", k, v, got) 107 } 108 } 109 if !strings.HasPrefix(resp.Hostname, exp.Backend) { 110 return fmt.Errorf("expected pod name to start with %s, got %s", exp.Backend, resp.Hostname) 111 } 112 return nil 113 } 114 115 func (m *MeshPod) request(args []string) (Response, error) { 116 container := "echo" 117 118 req := m.rc.Post(). 119 Resource("pods"). 120 Name(m.Name). 121 Namespace(m.Namespace). 122 SubResource("exec"). 123 Param("container", container). 124 VersionedParams(&v1.PodExecOptions{ 125 Container: container, 126 Command: args, 127 Stdin: false, 128 Stdout: true, 129 Stderr: true, 130 TTY: false, 131 }, scheme.ParameterCodec) 132 133 exec, err := remotecommand.NewSPDYExecutor(m.rcfg, "POST", req.URL()) 134 if err != nil { 135 return Response{}, err 136 } 137 138 var stdoutBuf, stderrBuf bytes.Buffer 139 err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{ 140 Stdin: nil, 141 Stdout: &stdoutBuf, 142 Stderr: &stderrBuf, 143 Tty: false, 144 }) 145 if err != nil { 146 return Response{}, err 147 } 148 149 return ParseResponse(stdoutBuf.String()), nil 150 } 151 152 func ConnectToApp(t *testing.T, s *suite.ConformanceTestSuite, app MeshApplication) MeshPod { 153 return ConnectToAppInNamespace(t, s, app, "gateway-conformance-mesh") 154 } 155 156 func ConnectToAppInNamespace(t *testing.T, s *suite.ConformanceTestSuite, app MeshApplication, ns string) MeshPod { 157 lbls, _ := klabels.Parse(string(app)) 158 159 podsList := v1.PodList{} 160 err := s.Client.List(context.Background(), &podsList, client.InNamespace(ns), client.MatchingLabelsSelector{Selector: lbls}) 161 if err != nil { 162 t.Fatalf("failed to query pods in app %v", app) 163 } 164 if len(podsList.Items) == 0 { 165 t.Fatalf("no pods found in app %v", app) 166 } 167 pod := podsList.Items[0] 168 podName := pod.Name 169 podNamespace := pod.Namespace 170 171 return MeshPod{ 172 Name: podName, 173 Namespace: podNamespace, 174 Address: pod.Status.PodIP, 175 rc: s.Clientset.CoreV1().RESTClient(), 176 rcfg: s.RestConfig, 177 } 178 }