github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/stream/streamer.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 stream 18 19 import ( 20 "bytes" 21 "fmt" 22 "io" 23 "net" 24 "os/exec" 25 26 "github.com/Mirantis/virtlet/pkg/cni" 27 "github.com/docker/docker/pkg/pools" 28 "github.com/golang/glog" 29 30 "k8s.io/client-go/tools/remotecommand" 31 32 kubeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" 33 kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" 34 ) 35 36 // GetAttach returns attach stream request 37 func (s *Server) GetAttach(req *kubeapi.AttachRequest) (*kubeapi.AttachResponse, error) { 38 return s.streamServer.GetAttach(req) 39 } 40 41 // GetPortForward returns pofrforward stream request 42 func (s *Server) GetPortForward(req *kubeapi.PortForwardRequest) (*kubeapi.PortForwardResponse, error) { 43 return s.streamServer.GetPortForward(req) 44 } 45 46 // Attach endpoint for streaming.Runtime 47 func (s *Server) Attach(containerID string, inputStream io.Reader, outputStream, errorStream io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 48 glog.V(1).Infoln("New Attach request", containerID) 49 c, ok := s.unixServer.UnixConnections.Load(containerID) 50 if ok == false { 51 return fmt.Errorf("could not find vm %q", containerID) 52 } 53 conn := c.(*net.UnixConn) 54 55 outChan := make(chan []byte) 56 s.unixServer.AddOutputReader(containerID, outChan) 57 58 kubecontainer.HandleResizing(resize, func(size remotecommand.TerminalSize) { 59 glog.Infof("Got a resize event: %+v", size) 60 }) 61 62 receiveStdout := make(chan error) 63 if outputStream != nil { 64 go func() { 65 for data := range outChan { 66 outputStream.Write(data) 67 } 68 }() 69 } 70 71 stdinDone := make(chan error) 72 go func() { 73 var err error 74 if inputStream != nil { 75 _, err = CopyDetachable(conn, inputStream, nil) 76 if err != nil { 77 glog.V(1).Info("Attach coppy error: %v", err) 78 } 79 stdinDone <- err 80 } 81 }() 82 83 var err error 84 select { 85 case err = <-receiveStdout: 86 case err = <-stdinDone: 87 } 88 s.unixServer.RemoveOutputReader(containerID, outChan) 89 glog.V(1).Infoln("Attach request finished", containerID) 90 return err 91 } 92 93 // PortForward endpoint for streaming.Runtime 94 func (s *Server) PortForward(podSandboxID string, port int32, stream io.ReadWriteCloser) error { 95 // implementation based on https://github.com/kubernetes-incubator/cri-o/blob/master/server/container_portforward.go 96 glog.V(1).Infoln("New PortForward request", podSandboxID) 97 98 socatPath, lookupErr := exec.LookPath("socat") 99 if lookupErr != nil { 100 return fmt.Errorf("unable to do port forwarding: socat not found") 101 } 102 103 ip, err := s.getPodSandboxIP(podSandboxID) 104 if err != nil { 105 return fmt.Errorf("unable to do port forwarding: %v", err) 106 } 107 108 args := []string{"-", fmt.Sprintf("TCP4:%s:%d", ip, port)} 109 110 command := exec.Command(socatPath, args...) 111 command.Stdout = stream 112 113 stderr := new(bytes.Buffer) 114 command.Stderr = stderr 115 116 // If we use Stdin, command.Run() won't return until the goroutine that's copying 117 // from stream finishes. Unfortunately, if you have a client like telnet connected 118 // via port forwarding, as long as the user's telnet client is connected to the user's 119 // local listener that port forwarding sets up, the telnet session never exits. This 120 // means that even if socat has finished running, command.Run() won't ever return 121 // (because the client still has the connection and stream open). 122 // 123 // The work around is to use StdinPipe(), as Wait() (called by Run()) closes the pipe 124 // when the command (socat) exits. 125 inPipe, err := command.StdinPipe() 126 if err != nil { 127 return fmt.Errorf("unable to do port forwarding: error creating stdin pipe: %v", err) 128 } 129 go func() { 130 pools.Copy(inPipe, stream) 131 inPipe.Close() 132 }() 133 134 if err := command.Run(); err != nil { 135 return fmt.Errorf("%v: %s", err, stderr.String()) 136 } 137 138 return nil 139 } 140 141 func (s *Server) getPodSandboxIP(sandboxID string) (string, error) { 142 sandbox := s.metadataStore.PodSandbox(sandboxID) 143 sandboxInfo, err := sandbox.Retrieve() 144 if err != nil { 145 glog.Errorf("Error when getting pod sandbox %q: %v", sandboxID, err) 146 return "", err 147 } 148 if sandboxInfo == nil { 149 glog.Errorf("Missing metadata for pod sandbox %q", sandboxID) 150 return "", fmt.Errorf("missing metadata for pod sandbox %q", sandboxID) 151 } 152 153 if sandboxInfo.ContainerSideNetwork == nil { 154 return "", fmt.Errorf("ContainerSideNetwork missing in PodSandboxInfo returned from medatada store") 155 } 156 ip := cni.GetPodIP(sandboxInfo.ContainerSideNetwork.Result) 157 if ip != "" { 158 return ip, nil 159 } 160 return "", fmt.Errorf("Couldn't get IP address for for PodSandbox: %s", sandboxID) 161 }