github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/tools/kubeclient.go (about) 1 /* 2 Copyright 2018 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 tools 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "net/url" 26 "os" 27 "os/signal" 28 "regexp" 29 "strconv" 30 "strings" 31 32 v1 "k8s.io/api/core/v1" 33 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/client-go/kubernetes" 35 "k8s.io/client-go/kubernetes/scheme" 36 _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // GKE support 37 "k8s.io/client-go/rest" 38 "k8s.io/client-go/tools/clientcmd" 39 "k8s.io/client-go/tools/portforward" 40 "k8s.io/client-go/tools/remotecommand" 41 "k8s.io/client-go/transport/spdy" 42 "k8s.io/client-go/util/exec" 43 ) 44 45 const ( 46 runtimeAnnotation = "kubernetes.io/target-runtime" 47 ) 48 49 // VMPodInfo describes a VM pod in a way that's necessary for virtletctl to 50 // handle it 51 type VMPodInfo struct { 52 // NodeName is the name of the node where the VM pod runs 53 NodeName string 54 // VirtletPodName is the name of the virtlet pod that manages this VM pod 55 VirtletPodName string 56 // ContainerID is the id of the container in the VM pod 57 ContainerID string 58 // ContainerName is the name of the container in the VM pod 59 ContainerName string 60 } 61 62 // LibvirtDomainName returns the name of the libvirt domain for the VMPodInfo. 63 func (podInfo VMPodInfo) LibvirtDomainName() string { 64 containerID := podInfo.ContainerID 65 if p := strings.Index(containerID, "__"); p >= 0 { 66 containerID = containerID[p+2:] 67 } 68 if len(containerID) > 13 { 69 containerID = containerID[:13] 70 } 71 return fmt.Sprintf("virtlet-%s-%s", containerID, podInfo.ContainerName) 72 } 73 74 // ForwardedPort specifies an entry for the PortForward request 75 type ForwardedPort struct { 76 // LocalPort specifies the local port to use. 0 means selecting 77 // a random local port. 78 LocalPort uint16 79 // RemotePort specifies the remote (pod-side) port to use. 80 RemotePort uint16 81 } 82 83 func (fp ForwardedPort) String() string { 84 if fp.LocalPort == 0 { 85 return fmt.Sprintf(":%d", fp.RemotePort) 86 } 87 return fmt.Sprintf("%d:%d", fp.LocalPort, fp.RemotePort) 88 } 89 90 // NOTE: this regexp ignores ipv6 port forward lines 91 var portForwardRx = regexp.MustCompile(`Forwarding from [^[]*:(\d+) -> \d+`) 92 93 // ParsePortForwardOutput extracts from returned by api "out" data information 94 // about local ports in each of ForwardedPort 95 func ParsePortForwardOutput(out string, ports []*ForwardedPort) error { 96 var localPorts []uint16 97 for _, l := range strings.Split(out, "\n") { 98 m := portForwardRx.FindStringSubmatch(l) 99 if m == nil { 100 continue 101 } 102 port, err := strconv.ParseUint(m[1], 10, 16) 103 if err != nil { 104 return fmt.Errorf("bad port forward line (can't parse the local port): %q", l) 105 } 106 localPorts = append(localPorts, uint16(port)) 107 } 108 if len(localPorts) != len(ports) { 109 return fmt.Errorf("bad port forward output (expected %d ports, got %d). Full output from the forwarder:\n%s", len(ports), len(localPorts), out) 110 } 111 for n, lp := range localPorts { 112 switch { 113 case ports[n].LocalPort == 0: 114 ports[n].LocalPort = lp 115 continue 116 case ports[n].LocalPort != lp: 117 return fmt.Errorf("port mismatch: %d instead of %d for the remote port %d. Full output from the forwarder:\n%s", lp, ports[n].LocalPort, ports[n].RemotePort, out) 118 } 119 } 120 return nil 121 } 122 123 // KubeClient contains methods for interfacing with Kubernetes clusters. 124 type KubeClient interface { 125 // GetNamesOfNodesMarkedForVirtlet returns a list of node names for nodes labeled 126 // with virtlet as an extra runtime. 127 GetNamesOfNodesMarkedForVirtlet() (nodeNames []string, err error) 128 // GetVirtletPodAndNodeNames returns a list of names of the 129 // virtlet pods present in the cluster and a list of 130 // corresponding node names that contain these pods. 131 GetVirtletPodAndNodeNames() (podNames []string, nodeNames []string, err error) 132 // GetVirtletPodNameForNode returns a name of the virtlet pod on 133 // the specified k8s node. 134 GetVirtletPodNameForNode(nodeName string) (string, error) 135 // GetVMPodInfo returns then name of the virtlet pod and the vm container name for 136 // the specified VM pod. 137 GetVMPodInfo(podName string) (*VMPodInfo, error) 138 // CreatePod creates a pod. 139 CreatePod(pod *v1.Pod) (*v1.Pod, error) 140 // GetPod retrieves a pod definition from the apiserver. 141 GetPod(name, namespace string) (*v1.Pod, error) 142 // DeletePod removes the specified pod from the specified namespace. 143 DeletePod(pod, namespace string) error 144 // ExecInContainer given a pod, a container, a namespace and a command 145 // executes that command inside the pod's container returning stdout and stderr output 146 // as strings and an error if it has occurred. 147 // The specified stdin, stdout and stderr are used as the 148 // standard input / output / error streams of the remote command. 149 // No TTY is allocated by this function stdin. 150 ExecInContainer(podName, containerName, namespace string, stdin io.Reader, stdout, stderr io.Writer, command []string) (int, error) 151 // ForwardPorts starts forwarding the specified ports to the specified pod in background. 152 // If a port entry has LocalPort = 0, it's updated with the real port number that was 153 // selected by the forwarder. 154 // The function returns when the ports are ready for use or if/when an error occurs. 155 // Close stopCh to stop the port forwarder. 156 ForwardPorts(podName, namespace string, ports []*ForwardedPort) (stopCh chan struct{}, err error) 157 // Retrieves the logs for the specified pod. If tailLines is 158 // non-zero, it limits the numer of lines to be retrieved. 159 PodLogs(podName, containerName, namespace string, tailLines int64) ([]byte, error) 160 } 161 162 type remoteExecutor interface { 163 stream(config *rest.Config, method string, url *url.URL, options remotecommand.StreamOptions) error 164 } 165 166 type defaultExecutor struct{} 167 168 var _ remoteExecutor = defaultExecutor{} 169 170 func (e defaultExecutor) stream(config *rest.Config, method string, url *url.URL, options remotecommand.StreamOptions) error { 171 executor, err := remotecommand.NewSPDYExecutor(config, method, url) 172 if err != nil { 173 return err 174 } 175 return executor.Stream(options) 176 } 177 178 type portForwarder interface { 179 forwardPorts(config *rest.Config, method string, url *url.URL, ports []string, stopChannel, readyChannel chan struct{}, out io.Writer) error 180 } 181 182 type defaultPortForwarder struct{} 183 184 var _ portForwarder = defaultPortForwarder{} 185 186 func (pf defaultPortForwarder) forwardPorts(config *rest.Config, method string, url *url.URL, ports []string, stopChannel, readyChannel chan struct{}, out io.Writer) error { 187 transport, upgrader, err := spdy.RoundTripperFor(config) 188 if err != nil { 189 return err 190 } 191 dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) 192 if err != nil { 193 return err 194 } 195 fw, err := portforward.New(dialer, ports, stopChannel, readyChannel, out, os.Stderr) 196 if err != nil { 197 return err 198 } 199 return fw.ForwardPorts() 200 } 201 202 // RealKubeClient is used to access a Kubernetes cluster. 203 type RealKubeClient struct { 204 client kubernetes.Interface 205 clientCfg clientcmd.ClientConfig 206 restClient rest.Interface 207 config *rest.Config 208 namespace string 209 executor remoteExecutor 210 portForwarder portForwarder 211 } 212 213 var _ KubeClient = &RealKubeClient{} 214 215 // NewRealKubeClient creates a RealKubeClient for the specified ClientConfig. 216 func NewRealKubeClient(clientCfg clientcmd.ClientConfig) *RealKubeClient { 217 return &RealKubeClient{ 218 clientCfg: clientCfg, 219 executor: defaultExecutor{}, 220 portForwarder: defaultPortForwarder{}, 221 } 222 } 223 224 func (c *RealKubeClient) setup() error { 225 if c.client != nil { 226 return nil 227 } 228 229 config, err := c.clientCfg.ClientConfig() 230 if err != nil { 231 return err 232 } 233 234 client, err := kubernetes.NewForConfig(config) 235 if err != nil { 236 return fmt.Errorf("can't create kubernetes api client: %v", err) 237 } 238 239 ns, _, err := c.clientCfg.Namespace() 240 if err != nil { 241 return err 242 } 243 244 c.client = client 245 c.config = config 246 c.namespace = ns 247 c.restClient = client.CoreV1().RESTClient() 248 return nil 249 } 250 251 // GetNamesOfNodesMarkedForVirtlet implements GetNamesOfNodesMarkedForVirtlet methor of KubeClient interface. 252 func (c *RealKubeClient) GetNamesOfNodesMarkedForVirtlet() ([]string, error) { 253 if err := c.setup(); err != nil { 254 return nil, err 255 } 256 opts := meta_v1.ListOptions{ 257 LabelSelector: "extraRuntime=virtlet", 258 } 259 nodes, err := c.client.CoreV1().Nodes().List(opts) 260 if err != nil { 261 return nil, err 262 } 263 264 var nodeNames []string 265 for _, item := range nodes.Items { 266 nodeNames = append(nodeNames, item.Name) 267 } 268 return nodeNames, nil 269 } 270 271 func (c *RealKubeClient) getVirtletPodAndNodeNames(nodeName string) (podNames []string, nodeNames []string, err error) { 272 if err := c.setup(); err != nil { 273 return nil, nil, err 274 } 275 opts := meta_v1.ListOptions{ 276 LabelSelector: "runtime=virtlet", 277 } 278 if nodeName != "" { 279 opts.FieldSelector = "spec.nodeName=" + nodeName 280 } 281 pods, err := c.client.CoreV1().Pods("kube-system").List(opts) 282 if err != nil { 283 return nil, nil, err 284 } 285 286 for _, item := range pods.Items { 287 podNames = append(podNames, item.Name) 288 nodeNames = append(nodeNames, item.Spec.NodeName) 289 } 290 return podNames, nodeNames, nil 291 } 292 293 func (c *RealKubeClient) getVMPod(podName string) (*v1.Pod, error) { 294 if err := c.setup(); err != nil { 295 return nil, err 296 } 297 pod, err := c.client.CoreV1().Pods(c.namespace).Get(podName, meta_v1.GetOptions{}) 298 if err != nil { 299 return nil, err 300 } 301 if pod.Annotations == nil || pod.Annotations[runtimeAnnotation] != virtletRuntime { 302 return nil, fmt.Errorf("%q is not a VM pod (missing annotation: %q=%q)", podName, runtimeAnnotation, virtletRuntime) 303 } 304 return pod, nil 305 } 306 307 // GetVirtletPodAndNodeNames implements GetVirtletPodAndNodeNames method of KubeClient interface. 308 func (c *RealKubeClient) GetVirtletPodAndNodeNames() (podNames []string, nodeNames []string, err error) { 309 return c.getVirtletPodAndNodeNames("") 310 } 311 312 // GetVirtletPodNameForNode implements GetVirtletPodNameForNode method of KubeClient interface. 313 func (c *RealKubeClient) GetVirtletPodNameForNode(nodeName string) (string, error) { 314 virtletPodNames, _, err := c.getVirtletPodAndNodeNames(nodeName) 315 if err != nil { 316 return "", err 317 } 318 319 if len(virtletPodNames) == 0 { 320 return "", fmt.Errorf("no Virtlet pods found on the node %q", nodeName) 321 } 322 323 if len(virtletPodNames) > 1 { 324 return "", fmt.Errorf("more than one Virtlet pod found on the node %q", nodeName) 325 } 326 327 return virtletPodNames[0], nil 328 } 329 330 // GetVMPodInfo implements GetVMPodInfo method of KubeClient interface. 331 func (c *RealKubeClient) GetVMPodInfo(podName string) (*VMPodInfo, error) { 332 pod, err := c.getVMPod(podName) 333 if err != nil { 334 return nil, err 335 } 336 if pod.Spec.NodeName == "" { 337 return nil, fmt.Errorf("pod %q doesn't have a node associated with it", podName) 338 } 339 if len(pod.Spec.Containers) != 1 { 340 return nil, fmt.Errorf("vm pod %q is expected to have just one container but it has %d containers instead", podName, len(pod.Spec.Containers)) 341 } 342 343 if len(pod.Status.ContainerStatuses) != 1 { 344 return nil, fmt.Errorf("vm pod %q is expected to have just one container status but it has %d container statuses instead", podName, len(pod.Status.ContainerStatuses)) 345 } 346 347 virtletPodName, err := c.GetVirtletPodNameForNode(pod.Spec.NodeName) 348 if err != nil { 349 return nil, err 350 } 351 352 return &VMPodInfo{ 353 NodeName: pod.Spec.NodeName, 354 VirtletPodName: virtletPodName, 355 ContainerID: pod.Status.ContainerStatuses[0].ContainerID, 356 ContainerName: pod.Spec.Containers[0].Name, 357 }, nil 358 } 359 360 // CreatePod implements CreatePod method of KubeClient interface. 361 func (c *RealKubeClient) CreatePod(pod *v1.Pod) (*v1.Pod, error) { 362 if err := c.setup(); err != nil { 363 return nil, err 364 } 365 return c.client.CoreV1().Pods(pod.Namespace).Create(pod) 366 } 367 368 // GetPod implements GetPod method of KubeClient interface. 369 func (c *RealKubeClient) GetPod(name, namespace string) (*v1.Pod, error) { 370 return c.client.CoreV1().Pods(namespace).Get(name, meta_v1.GetOptions{}) 371 } 372 373 // DeletePod implements DeletePod method of KubeClient interface. 374 func (c *RealKubeClient) DeletePod(name, namespace string) error { 375 return c.client.CoreV1().Pods(namespace).Delete(name, &meta_v1.DeleteOptions{}) 376 } 377 378 // ExecInContainer implements ExecInContainer method of KubeClient interface. 379 func (c *RealKubeClient) ExecInContainer(podName, containerName, namespace string, stdin io.Reader, stdout, stderr io.Writer, command []string) (int, error) { 380 if err := c.setup(); err != nil { 381 return 0, err 382 } 383 if namespace == "" { 384 namespace = c.namespace 385 } 386 req := c.restClient.Post(). 387 Resource("pods"). 388 Name(podName). 389 Namespace(namespace). 390 SubResource("exec"). 391 VersionedParams(&v1.PodExecOptions{ 392 Container: containerName, 393 Command: command, 394 Stdin: stdin != nil, 395 Stdout: stdout != nil, 396 Stderr: stderr != nil, 397 TTY: false, 398 }, scheme.ParameterCodec) 399 400 exitCode := 0 401 if err := c.executor.stream(c.config, "POST", req.URL(), remotecommand.StreamOptions{ 402 Stdin: stdin, 403 Stdout: stdout, 404 Stderr: stderr, 405 }); err != nil { 406 if c, ok := err.(exec.CodeExitError); ok { 407 exitCode = c.Code 408 } else { 409 return 0, err 410 } 411 } 412 413 return exitCode, nil 414 } 415 416 // ForwardPorts implements ForwardPorts method of KubeClient interface. 417 func (c *RealKubeClient) ForwardPorts(podName, namespace string, ports []*ForwardedPort) (stopCh chan struct{}, err error) { 418 if len(ports) == 0 { 419 return nil, errors.New("no ports specified") 420 } 421 422 if err := c.setup(); err != nil { 423 return nil, err 424 } 425 426 if namespace == "" { 427 namespace = c.namespace 428 } 429 430 pod, err := c.client.CoreV1().Pods(namespace).Get(podName, meta_v1.GetOptions{}) 431 if err != nil { 432 return nil, err 433 } 434 435 if pod.Status.Phase != v1.PodRunning { 436 return nil, fmt.Errorf("unable to forward port because pod is not running (current status is %v)", pod.Status.Phase) 437 } 438 439 signals := make(chan os.Signal, 1) 440 signal.Notify(signals, os.Interrupt) 441 defer signal.Stop(signals) 442 443 stopCh = make(chan struct{}) 444 go func() { 445 <-signals 446 if stopCh != nil { 447 close(stopCh) 448 } 449 }() 450 451 req := c.restClient.Post(). 452 Resource("pods"). 453 Namespace(namespace). 454 Name(pod.Name). 455 SubResource("portforward") 456 var buf bytes.Buffer 457 var portStrs []string 458 for _, p := range ports { 459 portStrs = append(portStrs, p.String()) 460 } 461 errCh := make(chan error, 1) 462 readyCh := make(chan struct{}) 463 go func() { 464 errCh <- c.portForwarder.forwardPorts(c.config, "POST", req.URL(), portStrs, stopCh, readyCh, &buf) 465 }() 466 467 select { 468 case err := <-errCh: 469 return nil, err 470 case <-readyCh: 471 // FIXME: there appears to be no better way to get back the local ports as of now 472 if err := ParsePortForwardOutput(buf.String(), ports); err != nil { 473 return nil, err 474 } 475 } 476 return stopCh, nil 477 } 478 479 // PodLogs retrieves the logs of the specified container in the pod. 480 // limitBytes of zero specifies no size limit for the logs. 481 // limitSeconds of zero specifies no time limit for the logs. 482 func (c *RealKubeClient) PodLogs(podName, containerName, namespace string, tailLines int64) ([]byte, error) { 483 // FIXME: that's hard to test properly using the fake 484 // clientset. 485 opts := &v1.PodLogOptions{ 486 Container: containerName, 487 } 488 if tailLines != 0 { 489 opts.TailLines = &tailLines 490 } 491 return c.client.CoreV1().Pods(namespace).GetLogs(podName, opts).Do().Raw() 492 }