github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/tests/e2e/framework/pod_interface.go (about) 1 /* 2 Copyright 2017 Mirantis 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 framework 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "os" 27 "os/signal" 28 "strings" 29 "time" 30 31 "github.com/davecgh/go-spew/spew" 32 "k8s.io/api/core/v1" 33 k8serrors "k8s.io/apimachinery/pkg/api/errors" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/client-go/kubernetes/scheme" 36 "k8s.io/client-go/tools/portforward" 37 "k8s.io/client-go/tools/remotecommand" 38 "k8s.io/client-go/transport/spdy" 39 "k8s.io/client-go/util/exec" 40 41 "github.com/Mirantis/virtlet/pkg/tools" 42 ) 43 44 // PodInterface provides API to work with a pod 45 type PodInterface struct { 46 controller *Controller 47 hasService bool 48 49 Pod *v1.Pod 50 } 51 52 func newPodInterface(controller *Controller, pod *v1.Pod) *PodInterface { 53 return &PodInterface{ 54 controller: controller, 55 Pod: pod, 56 } 57 } 58 59 // Create creates pod in the k8s 60 func (pi *PodInterface) Create() error { 61 updatedPod, err := pi.controller.client.Pods(pi.controller.Namespace()).Create(pi.Pod) 62 if err != nil { 63 return err 64 } 65 pi.Pod = updatedPod 66 return nil 67 } 68 69 // Delete deletes the pod and associated service, which was earlier created by `controller.Run()` 70 func (pi *PodInterface) Delete() error { 71 if pi.hasService { 72 pi.controller.client.Services(pi.Pod.Namespace).Delete(pi.Pod.Name, nil) 73 } 74 return pi.controller.client.Pods(pi.Pod.Namespace).Delete(pi.Pod.Name, nil) 75 } 76 77 // WaitForPodStatus waits for the pod to reach the specified status. If expectedContainerErrors 78 // is empty, the pod is expected to become Running and Ready. If it isn't, the pod is expected 79 // to have one of these errors among its container statuses. 80 func (pi *PodInterface) WaitForPodStatus(expectedContainerErrors []string, timing ...time.Duration) error { 81 timeout := time.Minute * 5 82 pollPeriond := time.Second 83 consistencyPeriod := time.Second * 5 84 if len(timing) > 0 { 85 timeout = timing[0] 86 } 87 if len(timing) > 1 { 88 pollPeriond = timing[1] 89 } 90 if len(timing) > 2 { 91 consistencyPeriod = timing[2] 92 } 93 94 return waitForConsistentState(func() error { 95 podUpdated, err := pi.controller.client.Pods(pi.Pod.Namespace).Get(pi.Pod.Name, metav1.GetOptions{}) 96 if err != nil { 97 return err 98 } 99 pi.Pod = podUpdated 100 101 needErrors := len(expectedContainerErrors) > 0 102 phase := v1.PodRunning 103 if needErrors { 104 phase = v1.PodPending 105 } 106 if podUpdated.Status.Phase != phase { 107 return fmt.Errorf("pod %s is not %s phase: %s", podUpdated.Name, phase, podUpdated.Status.Phase) 108 } 109 110 gotExpectedError := false 111 for _, cs := range podUpdated.Status.ContainerStatuses { 112 switch { 113 case !needErrors && cs.State.Running == nil: 114 return fmt.Errorf("container %s in pod %s is not running: %s", cs.Name, podUpdated.Name, spew.Sdump(cs.State)) 115 case !needErrors && !cs.Ready: 116 return fmt.Errorf("container %s in pod %s did not passed its readiness probe", cs.Name, podUpdated.Name) 117 case needErrors && cs.State.Waiting == nil: 118 return fmt.Errorf("container %s in pod %s not in waiting state", cs.Name, podUpdated.Name) 119 case needErrors: 120 for _, errStr := range expectedContainerErrors { 121 if cs.State.Waiting.Reason == errStr { 122 gotExpectedError = true 123 break 124 } 125 } 126 } 127 } 128 if needErrors && !gotExpectedError { 129 return fmt.Errorf("didn't get one of expected container errors: %s", strings.Join(expectedContainerErrors, " | ")) 130 } 131 return nil 132 }, timeout, pollPeriond, consistencyPeriod) 133 } 134 135 // Wait waits for pod to start and checks that it doesn't fail immediately after that 136 func (pi *PodInterface) Wait(timing ...time.Duration) error { 137 return pi.WaitForPodStatus(nil, timing...) 138 } 139 140 // WaitForDestruction waits for the pod to be deleted 141 func (pi *PodInterface) WaitForDestruction(timing ...time.Duration) error { 142 timeout := time.Minute * 5 143 pollPeriond := time.Second 144 consistencyPeriod := time.Second * 5 145 if len(timing) > 0 { 146 timeout = timing[0] 147 } 148 if len(timing) > 1 { 149 pollPeriond = timing[1] 150 } 151 if len(timing) > 2 { 152 consistencyPeriod = timing[2] 153 } 154 155 return waitForConsistentState(func() error { 156 if _, err := pi.controller.client.Pods(pi.Pod.Namespace).Get(pi.Pod.Name, metav1.GetOptions{}); err != nil { 157 if k8serrors.IsNotFound(err) { 158 return nil 159 } 160 return err 161 } 162 return fmt.Errorf("pod %s was not deleted", pi.Pod.Name) 163 }, timeout, pollPeriond, consistencyPeriod) 164 } 165 166 // Container returns an interface to handle one of the pod's 167 // containers. If name is empty, it takes the first container 168 // of the pod. 169 func (pi *PodInterface) Container(name string) (Executor, error) { 170 if name == "" && len(pi.Pod.Spec.Containers) > 0 { 171 name = pi.Pod.Spec.Containers[0].Name 172 } 173 found := false 174 for _, c := range pi.Pod.Spec.Containers { 175 if c.Name == name { 176 found = true 177 break 178 } 179 } 180 if !found { 181 return nil, fmt.Errorf("container %s doesn't exist in pod %s in namespace %s", name, pi.Pod.Name, pi.Pod.Namespace) 182 } 183 return &containerInterface{ 184 podInterface: pi, 185 name: name, 186 }, nil 187 } 188 189 // PortForward starts port forwarding to the specified ports to the specified pod 190 // in background. If a port entry has LocalPort = 0, it's updated with the real 191 // port number that was selected by the forwarder. 192 // Close returned channel to stop the port forwarder. 193 func (pi *PodInterface) PortForward(ports []*tools.ForwardedPort) (chan struct{}, error) { 194 if len(ports) == 0 { 195 return nil, errors.New("no ports specified") 196 } 197 198 signals := make(chan os.Signal, 1) 199 signal.Notify(signals, os.Interrupt) 200 defer signal.Stop(signals) 201 202 stopCh := make(chan struct{}) 203 go func() { 204 <-signals 205 if stopCh != nil { 206 close(stopCh) 207 } 208 }() 209 210 restClient := pi.controller.client.RESTClient() 211 req := restClient.Post(). 212 Resource("pods"). 213 Name(pi.Pod.Name). 214 Namespace(pi.Pod.Namespace). 215 SubResource("portforward") 216 217 var buf bytes.Buffer 218 var portsStr []string 219 for _, p := range ports { 220 portsStr = append(portsStr, p.String()) 221 } 222 errCh := make(chan error, 1) 223 readyCh := make(chan struct{}) 224 go func() { 225 transport, upgrader, err := spdy.RoundTripperFor(pi.controller.restConfig) 226 if err != nil { 227 errCh <- err 228 return 229 } 230 dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) 231 if err != nil { 232 errCh <- err 233 return 234 } 235 fw, err := portforward.New(dialer, portsStr, stopCh, readyCh, &buf, os.Stderr) 236 if err != nil { 237 errCh <- err 238 return 239 } 240 errCh <- fw.ForwardPorts() 241 }() 242 243 select { 244 case err := <-errCh: 245 return nil, err 246 case <-readyCh: 247 // FIXME: there appears to be no better way to get back the local ports as of now 248 if err := tools.ParsePortForwardOutput(buf.String(), ports); err != nil { 249 return nil, err 250 } 251 } 252 return stopCh, nil 253 254 } 255 256 // DinDNodeExecutor return DinD executor for node, where this pod is located 257 func (pi *PodInterface) DinDNodeExecutor() (Executor, error) { 258 return pi.controller.DinDNodeExecutor(pi.Pod.Spec.NodeName) 259 } 260 261 // LoadEvents retrieves the evnets for this pod as a list 262 // of strings of the form Type:Reason:Message 263 func (pi *PodInterface) LoadEvents() ([]string, error) { 264 events, err := pi.controller.client.Events(pi.controller.Namespace()).Search(scheme.Scheme, pi.Pod) 265 if err != nil { 266 return nil, err 267 } 268 var r []string 269 for _, e := range events.Items { 270 r = append(r, fmt.Sprintf("%s:%s:%s", e.Type, e.Reason, e.Message)) 271 } 272 return r, nil 273 } 274 275 type containerInterface struct { 276 podInterface *PodInterface 277 name string 278 } 279 280 // Run executes commands in one of containers in the pod 281 func (ci *containerInterface) Run(stdin io.Reader, stdout, stderr io.Writer, command ...string) error { 282 restClient := ci.podInterface.controller.client.RESTClient() 283 req := restClient.Post(). 284 Resource("pods"). 285 Name(ci.podInterface.Pod.Name). 286 Namespace(ci.podInterface.Pod.Namespace). 287 SubResource("exec") 288 req.VersionedParams(&v1.PodExecOptions{ 289 Container: ci.name, 290 Command: command, 291 Stdin: stdin != nil, 292 Stdout: stdout != nil, 293 Stderr: stderr != nil, 294 }, scheme.ParameterCodec) 295 296 executor, err := remotecommand.NewSPDYExecutor(ci.podInterface.controller.restConfig, "POST", req.URL()) 297 if err != nil { 298 return err 299 } 300 301 options := remotecommand.StreamOptions{ 302 Stdin: stdin, 303 Stdout: stdout, 304 Stderr: stderr, 305 } 306 307 if err := executor.Stream(options); err != nil { 308 if c, ok := err.(exec.CodeExitError); ok { 309 return CommandError{ExitCode: c.Code} 310 } 311 return err 312 } 313 314 return nil 315 } 316 317 // Close closes the executor 318 func (*containerInterface) Close() error { 319 return nil 320 } 321 322 // Start is a placeholder for fulfilling the Executor interface 323 func (*containerInterface) Start(stdin io.Reader, stdout, stderr io.Writer, command ...string) (Command, error) { 324 return nil, errors.New("Not Implemented") 325 } 326 327 // Logs returns the logs of the container as a string. 328 func (ci *containerInterface) Logs() (string, error) { 329 restClient := ci.podInterface.controller.client.RESTClient() 330 req := restClient.Get(). 331 Name(ci.podInterface.Pod.Name). 332 Namespace(ci.podInterface.Pod.Namespace). 333 Resource("pods"). 334 SubResource("log") 335 req.VersionedParams(&v1.PodLogOptions{ 336 Container: ci.name, 337 }, scheme.ParameterCodec) 338 stream, err := req.Stream() 339 if err != nil { 340 return "", err 341 } 342 defer stream.Close() 343 344 bs, err := ioutil.ReadAll(stream) 345 if err != nil { 346 return "", fmt.Errorf("ReadAll(): %v", err) 347 } 348 349 return string(bs), nil 350 }