2
0

portforward.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "os/signal"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/briandowns/spinner"
  14. api "github.com/porter-dev/porter/api/client"
  15. "github.com/porter-dev/porter/api/types"
  16. "github.com/porter-dev/porter/cli/cmd/utils"
  17. "github.com/spf13/cobra"
  18. corev1 "k8s.io/api/core/v1"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/runtime"
  21. "k8s.io/apimachinery/pkg/runtime/schema"
  22. "k8s.io/apimachinery/pkg/util/sets"
  23. "k8s.io/client-go/rest"
  24. "k8s.io/client-go/tools/clientcmd"
  25. "k8s.io/client-go/tools/portforward"
  26. "k8s.io/client-go/transport/spdy"
  27. "k8s.io/kubectl/pkg/util"
  28. )
  29. var address []string
  30. var portForwardCmd = &cobra.Command{
  31. Use: "port-forward [release] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]",
  32. Short: "Forward one or more local ports to a pod of a release",
  33. Args: cobra.MinimumNArgs(2),
  34. Run: func(cmd *cobra.Command, args []string) {
  35. err := checkLoginAndRun(args, portForward)
  36. if err != nil {
  37. os.Exit(1)
  38. }
  39. },
  40. }
  41. func init() {
  42. portForwardCmd.PersistentFlags().StringVar(
  43. &namespace,
  44. "namespace",
  45. "default",
  46. "namespace of the release whose pod you want to port-forward to",
  47. )
  48. portForwardCmd.Flags().StringSliceVar(
  49. &address,
  50. "address",
  51. []string{"localhost"},
  52. "Addresses to listen on (comma separated). Only accepts IP addresses or localhost as a value. "+
  53. "When localhost is supplied, kubectl will try to bind on both 127.0.0.1 and ::1 and will fail "+
  54. "if neither of these addresses are available to bind.")
  55. rootCmd.AddCommand(portForwardCmd)
  56. }
  57. func forwardPorts(
  58. method string,
  59. url *url.URL,
  60. kubeConfig *rest.Config,
  61. address, ports []string,
  62. stopChan <-chan struct{},
  63. readyChan chan struct{},
  64. ) error {
  65. transport, upgrader, err := spdy.RoundTripperFor(kubeConfig)
  66. if err != nil {
  67. return err
  68. }
  69. dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
  70. fw, err := portforward.NewOnAddresses(
  71. dialer, address, ports, stopChan, readyChan, os.Stdout, os.Stderr)
  72. if err != nil {
  73. return err
  74. }
  75. return fw.ForwardPorts()
  76. }
  77. // splitPort splits port string which is in form of [LOCAL PORT]:REMOTE PORT
  78. // and returns local and remote ports separately
  79. func splitPort(port string) (local, remote string) {
  80. parts := strings.Split(port, ":")
  81. if len(parts) == 2 {
  82. return parts[0], parts[1]
  83. }
  84. return parts[0], parts[0]
  85. }
  86. func portForward(user *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  87. var err error
  88. var pod corev1.Pod
  89. s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
  90. s.Color("cyan")
  91. s.Suffix = fmt.Sprintf(" Loading list of pods for %s", args[0])
  92. s.Start()
  93. podsResp, err := client.GetK8sAllPods(context.Background(), cliConf.Project, cliConf.Cluster, namespace, args[0])
  94. s.Stop()
  95. if err != nil {
  96. return err
  97. }
  98. pods := *podsResp
  99. if len(pods) > 1 {
  100. selectedPod, err := utils.PromptSelect("Select a pod to port-forward", func() []string {
  101. var names []string
  102. for i, pod := range pods {
  103. names = append(names, fmt.Sprintf("%d - %s", (i+1), pod.Name))
  104. }
  105. return names
  106. }())
  107. if err != nil {
  108. return err
  109. }
  110. podIdxStr := strings.Split(selectedPod, " - ")[0]
  111. podIdx, err := strconv.Atoi(podIdxStr)
  112. if err != nil {
  113. return err
  114. }
  115. pod = pods[podIdx-1]
  116. } else {
  117. pod = pods[0]
  118. }
  119. var kubeBytes []byte
  120. if cliConf.Kubeconfig == "" {
  121. kubeResp, err := client.GetKubeconfig(context.TODO(), cliConf.Project, cliConf.Cluster)
  122. if err != nil {
  123. return err
  124. }
  125. kubeBytes = kubeResp.Kubeconfig
  126. } else {
  127. file, err := os.Open(cliConf.Kubeconfig)
  128. if err != nil {
  129. return err
  130. }
  131. kubeBytes, err = io.ReadAll(file)
  132. if err != nil {
  133. return err
  134. }
  135. }
  136. cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
  137. if err != nil {
  138. return err
  139. }
  140. restConf, err := cmdConf.ClientConfig()
  141. if err != nil {
  142. return err
  143. }
  144. restConf.GroupVersion = &schema.GroupVersion{
  145. Group: "api",
  146. Version: "v1",
  147. }
  148. restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
  149. restClient, err := rest.RESTClientFor(restConf)
  150. if err != nil {
  151. return err
  152. }
  153. err = checkUDPPortInPod(args[1:], &pod)
  154. if err != nil {
  155. return err
  156. }
  157. ports, err := convertPodNamedPortToNumber(args[1:], pod)
  158. if err != nil {
  159. return err
  160. }
  161. stopChannel := make(chan struct{}, 1)
  162. readyChannel := make(chan struct{})
  163. signals := make(chan os.Signal, 1)
  164. signal.Notify(signals, os.Interrupt)
  165. defer signal.Stop(signals)
  166. go func() {
  167. <-signals
  168. if stopChannel != nil {
  169. close(stopChannel)
  170. }
  171. }()
  172. req := restClient.Post().
  173. Resource("pods").
  174. Namespace(namespace).
  175. Name(pod.Name).
  176. SubResource("portforward")
  177. return forwardPorts("POST", req.URL(), restConf, address, ports, stopChannel, readyChannel)
  178. }
  179. func checkUDPPortInPod(ports []string, pod *corev1.Pod) error {
  180. udpPorts := sets.NewInt()
  181. tcpPorts := sets.NewInt()
  182. for _, ct := range pod.Spec.Containers {
  183. for _, ctPort := range ct.Ports {
  184. portNum := int(ctPort.ContainerPort)
  185. switch ctPort.Protocol {
  186. case corev1.ProtocolUDP:
  187. udpPorts.Insert(portNum)
  188. case corev1.ProtocolTCP:
  189. tcpPorts.Insert(portNum)
  190. }
  191. }
  192. }
  193. return checkUDPPorts(udpPorts.Difference(tcpPorts), ports, pod)
  194. }
  195. func checkUDPPorts(udpOnlyPorts sets.Int, ports []string, obj metav1.Object) error {
  196. for _, port := range ports {
  197. _, remotePort := splitPort(port)
  198. portNum, err := strconv.Atoi(remotePort)
  199. if err != nil {
  200. switch v := obj.(type) {
  201. case *corev1.Service:
  202. svcPort, err := util.LookupServicePortNumberByName(*v, remotePort)
  203. if err != nil {
  204. return err
  205. }
  206. portNum = int(svcPort)
  207. case *corev1.Pod:
  208. ctPort, err := util.LookupContainerPortNumberByName(*v, remotePort)
  209. if err != nil {
  210. return err
  211. }
  212. portNum = int(ctPort)
  213. default:
  214. return fmt.Errorf("unknown object: %v", obj)
  215. }
  216. }
  217. if udpOnlyPorts.Has(portNum) {
  218. return fmt.Errorf("UDP protocol is not supported for %s", remotePort)
  219. }
  220. }
  221. return nil
  222. }
  223. func convertPodNamedPortToNumber(ports []string, pod corev1.Pod) ([]string, error) {
  224. var converted []string
  225. for _, port := range ports {
  226. localPort, remotePort := splitPort(port)
  227. containerPortStr := remotePort
  228. _, err := strconv.Atoi(remotePort)
  229. if err != nil {
  230. containerPort, err := util.LookupContainerPortNumberByName(pod, remotePort)
  231. if err != nil {
  232. return nil, err
  233. }
  234. containerPortStr = strconv.Itoa(int(containerPort))
  235. }
  236. if localPort != remotePort {
  237. converted = append(converted, fmt.Sprintf("%s:%s", localPort, containerPortStr))
  238. } else {
  239. converted = append(converted, containerPortStr)
  240. }
  241. }
  242. return converted, nil
  243. }