├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile.alpine ├── Dockerfile.ubuntu ├── LICENSE ├── NOTICE ├── README.md ├── README_CN.md ├── README_PACKAGING.md ├── SECURITY.md ├── cmd └── kubefwd │ ├── kubefwd.go │ └── services │ └── services.go ├── example.fwdconf.yml ├── go.mod ├── go.sum ├── k8s └── test-env │ ├── kf-a.yml │ └── kf-b.yml ├── kubefwd-mast2.jpg ├── kubefwd_ani.gif ├── kubefwd_sn.png └── pkg ├── fwdIp └── fwdIp.go ├── fwdcfg └── fwdcfg.go ├── fwdhost └── fwdhost.go ├── fwdnet └── fwdnet.go ├── fwdport ├── fwdport.go └── fwdport_test.go ├── fwdpub └── fwdpub.go ├── fwdservice └── fwdservice.go ├── fwdsvcregistry └── fwdsvcregistry.go └── utils ├── root.go ├── root_windows.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .vscode 4 | dist 5 | vendor 6 | 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - id: kubefwd 9 | main: ./cmd/kubefwd/kubefwd.go 10 | binary: kubefwd 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | goarch: 16 | - "386" 17 | - amd64 18 | - arm 19 | - arm64 20 | ignore: 21 | - goos: windows 22 | goarch: arm64 23 | mod_timestamp: '{{ .CommitTimestamp }}' 24 | env: 25 | - CGO_ENABLED=0 26 | flags: 27 | - -trimpath 28 | - -tags=netgo 29 | - -a 30 | - -v 31 | ldflags: -s -w -X main.Version={{.Version}} 32 | 33 | checksum: 34 | name_template: '{{ .ProjectName }}_checksums.txt' 35 | 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - '^docs:' 41 | - '^test:' 42 | - Merge pull request 43 | - Merge branch 44 | - go mod tidy 45 | 46 | dockers: 47 | - 48 | goos: linux 49 | goarch: amd64 50 | goarm: '' 51 | dockerfile: Dockerfile.alpine 52 | ids: 53 | - kubefwd 54 | image_templates: 55 | - "txn2/kubefwd:latest" 56 | - "txn2/kubefwd:{{ .Tag }}" 57 | - "txn2/kubefwd:v{{ .Major }}" 58 | - "txn2/kubefwd:amd64-{{ .Tag }}" 59 | - "txn2/kubefwd:amd64-v{{ .Major }}" 60 | - "txn2/kubefwd:latest_alpine-3" 61 | - "txn2/kubefwd:{{ .Tag }}_alpine-3" 62 | - "txn2/kubefwd:v{{ .Major }}_alpine-3" 63 | - "txn2/kubefwd:amd64-{{ .Tag }}_alpine-3" 64 | - "txn2/kubefwd:amd64-v{{ .Major }}_alpine-3" 65 | build_flag_templates: 66 | - "--label=org.label-schema.schema-version=1.0" 67 | - "--label=org.label-schema.version={{.Version}}" 68 | - "--label=org.label-schema.name={{.ProjectName}}" 69 | - 70 | goos: linux 71 | goarch: amd64 72 | goarm: '' 73 | dockerfile: Dockerfile.ubuntu 74 | ids: 75 | - kubefwd 76 | image_templates: 77 | - "txn2/kubefwd:latest_ubuntu-20.04" 78 | - "txn2/kubefwd:{{ .Tag }}_ubuntu-20.04" 79 | - "txn2/kubefwd:v{{ .Major }}_ubuntu-20.04" 80 | - "txn2/kubefwd:amd64-{{ .Tag }}_ubuntu-20.04" 81 | - "txn2/kubefwd:amd64-v{{ .Major }}_ubuntu-20.04" 82 | build_flag_templates: 83 | - "--label=org.label-schema.schema-version=1.0" 84 | - "--label=org.label-schema.version={{.Version}}" 85 | - "--label=org.label-schema.name={{.ProjectName}}" 86 | 87 | archives: 88 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 89 | replacements: 90 | darwin: Darwin 91 | linux: Linux 92 | windows: Windows 93 | 386: i386 94 | amd64: x86_64 95 | format_overrides: 96 | - goos: windows 97 | format: zip 98 | 99 | nfpms: 100 | - file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 101 | homepage: https://github.com/txn2/kubefwd 102 | description: Kubernetes bulk port forwarding utility. 103 | maintainer: Craig Johnston https://twitter.com/cjimti 104 | license: Apache 2.0 105 | vendor: https://github.com/txn2 106 | formats: 107 | - apk 108 | - deb 109 | - rpm 110 | recommends: 111 | - kubectl 112 | 113 | release: 114 | github: 115 | owner: txn2 116 | name: kubefwd 117 | name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" 118 | 119 | brews: 120 | - name: kubefwd 121 | tap: 122 | owner: txn2 123 | name: homebrew-tap 124 | commit_author: 125 | name: Craig Johnston 126 | email: cj@imti.co 127 | folder: Formula 128 | homepage: https://github.com/txn2/kubefwd 129 | description: "Kubernetes bulk port forwarding utility." 130 | skip_upload: false 131 | dependencies: 132 | - name: kubectl 133 | type: optional 134 | test: |- 135 | kubefwd version 136 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.16.x 5 | 6 | # Only clone the most recent commit. 7 | git: 8 | depth: 1 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cjimti@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM alpine:3 2 | 3 | RUN apk update \ 4 | && apk add --no-cache curl ca-certificates \ 5 | && rm -rf /var/cache/apk/* 6 | 7 | COPY kubefwd / 8 | WORKDIR / 9 | ENTRYPOINT ["/kubefwd"] 10 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | RUN apt update && apt install -y curl 4 | 5 | COPY kubefwd / 6 | 7 | WORKDIR / 8 | ENTRYPOINT ["/kubefwd"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This software is not an official Kubernetes project or endorsed by 2 | The Kubernetes Authors or The Linux Foundation. 3 | 4 | This software contains code derived from The Kubernetes Authors. 5 | MD5 Message-Digest Algorithm, including various 6 | modifications by Spyglass Inc., Carnegie Mellon University, and 7 | Bell Communications Research, Inc (Bellcore). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/txn2/kubefwd/blob/master/README.md)|[中文](https://github.com/txn2/kubefwd/blob/master/README_CN.md) 2 | 3 | Kubernetes port forwarding for local development. 4 | 5 | **NOTE:** Accepting pull requests for bug fixes, tests, and documentation only. 6 | 7 | ![kubefwd - kubernetes bulk port forwarding](https://raw.githubusercontent.com/txn2/kubefwd/master/kubefwd-mast2.jpg) 8 | 9 | [![Build Status](https://travis-ci.com/txn2/kubefwd.svg?branch=master)](https://travis-ci.com/txn2/kubefwd) 10 | [![GitHub license](https://img.shields.io/github/license/txn2/kubefwd.svg)](https://github.com/txn2/kubefwd/blob/master/LICENSE) 11 | [![Go Report Card](https://goreportcard.com/badge/github.com/txn2/kubefwd)](https://goreportcard.com/report/github.com/txn2/kubefwd) 12 | [![GitHub release](https://img.shields.io/github/release/txn2/kubefwd.svg)](https://github.com/txn2/kubefwd/releases) 13 | 14 | # kubefwd (Kube Forward) 15 | 16 | Read [Kubernetes Port Forwarding for Local Development](https://mk.imti.co/kubernetes-port-forwarding/) for background and a detailed guide to **kubefwd**. Follow [Craig Johnston](https://twitter.com/cjimti) on Twitter for project updates. 17 | 18 | **kubefwd** is a command line utility built to port forward multiple [services] within one or more [namespaces] on one or more Kubernetes clusters. **kubefwd** uses the same port exposed by the service and forwards it from a loopback IP address on your local workstation. **kubefwd** temporally adds domain entries to your `/etc/hosts` file with the service names it forwards. 19 | 20 | When working on our local workstation, my team and I often build applications that access services through their service names and ports within a [Kubernetes] namespace. **kubefwd** allows us to develop locally with services available as they would be in the cluster. 21 | 22 | ![kubefwd - Kubernetes port forward](kubefwd_ani.gif) 23 | 24 |

25 | kubefwd - Kubernetes Port Forward Diagram 26 |

27 | 28 | ## OS 29 | 30 | Tested directly on **macOS** and **Linux** based docker containers. 31 | 32 | ## MacOs Install / Update 33 | 34 | **kubefwd** assumes you have **kubectl** installed and configured with access to a Kubernetes cluster. **kubefwd** uses the **kubectl** current context. The **kubectl** configuration is not used. However, its configuration is needed to access a Kubernetes cluster. 35 | 36 | Ensure you have a context by running: 37 | ```bash 38 | kubectl config current-context 39 | ``` 40 | 41 | If you are running MacOS and use [homebrew] you can install **kubefwd** directly from the [txn2] tap: 42 | 43 | ```bash 44 | brew install txn2/tap/kubefwd 45 | ``` 46 | 47 | To upgrade: 48 | ```bash 49 | brew upgrade kubefwd 50 | ``` 51 | 52 | ## Windows Install / Update 53 | 54 | ```batch 55 | scoop install kubefwd 56 | ``` 57 | 58 | To upgrade: 59 | ```batch 60 | scoop update kubefwd 61 | ``` 62 | 63 | ## Docker 64 | 65 | Forward all services from the namespace **the-project** to a Docker container named **the-project**: 66 | 67 | ```bash 68 | docker run -it --rm --privileged --name the-project \ 69 | -v "$(echo $HOME)/.kube/":/root/.kube/ \ 70 | txn2/kubefwd services -n the-project 71 | ``` 72 | 73 | 74 | Execute a curl call to an Elasticsearch service in your Kubernetes cluster: 75 | 76 | ```bash 77 | docker exec the-project curl -s elasticsearch:9200 78 | ``` 79 | 80 | ## Alternative Installs (tar.gz, RPM, deb) 81 | Check out the [releases](https://github.com/txn2/kubefwd/releases) section on Github for alternative binaries. 82 | 83 | ## Contribute 84 | [Fork kubefwd](https://github.com/txn2/kubefwd) and build a custom version. 85 | Accepting pull requests for bug fixes, tests, stability and compatibility 86 | enhancements, and documentation only. 87 | 88 | ## Usage 89 | 90 | Forward all services for the namespace `the-project`. Kubefwd finds the first Pod associated with each Kubernetes service found in the Namespace and port forwards it based on the Service spec to a local IP address and port. A domain name is added to your /etc/hosts file pointing to the local IP. 91 | 92 | ### Update 93 | Forwarding of headlesss Service is currently supported, Kubefwd forward all Pods for headless service; At the same time, the namespace-level service monitoring is supported. When a new service is created or the old service is deleted under the namespace, kubefwd can automatically start/end forwarding; Supports Pod-level forwarding monitoring. When the forwarded Pod is deleted (such as updating the deployment, etc.), the forwarding of the service to which the pod belongs is automatically restarted; 94 | 95 | ```bash 96 | sudo kubefwd svc -n the-project 97 | ``` 98 | 99 | Forward all svc for the namespace `the-project` where labeled `system: wx`: 100 | 101 | ```bash 102 | sudo kubefwd svc -l system=wx -n the-project 103 | ``` 104 | 105 | Forward a single service named `my-service` in the namespace `the-project`: 106 | 107 | ``` 108 | sudo kubefwd svc -n the-project -f metadata.name=my-service 109 | ``` 110 | 111 | Forward more than one service using the `in` clause: 112 | ```bash 113 | sudo kubefwd svc -l "app in (app1, app2)" 114 | ``` 115 | 116 | ## Help 117 | 118 | ```bash 119 | $ kubefwd svc --help 120 | 121 | INFO[00:00:48] _ _ __ _ 122 | INFO[00:00:48] | | ___ _| |__ ___ / _|_ ____| | 123 | INFO[00:00:48] | |/ / | | | '_ \ / _ \ |_\ \ /\ / / _ | 124 | INFO[00:00:48] | <| |_| | |_) | __/ _|\ V V / (_| | 125 | INFO[00:00:48] |_|\_\\__,_|_.__/ \___|_| \_/\_/ \__,_| 126 | INFO[00:00:48] 127 | INFO[00:00:48] Version 0.0.0 128 | INFO[00:00:48] https://github.com/txn2/kubefwd 129 | INFO[00:00:48] 130 | Forward multiple Kubernetes services from one or more namespaces. Filter services with selector. 131 | 132 | Usage: 133 | kubefwd services [flags] 134 | 135 | Aliases: 136 | services, svcs, svc 137 | 138 | Examples: 139 | kubefwd svc -n the-project 140 | kubefwd svc -n the-project -l app=wx,component=api 141 | kubefwd svc -n default -l "app in (ws, api)" 142 | kubefwd svc -n default -n the-project 143 | kubefwd svc -n default -d internal.example.com 144 | kubefwd svc -n the-project -x prod-cluster 145 | kubefwd svc -n the-project -m 80:8080 -m 443:1443 146 | kubefwd svc -n the-project -z path/to/conf.yml 147 | kubefwd svc -n the-project -r svc.ns:127.3.3.1 148 | kubefwd svc --all-namespaces 149 | 150 | Flags: 151 | -A, --all-namespaces Enable --all-namespaces option like kubectl. 152 | -x, --context strings specify a context to override the current context 153 | -d, --domain string Append a pseudo domain name to generated host names. 154 | -f, --field-selector string Field selector to filter on; supports '=', '==', and '!=' (e.g. -f metadata.name=service-name). 155 | -z, --fwd-conf string Define an IP reservation configuration 156 | -h, --help help for services 157 | -c, --kubeconfig string absolute path to a kubectl config file 158 | -m, --mapping strings Specify a port mapping. Specify multiple mapping by duplicating this argument. 159 | -n, --namespace strings Specify a namespace. Specify multiple namespaces by duplicating this argument. 160 | -r, --reserve strings Specify an IP reservation. Specify multiple reservations by duplicating this argument. 161 | -l, --selector string Selector (label query) to filter on; supports '=', '==', and '!=' (e.g. -l key1=value1,key2=value2). 162 | -v, --verbose Verbose output. 163 | ``` 164 | 165 | ### License 166 | 167 | Apache License 2.0 168 | 169 | # Sponsor 170 | 171 | Open source utility by [Craig Johnston](https://twitter.com/cjimti), [imti blog](http://imti.co/) and sponsored by [Deasil Works, Inc.] 172 | 173 | Please check out my book [Advanced Platform Development with Kubernetes](https://imti.co/kubernetes-platform-book/): 174 | Enabling Data Management, the Internet of Things, Blockchain, and Machine Learning. 175 | 176 | [![Book Cover - Advanced Platform Development with Kubernetes: Enabling Data Management, the Internet of Things, Blockchain, and Machine Learning](https://raw.githubusercontent.com/apk8s/book-source/master/img/apk8s-banner-w.jpg)](https://amzn.to/3g3ihZ3) 177 | 178 | Source code from the book [Advanced Platform Development with Kubernetes: Enabling Data Management, the Internet of Things, Blockchain, and Machine Learning](https://amzn.to/3g3ihZ3) by [Craig Johnston](https://imti.co) ([@cjimti](https://twitter.com/cjimti)) ISBN 978-1-4842-5610-7 [Apress; 1st ed. edition (September, 2020)](https://www.apress.com/us/book/9781484256107) 179 | 180 | Read my blog post [Advanced Platform Development with Kubernetes](https://imti.co/kubernetes-platform-book/) for more info and background on the book. 181 | 182 | Follow me on Twitter: [@cjimti](https://twitter.com/cjimti) ([Craig Johnston](https://twitter.com/cjimti)) 183 | 184 | 185 | ## Please Help the Children of Ukraine 186 | 187 | UNICEF is on the ground helping Ukraine's children, please donate to https://www.unicefusa.org/ <- "like" this project by donating. 188 | 189 | 190 | [Kubernetes]:https://kubernetes.io/ 191 | [namespaces]:https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 192 | [services]:https://kubernetes.io/docs/concepts/services-networking/service/ 193 | [homebrew]:https://brew.sh/ 194 | [txn2]:https://txn2.com/ 195 | [Deasil Works, Inc.]:https://deasil.works/ 196 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/txn2/kubefwd/blob/master/README.md)|[中文](https://github.com/txn2/kubefwd/blob/master/README_CN.md) 2 | 3 | 实现批量端口转发让本地能方便访问远程Kubernetes服务, 欢迎贡献! 4 | 5 | ![kubefwd - kubernetes批量端口转发](https://raw.githubusercontent.com/txn2/kubefwd/master/kubefwd-mast2.jpg) 6 | 7 | [![GitHub license](https://img.shields.io/github/license/txn2/kubefwd.svg)](https://github.com/txn2/kubefwd/blob/master/LICENSE) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/bc696045260db8e0ba89/maintainability)](https://codeclimate.com/github/txn2/kubefwd/maintainability) 9 | [![Go Report Card](https://goreportcard.com/badge/github.com/txn2/kubefwd)](https://goreportcard.com/report/github.com/txn2/kubefwd) 10 | [![GitHub release](https://img.shields.io/github/release/txn2/kubefwd.svg)](https://github.com/txn2/kubefwd/releases) 11 | 12 | # kubefwd (Kube Forward) 13 | 14 | 阅读 [Kubernetes Port Forwarding for Local Development](https://mk.imti.co/kubernetes-port-forwarding/) 的背景资料和**kubefwd**的详细指南。 15 | 16 | **kubefwd** 是一个用于端口转发Kubernetes中指定namespace下的全部或者部分pod的命令行工具。 **kubefwd** 使用本地的环回IP地址转发需要访问的service,并且使用与service相同的端口。 **kubefwd** 会临时将service的域条目添加到 `/etc/hosts` 文件中。 17 | 18 | 启动**kubefwd**后,在本地就能像在Kubernetes集群中一样使用service名字与端口访问对应的应用程序。 19 | 20 | ![kubefwd - kubernetes批量端口转发](kubefwd_ani.gif) 21 | 22 |

23 | kubefwd - Kubernetes Port Forward Diagram 24 |

25 | 26 | ## OS 27 | 28 | 直接在**macOS**或者**Linux**的docker容器上测试。 29 | 30 | ## MacOs 安装 / 升级 31 | 32 | **kubefwd** 默认你已经安装了 **kubectl** 工具并且也已经设置好了访问Kubernetes集群的配置文件。**kubefwd** 使用 **kubectl** 的上下文运行环境. **kubectl** 工具并不会用到,但是它的配置文件会被用来访问Kubernetes集群。 33 | 34 | 确保你有上下文运行环境配置: 35 | ```bash 36 | kubectl config current-context 37 | ``` 38 | 39 | 如果你使用MacOs,并且安装了 [homebrew] ,那么你可以直接使用下面的命令来安装**kubefwd**: 40 | 41 | ```bash 42 | brew install txn2/tap/kubefwd 43 | ``` 44 | 45 | 升级: 46 | ```bash 47 | brew upgrade kubefwd 48 | ``` 49 | 50 | ## Windows 安装 / 升级 51 | 52 | ```batch 53 | scoop install kubefwd 54 | ``` 55 | 56 | 升级: 57 | ```batch 58 | scoop update kubefwd 59 | ``` 60 | 61 | ## Docker 62 | 63 | 将namespace为**the-project**的所有服务转发到名为**the-project**的Docker容器: 64 | 65 | ```bash 66 | docker run -it --rm --privileged --name the-project \ 67 | -v "$(echo $HOME)/.kube/":/root/.kube/ \ 68 | txn2/kubefwd services -n the-project 69 | ``` 70 | 71 | 72 | 通过curl命令访问Kubernetes集群下的Elasticsearch service : 73 | 74 | ```bash 75 | docker exec the-project curl -s elasticsearch:9200 76 | ``` 77 | 78 | ## 其它安装方式 (tar.gz, RPM, deb, snap) 79 | 查看在Github上 [releases](https://github.com/txn2/kubefwd/releases) 部分的二进制包。 80 | 81 | ## 贡献 82 | [Fork kubefwd](https://github.com/txn2/kubefwd) 并构建自定义版本。我们也非常欢迎大家贡献自己的智慧。 83 | 84 | ## 用法 85 | 86 | 转发namespace `the-project`下的所有服务。 Kubefwd找到Kubernetess集群中,该namespace下对应的Service端口匹配的第一个Pod,并将其转发到本地IP地址和端口。同时service的域名将被添加到本地的 hosts文件中。 87 | 88 | ### 更新 89 | 当前已支持headlesss Service的转发,Kubefwd将转发所有headlesss Service的Pod; 90 | 91 | 同时支持namespace级服务监听,当namespace下有新Service创建或旧Service删除时,Kubefwd能够自动完成转发/结束转发;支持Pod级转发监听,当转发的Pod被删除时(如更新deployment等情况),自动重启该pod所属Service的转发; 92 | ```bash 93 | sudo kubefwd svc -n the-project 94 | ``` 95 | 96 | 转发namespace `the-project`下所有的带有label为`system: wx`的service: 97 | 98 | ```bash 99 | sudo kubefwd svc -l system=wx -n the-project 100 | ``` 101 | 102 | ## 帮助说明 103 | 104 | ```bash 105 | $ kubefwd svc --help 106 | 107 | 2019/03/09 21:13:18 _ _ __ _ 108 | 2019/03/09 21:13:18 | | ___ _| |__ ___ / _|_ ____| | 109 | 2019/03/09 21:13:18 | |/ / | | | '_ \ / _ \ |_\ \ /\ / / _ | 110 | 2019/03/09 21:13:18 | <| |_| | |_) | __/ _|\ V V / (_| | 111 | 2019/03/09 21:13:18 |_|\_\\__,_|_.__/ \___|_| \_/\_/ \__,_| 112 | 2019/03/09 21:13:18 113 | 2019/03/09 21:13:18 Version 1.7.3 114 | 2019/03/09 21:13:18 https://github.com/txn2/kubefwd 115 | 2019/03/09 21:13:18 116 | Forward multiple Kubernetes services from one or more namespaces. Filter services with selector. 117 | 118 | Usage: 119 | kubefwd services [flags] 120 | 121 | Aliases: 122 | services, svcs, svc 123 | 124 | Examples: 125 | kubefwd svc -n the-project 126 | kubefwd svc -n the-project -l env=dev,component=api 127 | kubefwd svc -n default -l "app in (ws, api)" 128 | kubefwd svc -n default -n the-project 129 | kubefwd svc -n default -d internal.example.com 130 | kubefwd svc -n the-project -x prod-cluster 131 | kubefwd svc -n the-project -m 80:8080 -m 443:1443 132 | kubefwd svc -n the-project --all-namespaces 133 | 134 | 135 | Flags: 136 | -x, --context strings specify a context to override the current context 137 | -d, --domain string Append a pseudo domain name to generated host names. 138 | --exitonfailure Exit(1) on failure. Useful for forcing a container restart. 139 | -h, --help help for services 140 | -c, --kubeconfig string absolute path to a kubectl config fil (default "/Users/cjimti/.kube/config") 141 | -n, --namespace strings Specify a namespace. Specify multiple namespaces by duplicating this argument. 142 | -l, --selector string Selector (label query) to filter on; supports '=', '==', and '!=' (e.g. -l key1=value1,key2=value2). 143 | -m, --mapping strings Specify a port mapping. Specify multiple mapping by duplicating this argument. 144 | --all-namespaces Enable --all-namespaces or -A option like kubectl. 145 | -v, --verbose Verbose output. 146 | ``` 147 | 148 | ## 开发 149 | 150 | ### 构建并运行 151 | 本地 152 | 153 | ```bash 154 | go run ./cmd/kubefwd/kubefwd.go 155 | ``` 156 | 157 | ### 使用Docker构建并运行 158 | 159 | 使用镜像 [golang:1.11.5] 运行容器: 160 | ```bash 161 | docker run -it --rm --privileged \ 162 | -v "$(pwd)":/kubefwd \ 163 | -v "$(echo $HOME)/.kube/":/root/.kube/ \ 164 | -w /kubefwd golang:1.11.5 bash 165 | ``` 166 | 167 | ```bash 168 | sudo go run -mod vendor ./cmd/kubefwd/kubefwd.go svc 169 | ``` 170 | 171 | ### 构建版本 172 | 173 | 构建并测试: 174 | ```bash 175 | goreleaser --skip-publish --rm-dist --skip-validate 176 | ``` 177 | 178 | 构建并发布: 179 | ```bash 180 | GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist 181 | ``` 182 | 183 | ### 使用Snap测试 184 | 185 | ```bash 186 | multipass launch -n testvm 187 | cd ./dist 188 | multipass copy-files *.snap testvm: 189 | multipass shell testvm 190 | sudo snap install --dangerous kubefwd_64-bit.snap 191 | ``` 192 | 193 | 194 | ### 开源协议 195 | 196 | Apache License 2.0 197 | 198 | ### 赞助 199 | 200 | 由 [Deasil Works, Inc.] & 201 | [Craig Johnston](https://imti.co) 赞助的开源工具 202 | 203 | [Kubernetes]:https://kubernetes.io/ 204 | [Kubernetes namespace]:https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 205 | [homebrew]:https://brew.sh/ 206 | [txn2]:https://txn2.com/ 207 | [golang:1.11.5]:https://hub.docker.com/_/golang/ 208 | [Deasil Works, Inc.]:https://deasil.works/ 209 | -------------------------------------------------------------------------------- /README_PACKAGING.md: -------------------------------------------------------------------------------- 1 | # Packaging 2 | 3 | ### Build and Run 4 | 5 | ```bash 6 | go run ./cmd/kubefwd/kubefwd.go 7 | ``` 8 | 9 | ### Build Release 10 | 11 | Build test release: 12 | ```bash 13 | goreleaser --skip-publish --rm-dist --skip-validate 14 | ``` 15 | 16 | Build and release: 17 | ```bash 18 | GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist 19 | ``` 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions currently are being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.22.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please DM [@cjimti](https://twitter.com/cjimti) on Twitter. 14 | -------------------------------------------------------------------------------- /cmd/kubefwd/kubefwd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Craig Johnston 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 | package main 17 | 18 | import ( 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "os" 23 | 24 | log "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | "github.com/txn2/kubefwd/cmd/kubefwd/services" 27 | ) 28 | 29 | var globalUsage = `` 30 | var Version = "0.0.0" 31 | 32 | func init() { 33 | // quiet version 34 | args := os.Args[1:] 35 | if len(args) == 2 && args[0] == "version" && args[1] == "quiet" { 36 | fmt.Println(Version) 37 | os.Exit(0) 38 | } 39 | 40 | log.SetOutput(&LogOutputSplitter{}) 41 | if len(args) > 0 && (args[0] == "completion" || args[0] == "__complete") { 42 | log.SetOutput(io.Discard) 43 | } 44 | } 45 | 46 | func newRootCmd() *cobra.Command { 47 | cmd := &cobra.Command{ 48 | Use: "kubefwd", 49 | Short: "Expose Kubernetes services for local development.", 50 | Example: " kubefwd services --help\n" + 51 | " kubefwd svc -n the-project\n" + 52 | " kubefwd svc -n the-project -l env=dev,component=api\n" + 53 | " kubefwd svc -n the-project -f metadata.name=service-name\n" + 54 | " kubefwd svc -n default -l \"app in (ws, api)\"\n" + 55 | " kubefwd svc -n default -n the-project\n" + 56 | " kubefwd svc -n the-project -m 80:8080 -m 443:1443\n" + 57 | " kubefwd svc -n the-project -z path/to/conf.yml\n" + 58 | " kubefwd svc -n the-project -r svc.ns:127.3.3.1\n" + 59 | " kubefwd svc --all-namespaces", 60 | 61 | Long: globalUsage, 62 | } 63 | 64 | versionCmd := &cobra.Command{ 65 | Use: "version", 66 | Short: "Print the version of Kubefwd", 67 | Example: " kubefwd version\n" + 68 | " kubefwd version quiet\n", 69 | Long: ``, 70 | Run: func(cmd *cobra.Command, args []string) { 71 | fmt.Printf("Kubefwd version: %s\nhttps://github.com/txn2/kubefwd\n", Version) 72 | }, 73 | } 74 | 75 | cmd.AddCommand(versionCmd, services.Cmd) 76 | 77 | return cmd 78 | } 79 | 80 | type LogOutputSplitter struct{} 81 | 82 | func (splitter *LogOutputSplitter) Write(p []byte) (n int, err error) { 83 | if bytes.Contains(p, []byte("level=error")) || bytes.Contains(p, []byte("level=warn")) { 84 | return os.Stderr.Write(p) 85 | } 86 | return os.Stdout.Write(p) 87 | } 88 | 89 | func main() { 90 | 91 | log.SetFormatter(&log.TextFormatter{ 92 | FullTimestamp: true, 93 | ForceColors: true, 94 | TimestampFormat: "15:04:05", 95 | }) 96 | 97 | log.Print(` _ _ __ _`) 98 | log.Print(`| | ___ _| |__ ___ / _|_ ____| |`) 99 | log.Print(`| |/ / | | | '_ \ / _ \ |_\ \ /\ / / _ |`) 100 | log.Print(`| <| |_| | |_) | __/ _|\ V V / (_| |`) 101 | log.Print(`|_|\_\\__,_|_.__/ \___|_| \_/\_/ \__,_|`) 102 | log.Print("") 103 | log.Printf("Version %s", Version) 104 | log.Print("https://github.com/txn2/kubefwd") 105 | log.Print("") 106 | 107 | cmd := newRootCmd() 108 | 109 | if err := cmd.Execute(); err != nil { 110 | os.Exit(1) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/kubefwd/services/services.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Craig Johnston 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 services 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "os/signal" 24 | "strings" 25 | "sync" 26 | "syscall" 27 | "time" 28 | 29 | "github.com/bep/debounce" 30 | "github.com/txn2/kubefwd/pkg/fwdcfg" 31 | "github.com/txn2/kubefwd/pkg/fwdhost" 32 | "github.com/txn2/kubefwd/pkg/fwdport" 33 | "github.com/txn2/kubefwd/pkg/fwdservice" 34 | "github.com/txn2/kubefwd/pkg/fwdsvcregistry" 35 | "github.com/txn2/kubefwd/pkg/utils" 36 | "github.com/txn2/txeh" 37 | 38 | log "github.com/sirupsen/logrus" 39 | "github.com/spf13/cobra" 40 | authorizationv1 "k8s.io/api/authorization/v1" 41 | v1 "k8s.io/api/core/v1" 42 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 43 | "k8s.io/apimachinery/pkg/labels" 44 | "k8s.io/apimachinery/pkg/runtime" 45 | utilRuntime "k8s.io/apimachinery/pkg/util/runtime" 46 | "k8s.io/apimachinery/pkg/watch" 47 | "k8s.io/client-go/kubernetes" 48 | _ "k8s.io/client-go/plugin/pkg/client/auth" 49 | restclient "k8s.io/client-go/rest" 50 | "k8s.io/client-go/tools/cache" 51 | ) 52 | 53 | // cmdline arguments 54 | var namespaces []string 55 | var contexts []string 56 | var verbose bool 57 | var domain string 58 | var mappings []string 59 | var isAllNs bool 60 | var fwdConfigurationPath string 61 | var fwdReservations []string 62 | var timeout int 63 | 64 | func init() { 65 | // override error output from k8s.io/apimachinery/pkg/util/runtime 66 | utilRuntime.ErrorHandlers[0] = func(err error) { 67 | // "broken pipe" see: https://github.com/kubernetes/kubernetes/issues/74551 68 | log.Errorf("Runtime: %s", err.Error()) 69 | } 70 | 71 | Cmd.Flags().StringP("kubeconfig", "c", "", "absolute path to a kubectl config file") 72 | Cmd.Flags().StringSliceVarP(&contexts, "context", "x", []string{}, "specify a context to override the current context") 73 | Cmd.Flags().StringSliceVarP(&namespaces, "namespace", "n", []string{}, "Specify a namespace. Specify multiple namespaces by duplicating this argument.") 74 | Cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on; supports '=', '==', and '!=' (e.g. -l key1=value1,key2=value2).") 75 | Cmd.Flags().StringP("field-selector", "f", "", "Field selector to filter on; supports '=', '==', and '!=' (e.g. -f metadata.name=service-name).") 76 | Cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output.") 77 | Cmd.Flags().StringVarP(&domain, "domain", "d", "", "Append a pseudo domain name to generated host names.") 78 | Cmd.Flags().StringSliceVarP(&mappings, "mapping", "m", []string{}, "Specify a port mapping. Specify multiple mapping by duplicating this argument.") 79 | Cmd.Flags().BoolVarP(&isAllNs, "all-namespaces", "A", false, "Enable --all-namespaces option like kubectl.") 80 | Cmd.Flags().StringSliceVarP(&fwdReservations, "reserve", "r", []string{}, "Specify an IP reservation. Specify multiple reservations by duplicating this argument.") 81 | Cmd.Flags().StringVarP(&fwdConfigurationPath, "fwd-conf", "z", "", "Define an IP reservation configuration") 82 | Cmd.Flags().IntVarP(&timeout, "timeout", "t", 300, "Specify a timeout seconds for the port forwarding.") 83 | 84 | } 85 | 86 | var Cmd = &cobra.Command{ 87 | Use: "services", 88 | Aliases: []string{"svcs", "svc"}, 89 | Short: "Forward services", 90 | Long: `Forward multiple Kubernetes services from one or more namespaces. Filter services with selector.`, 91 | Example: " kubefwd svc -n the-project\n" + 92 | " kubefwd svc -n the-project -l app=wx,component=api\n" + 93 | " kubefwd svc -n default -l \"app in (ws, api)\"\n" + 94 | " kubefwd svc -n default -n the-project\n" + 95 | " kubefwd svc -n default -d internal.example.com\n" + 96 | " kubefwd svc -n the-project -x prod-cluster\n" + 97 | " kubefwd svc -n the-project -m 80:8080 -m 443:1443\n" + 98 | " kubefwd svc -n the-project -z path/to/conf.yml\n" + 99 | " kubefwd svc -n the-project -r svc.ns:127.3.3.1\n" + 100 | " kubefwd svc --all-namespaces", 101 | Run: runCmd, 102 | } 103 | 104 | // setAllNamespace Form V1Core get all namespace 105 | func setAllNamespace(clientSet *kubernetes.Clientset, options metav1.ListOptions, namespaces *[]string) { 106 | nsList, err := clientSet.CoreV1().Namespaces().List(context.TODO(), options) 107 | if err != nil { 108 | log.Fatalf("Error get all namespaces by CoreV1: %s\n", err.Error()) 109 | } 110 | if nsList == nil { 111 | log.Warn("No namespaces returned.") 112 | return 113 | } 114 | 115 | for _, ns := range nsList.Items { 116 | *namespaces = append(*namespaces, ns.Name) 117 | } 118 | } 119 | 120 | // checkConnection tests if you can connect to the cluster in your config, 121 | // and if you have the necessary permissions to use kubefwd. 122 | func checkConnection(clientSet *kubernetes.Clientset, namespaces []string) error { 123 | // Check simple connectivity: can you connect to the api server 124 | _, err := clientSet.Discovery().ServerVersion() 125 | if err != nil { 126 | return err 127 | } 128 | 129 | // Check RBAC permissions for each of the requested namespaces 130 | requiredPermissions := []authorizationv1.ResourceAttributes{ 131 | {Verb: "list", Resource: "pods"}, {Verb: "get", Resource: "pods"}, {Verb: "watch", Resource: "pods"}, 132 | {Verb: "get", Resource: "services"}, 133 | } 134 | for _, namespace := range namespaces { 135 | for _, perm := range requiredPermissions { 136 | perm.Namespace = namespace 137 | var accessReview = &authorizationv1.SelfSubjectAccessReview{ 138 | Spec: authorizationv1.SelfSubjectAccessReviewSpec{ 139 | ResourceAttributes: &perm, 140 | }, 141 | } 142 | accessReview, err = clientSet.AuthorizationV1().SelfSubjectAccessReviews().Create(context.TODO(), accessReview, metav1.CreateOptions{}) 143 | if err != nil { 144 | return err 145 | } 146 | if !accessReview.Status.Allowed { 147 | return fmt.Errorf("missing RBAC permission: %v", perm) 148 | } 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func runCmd(cmd *cobra.Command, _ []string) { 156 | 157 | if verbose { 158 | log.SetLevel(log.DebugLevel) 159 | } 160 | 161 | hasRoot, err := utils.CheckRoot() 162 | 163 | if !hasRoot { 164 | log.Errorf(` 165 | This program requires superuser privileges to run. These 166 | privileges are required to add IP address aliases to your 167 | loopback interface. Superuser privileges are also needed 168 | to listen on low port numbers for these IP addresses. 169 | 170 | Try: 171 | - sudo -E kubefwd services (Unix) 172 | - Running a shell with administrator rights (Windows) 173 | 174 | `) 175 | if err != nil { 176 | log.Fatalf("Root check failure: %s", err.Error()) 177 | } 178 | return 179 | } 180 | 181 | log.Println("Press [Ctrl-C] to stop forwarding.") 182 | log.Println("'cat /etc/hosts' to see all host entries.") 183 | 184 | hostFile, err := txeh.NewHostsDefault() 185 | if err != nil { 186 | log.Fatalf("HostFile error: %s", err.Error()) 187 | } 188 | 189 | log.Printf("Loaded hosts file %s\n", hostFile.ReadFilePath) 190 | 191 | msg, err := fwdhost.BackupHostFile(hostFile) 192 | if err != nil { 193 | log.Fatalf("Error backing up hostfile: %s\n", err.Error()) 194 | } 195 | 196 | log.Printf("HostFile management: %s", msg) 197 | 198 | if domain != "" { 199 | log.Printf("Adding custom domain %s to all forwarded entries\n", domain) 200 | } 201 | 202 | // if sudo -E is used and the KUBECONFIG environment variable is set 203 | // it's easy to merge with kubeconfig files in env automatic. 204 | // if KUBECONFIG is blank, ToRawKubeConfigLoader() will use the 205 | // default kubeconfig file in $HOME/.kube/config 206 | cfgFilePath := "" 207 | 208 | // if we set the option --kubeconfig, It will have a higher priority 209 | // than KUBECONFIG environment. so it will override the KubeConfig options. 210 | flagCfgFilePath := cmd.Flag("kubeconfig").Value.String() 211 | if flagCfgFilePath != "" { 212 | cfgFilePath = flagCfgFilePath 213 | } 214 | 215 | // create a ConfigGetter 216 | configGetter := fwdcfg.NewConfigGetter() 217 | // build the ClientConfig 218 | rawConfig, err := configGetter.GetClientConfig(cfgFilePath) 219 | if err != nil { 220 | log.Fatalf("Error in get rawConfig: %s\n", err.Error()) 221 | } 222 | 223 | // labels selector to filter services 224 | // see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ 225 | listOptions := metav1.ListOptions{} 226 | listOptions.LabelSelector = cmd.Flag("selector").Value.String() 227 | listOptions.FieldSelector = cmd.Flag("field-selector").Value.String() 228 | 229 | // if no namespaces were specified via the flags, check config from the k8s context 230 | // then explicitly set one to "default" 231 | if len(namespaces) < 1 { 232 | namespaces = []string{"default"} 233 | x := rawConfig.CurrentContext 234 | // use the first context if specified 235 | if len(contexts) > 0 { 236 | x = contexts[0] 237 | } 238 | 239 | for ctxName, ctxConfig := range rawConfig.Contexts { 240 | if ctxName == x { 241 | if ctxConfig.Namespace != "" { 242 | log.Printf("Using namespace %s from current context %s.", ctxConfig.Namespace, ctxName) 243 | namespaces = []string{ctxConfig.Namespace} 244 | break 245 | } 246 | } 247 | } 248 | } 249 | 250 | stopListenCh := make(chan struct{}) 251 | 252 | // Listen for shutdown signal from user 253 | go func() { 254 | sigint := make(chan os.Signal, 1) 255 | signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) 256 | defer func() { 257 | signal.Stop(sigint) 258 | }() 259 | <-sigint 260 | log.Infof("Received shutdown signal") 261 | close(stopListenCh) 262 | }() 263 | 264 | // if no context override 265 | if len(contexts) < 1 { 266 | contexts = append(contexts, rawConfig.CurrentContext) 267 | } 268 | 269 | fwdsvcregistry.Init(stopListenCh) 270 | 271 | nsWatchesDone := &sync.WaitGroup{} // We'll wait on this to exit the program. Done() indicates that all namespace watches have shutdown cleanly. 272 | 273 | for i, ctx := range contexts { 274 | // k8s REST config 275 | restConfig, err := configGetter.GetRestConfig(cfgFilePath, ctx) 276 | if err != nil { 277 | log.Fatalf("Error generating REST configuration: %s\n", err.Error()) 278 | } 279 | 280 | // create the k8s clientSet 281 | clientSet, err := kubernetes.NewForConfig(restConfig) 282 | if err != nil { 283 | log.Fatalf("Error creating k8s clientSet: %s\n", err.Error()) 284 | } 285 | 286 | // if use --all-namespace ,from v1 api get all ns. 287 | if isAllNs { 288 | if len(namespaces) > 1 { 289 | log.Fatalf("Error: cannot combine options --all-namespaces and -n.") 290 | } 291 | setAllNamespace(clientSet, listOptions, &namespaces) 292 | } 293 | 294 | // check connectivity 295 | err = checkConnection(clientSet, namespaces) 296 | if err != nil { 297 | log.Fatalf("Error connecting to k8s cluster: %s\n", err.Error()) 298 | } 299 | log.Infof("Successfully connected context: %v", ctx) 300 | 301 | // create the k8s RESTclient 302 | restClient, err := configGetter.GetRESTClient() 303 | if err != nil { 304 | log.Fatalf("Error creating k8s RestClient: %s\n", err.Error()) 305 | } 306 | 307 | for ii, namespace := range namespaces { 308 | nsWatchesDone.Add(1) 309 | 310 | nameSpaceOpts := NamespaceOpts{ 311 | ClientSet: *clientSet, 312 | Context: ctx, 313 | Namespace: namespace, 314 | 315 | // For parallelization of ip handout, 316 | // each cluster and namespace has its own ip range 317 | NamespaceIPLock: &sync.Mutex{}, 318 | ListOptions: listOptions, 319 | HostFile: &fwdport.HostFileWithLock{Hosts: hostFile}, 320 | ClientConfig: *restConfig, 321 | RESTClient: *restClient, 322 | ClusterN: i, 323 | NamespaceN: ii, 324 | Domain: domain, 325 | ManualStopChannel: stopListenCh, 326 | PortMapping: mappings, 327 | } 328 | 329 | go func(npo NamespaceOpts) { 330 | nameSpaceOpts.watchServiceEvents(stopListenCh) 331 | nsWatchesDone.Done() 332 | }(nameSpaceOpts) 333 | } 334 | } 335 | 336 | nsWatchesDone.Wait() 337 | log.Debugf("All namespace watchers are done") 338 | 339 | // Shutdown all active services 340 | <-fwdsvcregistry.Done() 341 | 342 | log.Infof("Clean exit") 343 | } 344 | 345 | type NamespaceOpts struct { 346 | NamespaceIPLock *sync.Mutex 347 | ListOptions metav1.ListOptions 348 | HostFile *fwdport.HostFileWithLock 349 | 350 | ClientSet kubernetes.Clientset 351 | ClientConfig restclient.Config 352 | RESTClient restclient.RESTClient 353 | 354 | // Context is a unique key (string) in kubectl config representing 355 | // a user/cluster combination. Kubefwd uses context as the 356 | // cluster name when forwarding to more than one cluster. 357 | Context string 358 | 359 | // Namespace is the current Kubernetes Namespace to locate services 360 | // and the pods that back them for port-forwarding 361 | Namespace string 362 | 363 | // ClusterN is the ordinal index of the cluster (from configuration) 364 | // cluster 0 is considered local while > 0 is remote 365 | ClusterN int 366 | 367 | // NamespaceN is the ordinal index of the namespace from the 368 | // perspective of the user. Namespace 0 is considered local 369 | // while > 0 is an external namespace 370 | NamespaceN int 371 | 372 | // Domain is specified by the user and used in place of .local 373 | Domain string 374 | // meaning any source port maps to target port. 375 | PortMapping []string 376 | 377 | ManualStopChannel chan struct{} 378 | } 379 | 380 | // watchServiceEvents sets up event handlers to act on service-related events. 381 | func (opts *NamespaceOpts) watchServiceEvents(stopListenCh <-chan struct{}) { 382 | // Apply filtering 383 | optionsModifier := func(options *metav1.ListOptions) { 384 | options.FieldSelector = opts.ListOptions.FieldSelector 385 | options.LabelSelector = opts.ListOptions.LabelSelector 386 | } 387 | 388 | // Construct the informer object which will query the api server, 389 | // and send events to our handler functions 390 | // https://engineering.bitnami.com/articles/kubewatch-an-example-of-kubernetes-custom-controller.html 391 | _, controller := cache.NewInformer( 392 | &cache.ListWatch{ 393 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 394 | optionsModifier(&options) 395 | return opts.ClientSet.CoreV1().Services(opts.Namespace).List(context.TODO(), options) 396 | }, 397 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 398 | options.Watch = true 399 | optionsModifier(&options) 400 | return opts.ClientSet.CoreV1().Services(opts.Namespace).Watch(context.TODO(), options) 401 | }, 402 | }, 403 | &v1.Service{}, 404 | 0, 405 | cache.ResourceEventHandlerFuncs{ 406 | AddFunc: opts.AddServiceHandler, 407 | DeleteFunc: opts.DeleteServiceHandler, 408 | UpdateFunc: opts.UpdateServiceHandler, 409 | }, 410 | ) 411 | 412 | // Start the informer, blocking call until we receive a stop signal 413 | controller.Run(stopListenCh) 414 | log.Infof("Stopped watching Service events in namespace %s in %s context", opts.Namespace, opts.Context) 415 | } 416 | 417 | // AddServiceHandler is the event handler for when a new service comes in from k8s 418 | // (the initial list of services will also be coming in using this event for each). 419 | func (opts *NamespaceOpts) AddServiceHandler(obj interface{}) { 420 | svc, ok := obj.(*v1.Service) 421 | if !ok { 422 | return 423 | } 424 | 425 | // Check if service has a valid config to do forwarding 426 | selector := labels.Set(svc.Spec.Selector).AsSelector().String() 427 | if selector == "" { 428 | log.Warnf("WARNING: No Pod selector for service %s.%s, skipping\n", svc.Name, svc.Namespace) 429 | return 430 | } 431 | 432 | // Define a service to forward 433 | svcfwd := &fwdservice.ServiceFWD{ 434 | ClientSet: opts.ClientSet, 435 | Context: opts.Context, 436 | Namespace: opts.Namespace, 437 | Timeout: timeout, 438 | Hostfile: opts.HostFile, 439 | ClientConfig: opts.ClientConfig, 440 | RESTClient: opts.RESTClient, 441 | NamespaceN: opts.NamespaceN, 442 | ClusterN: opts.ClusterN, 443 | Domain: opts.Domain, 444 | PodLabelSelector: selector, 445 | NamespaceServiceLock: opts.NamespaceIPLock, 446 | Svc: svc, 447 | Headless: svc.Spec.ClusterIP == "None", 448 | PortForwards: make(map[string]*fwdport.PortForwardOpts), 449 | SyncDebouncer: debounce.New(5 * time.Second), 450 | DoneChannel: make(chan struct{}), 451 | PortMap: opts.ParsePortMap(mappings), 452 | ForwardConfigurationPath: fwdConfigurationPath, 453 | ForwardIPReservations: fwdReservations, 454 | } 455 | 456 | // Add the service to the catalog of services being forwarded 457 | fwdsvcregistry.Add(svcfwd) 458 | } 459 | 460 | // DeleteServiceHandler is the event handler for when a service gets deleted in k8s. 461 | func (opts *NamespaceOpts) DeleteServiceHandler(obj interface{}) { 462 | svc, ok := obj.(*v1.Service) 463 | if !ok { 464 | return 465 | } 466 | 467 | // If we are currently forwarding this service, shut it down. 468 | fwdsvcregistry.RemoveByName(svc.Name + "." + svc.Namespace + "." + opts.Context) 469 | } 470 | 471 | // UpdateServiceHandler is the event handler to deal with service changes from k8s. 472 | // It currently does not do anything. 473 | func (opts *NamespaceOpts) UpdateServiceHandler(_ interface{}, new interface{}) { 474 | key, err := cache.MetaNamespaceKeyFunc(new) 475 | if err == nil { 476 | log.Printf("update service %s.", key) 477 | } 478 | } 479 | 480 | // ParsePortMap parse string port to PortMap 481 | func (opts *NamespaceOpts) ParsePortMap(mappings []string) *[]fwdservice.PortMap { 482 | var portList []fwdservice.PortMap 483 | if mappings == nil { 484 | return nil 485 | } 486 | for _, s := range mappings { 487 | portInfo := strings.Split(s, ":") 488 | portList = append(portList, fwdservice.PortMap{SourcePort: portInfo[0], TargetPort: portInfo[1]}) 489 | } 490 | return &portList 491 | } 492 | -------------------------------------------------------------------------------- /example.fwdconf.yml: -------------------------------------------------------------------------------- 1 | baseUnreservedIP: 127.1.27.1 2 | serviceConfigurations: 3 | - # name of the target service 4 | # this will match a service in the first namespace 5 | name: myservice 6 | # ip address to be bound to 7 | ip: 127.1.28.1 8 | - # you may utilize any of the following formats to identify a service 9 | # examples: 10 | # pod 11 | # service 12 | # service.svc 13 | # pod.service 14 | # pod.context 15 | # service.context 16 | # pod.namespace 17 | # pod.namespace.svc 18 | # pod.namespace.svc.cluster.local 19 | # service.namespace 20 | # service.namespace.svc 21 | # service.namespace.svc.cluster.local 22 | # pod.service.namespace 23 | # pod.service.namespace.svc 24 | # pod.service.namespace.svc.cluster.local 25 | # pod.service.context 26 | # pod.namespace.context 27 | # pod.namespace.svc.context 28 | # pod.namespace.svc.cluster.context 29 | # service.namespace.context 30 | # service.namespace.svc.context 31 | # service.namespace.svc.cluster.context 32 | # pod.service.namespace.context 33 | # pod.service.namespace.svc.context 34 | # pod.service.namespace.svc.cluster.context 35 | name: your-svc.your-namespace 36 | ip: 127.1.28.2 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/txn2/kubefwd 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bep/debounce v1.2.0 7 | github.com/pkg/errors v0.9.1 8 | github.com/sirupsen/logrus v1.8.1 9 | github.com/spf13/cobra v1.4.0 10 | github.com/txn2/txeh v1.3.0 11 | golang.org/x/sys v0.31.0 12 | gopkg.in/yaml.v2 v2.4.0 13 | k8s.io/api v0.23.5 14 | k8s.io/apimachinery v0.23.5 15 | k8s.io/cli-runtime v0.23.5 16 | k8s.io/client-go v0.23.5 17 | k8s.io/kubectl v0.23.5 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go v0.81.0 // indirect 22 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 23 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 24 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 25 | github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect 26 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 27 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 28 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 29 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect 30 | github.com/PuerkitoBio/purell v1.1.1 // indirect 31 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 32 | github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 35 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 36 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect 37 | github.com/go-errors/errors v1.0.1 // indirect 38 | github.com/go-logr/logr v1.2.0 // indirect 39 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 40 | github.com/go-openapi/jsonreference v0.19.5 // indirect 41 | github.com/go-openapi/swag v0.19.14 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang/protobuf v1.5.2 // indirect 44 | github.com/google/btree v1.0.1 // indirect 45 | github.com/google/go-cmp v0.5.5 // indirect 46 | github.com/google/gofuzz v1.1.0 // indirect 47 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 48 | github.com/google/uuid v1.1.2 // indirect 49 | github.com/googleapis/gnostic v0.5.5 // indirect 50 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 51 | github.com/imdario/mergo v0.3.5 // indirect 52 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 56 | github.com/mailru/easyjson v0.7.6 // indirect 57 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 58 | github.com/moby/spdystream v0.2.0 // indirect 59 | github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 | github.com/modern-go/reflect2 v1.0.2 // indirect 62 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 63 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 64 | github.com/pmezard/go-difflib v1.0.0 // indirect 65 | github.com/russross/blackfriday v1.5.2 // indirect 66 | github.com/spf13/pflag v1.0.5 // indirect 67 | github.com/stretchr/testify v1.7.0 // indirect 68 | github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect 69 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 70 | golang.org/x/crypto v0.36.0 // indirect 71 | golang.org/x/net v0.38.0 // indirect 72 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect 73 | golang.org/x/term v0.30.0 // indirect 74 | golang.org/x/text v0.23.0 // indirect 75 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 76 | google.golang.org/appengine v1.6.7 // indirect 77 | google.golang.org/protobuf v1.33.0 // indirect 78 | gopkg.in/inf.v0 v0.9.1 // indirect 79 | gopkg.in/yaml.v3 v3.0.0 // indirect 80 | k8s.io/component-base v0.23.5 // indirect 81 | k8s.io/klog/v2 v2.30.0 // indirect 82 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 83 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 84 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 85 | sigs.k8s.io/kustomize/api v0.10.1 // indirect 86 | sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect 87 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 88 | sigs.k8s.io/yaml v1.2.0 // indirect 89 | ) 90 | -------------------------------------------------------------------------------- /k8s/test-env/kf-a.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kf-a 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: ok 10 | namespace: kf-a 11 | spec: 12 | selector: 13 | app: ok 14 | component: deployment 15 | ports: 16 | - protocol: "TCP" 17 | port: 8080 18 | targetPort: http-web 19 | type: ClusterIP 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: ok-headless 25 | namespace: kf-a 26 | spec: 27 | selector: 28 | app: ok 29 | component: deployment 30 | ports: 31 | - protocol: "TCP" 32 | port: 8080 33 | targetPort: http-web 34 | type: ClusterIP 35 | clusterIP: None 36 | --- 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: ok 41 | namespace: kf-a 42 | labels: 43 | app: ok 44 | spec: 45 | replicas: 5 46 | revisionHistoryLimit: 1 47 | selector: 48 | matchLabels: 49 | app: ok 50 | template: 51 | metadata: 52 | labels: 53 | app: ok 54 | component: deployment 55 | spec: 56 | containers: 57 | - name: ok 58 | image: txn2/ok:3.0.0 59 | imagePullPolicy: IfNotPresent # IfNotPresent for production 60 | env: 61 | - name: "MESSAGE" 62 | value: "ok deployment in kf-a" 63 | - name: PORT 64 | value: "8080" 65 | - name: NODE_NAME 66 | valueFrom: 67 | fieldRef: 68 | fieldPath: spec.nodeName 69 | - name: POD_NAME 70 | valueFrom: 71 | fieldRef: 72 | fieldPath: metadata.name 73 | - name: POD_NAMESPACE 74 | valueFrom: 75 | fieldRef: 76 | fieldPath: metadata.namespace 77 | - name: POD_IP 78 | valueFrom: 79 | fieldRef: 80 | fieldPath: status.podIP 81 | - name: SERVICE_ACCOUNT 82 | valueFrom: 83 | fieldRef: 84 | fieldPath: spec.serviceAccountName 85 | ports: 86 | - name: http-web 87 | containerPort: 8080 88 | resources: 89 | requests: 90 | cpu: 100m 91 | memory: 16Mi 92 | limits: 93 | cpu: 200m 94 | memory: 32Mi 95 | --- 96 | apiVersion: v1 97 | kind: Service 98 | metadata: 99 | name: ok-ss 100 | namespace: kf-a 101 | spec: 102 | selector: 103 | app: ok-ss 104 | component: ss 105 | ports: 106 | - name: http-web 107 | protocol: "TCP" 108 | port: 8080 109 | targetPort: http-web 110 | - name: http-web2 111 | protocol: "TCP" 112 | port: 8081 113 | targetPort: http-web2 114 | type: ClusterIP 115 | --- 116 | apiVersion: v1 117 | kind: Service 118 | metadata: 119 | name: ok-ss-headless 120 | namespace: kf-a 121 | spec: 122 | selector: 123 | app: ok-ss 124 | component: ss 125 | ports: 126 | - name: http-web 127 | protocol: "TCP" 128 | port: 8080 129 | targetPort: http-web 130 | - name: http-web2 131 | protocol: "TCP" 132 | port: 8081 133 | targetPort: http-web2 134 | type: ClusterIP 135 | clusterIP: None 136 | --- 137 | apiVersion: apps/v1 138 | kind: StatefulSet 139 | metadata: 140 | name: ok-ss 141 | namespace: kf-a 142 | labels: 143 | app: ok-ss 144 | spec: 145 | replicas: 3 146 | revisionHistoryLimit: 1 147 | serviceName: ok-ss 148 | selector: 149 | matchLabels: 150 | app: ok-ss 151 | component: ss 152 | template: 153 | metadata: 154 | labels: 155 | app: ok-ss 156 | component: ss 157 | spec: 158 | containers: 159 | - name: ok 160 | image: txn2/ok:3.0.0 161 | imagePullPolicy: IfNotPresent # IfNotPresent for production 162 | env: 163 | - name: "MESSAGE" 164 | value: "ok ss in kf-a" 165 | - name: PORT 166 | value: "8080" 167 | - name: NODE_NAME 168 | valueFrom: 169 | fieldRef: 170 | fieldPath: spec.nodeName 171 | - name: POD_NAME 172 | valueFrom: 173 | fieldRef: 174 | fieldPath: metadata.name 175 | - name: POD_NAMESPACE 176 | valueFrom: 177 | fieldRef: 178 | fieldPath: metadata.namespace 179 | - name: POD_IP 180 | valueFrom: 181 | fieldRef: 182 | fieldPath: status.podIP 183 | - name: SERVICE_ACCOUNT 184 | valueFrom: 185 | fieldRef: 186 | fieldPath: spec.serviceAccountName 187 | ports: 188 | - name: http-web 189 | containerPort: 8080 190 | resources: 191 | requests: 192 | cpu: 100m 193 | memory: 16Mi 194 | limits: 195 | cpu: 200m 196 | memory: 32Mi 197 | - name: ok2 198 | image: txn2/ok:3.0.0 199 | imagePullPolicy: IfNotPresent # IfNotPresent for production 200 | env: 201 | - name: "MESSAGE" 202 | value: "ok2 container ss in kf-a" 203 | - name: PORT 204 | value: "8081" 205 | - name: NODE_NAME 206 | valueFrom: 207 | fieldRef: 208 | fieldPath: spec.nodeName 209 | - name: POD_NAME 210 | valueFrom: 211 | fieldRef: 212 | fieldPath: metadata.name 213 | - name: POD_NAMESPACE 214 | valueFrom: 215 | fieldRef: 216 | fieldPath: metadata.namespace 217 | - name: POD_IP 218 | valueFrom: 219 | fieldRef: 220 | fieldPath: status.podIP 221 | - name: SERVICE_ACCOUNT 222 | valueFrom: 223 | fieldRef: 224 | fieldPath: spec.serviceAccountName 225 | ports: 226 | - name: http-web2 227 | containerPort: 8081 228 | resources: 229 | requests: 230 | cpu: 100m 231 | memory: 16Mi 232 | limits: 233 | cpu: 200m 234 | memory: 32Mi -------------------------------------------------------------------------------- /k8s/test-env/kf-b.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kf-b 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: ok 10 | namespace: kf-b 11 | spec: 12 | selector: 13 | app: ok 14 | component: deployment 15 | ports: 16 | - protocol: "TCP" 17 | port: 8080 18 | targetPort: http-web 19 | type: ClusterIP 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: ok-headless 25 | namespace: kf-b 26 | spec: 27 | selector: 28 | app: ok 29 | component: deployment 30 | ports: 31 | - protocol: "TCP" 32 | port: 8080 33 | targetPort: http-web 34 | type: ClusterIP 35 | clusterIP: None 36 | --- 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: ok 41 | namespace: kf-b 42 | labels: 43 | app: ok 44 | spec: 45 | replicas: 5 46 | revisionHistoryLimit: 1 47 | selector: 48 | matchLabels: 49 | app: ok 50 | template: 51 | metadata: 52 | labels: 53 | app: ok 54 | component: deployment 55 | spec: 56 | containers: 57 | - name: ok 58 | image: txn2/ok:3.0.0 59 | imagePullPolicy: IfNotPresent # IfNotPresent for production 60 | env: 61 | - name: "MESSAGE" 62 | value: "ok deployment in kf-b" 63 | - name: PORT 64 | value: "8080" 65 | - name: NODE_NAME 66 | valueFrom: 67 | fieldRef: 68 | fieldPath: spec.nodeName 69 | - name: POD_NAME 70 | valueFrom: 71 | fieldRef: 72 | fieldPath: metadata.name 73 | - name: POD_NAMESPACE 74 | valueFrom: 75 | fieldRef: 76 | fieldPath: metadata.namespace 77 | - name: POD_IP 78 | valueFrom: 79 | fieldRef: 80 | fieldPath: status.podIP 81 | - name: SERVICE_ACCOUNT 82 | valueFrom: 83 | fieldRef: 84 | fieldPath: spec.serviceAccountName 85 | ports: 86 | - name: http-web 87 | containerPort: 8080 88 | resources: 89 | requests: 90 | cpu: 100m 91 | memory: 16Mi 92 | limits: 93 | cpu: 200m 94 | memory: 32Mi 95 | --- 96 | apiVersion: v1 97 | kind: Service 98 | metadata: 99 | name: ok-ss 100 | namespace: kf-b 101 | spec: 102 | selector: 103 | app: ok-ss 104 | component: ss 105 | ports: 106 | - name: http-web 107 | protocol: "TCP" 108 | port: 8080 109 | targetPort: http-web 110 | - name: http-web2 111 | protocol: "TCP" 112 | port: 8081 113 | targetPort: http-web2 114 | type: ClusterIP 115 | --- 116 | apiVersion: v1 117 | kind: Service 118 | metadata: 119 | name: ok-ss-headless 120 | namespace: kf-b 121 | spec: 122 | selector: 123 | app: ok-ss 124 | component: ss 125 | ports: 126 | - name: http-web 127 | protocol: "TCP" 128 | port: 8080 129 | targetPort: http-web 130 | - name: http-web2 131 | protocol: "TCP" 132 | port: 8081 133 | targetPort: http-web2 134 | type: ClusterIP 135 | clusterIP: None 136 | --- 137 | apiVersion: apps/v1 138 | kind: StatefulSet 139 | metadata: 140 | name: ok-ss 141 | namespace: kf-b 142 | labels: 143 | app: ok-ss 144 | spec: 145 | replicas: 3 146 | revisionHistoryLimit: 1 147 | serviceName: ok-ss 148 | selector: 149 | matchLabels: 150 | app: ok-ss 151 | component: ss 152 | template: 153 | metadata: 154 | labels: 155 | app: ok-ss 156 | component: ss 157 | spec: 158 | containers: 159 | - name: ok 160 | image: txn2/ok:3.0.0 161 | imagePullPolicy: IfNotPresent # IfNotPresent for production 162 | env: 163 | - name: "MESSAGE" 164 | value: "ok ss in kf-b" 165 | - name: PORT 166 | value: "8080" 167 | - name: NODE_NAME 168 | valueFrom: 169 | fieldRef: 170 | fieldPath: spec.nodeName 171 | - name: POD_NAME 172 | valueFrom: 173 | fieldRef: 174 | fieldPath: metadata.name 175 | - name: POD_NAMESPACE 176 | valueFrom: 177 | fieldRef: 178 | fieldPath: metadata.namespace 179 | - name: POD_IP 180 | valueFrom: 181 | fieldRef: 182 | fieldPath: status.podIP 183 | - name: SERVICE_ACCOUNT 184 | valueFrom: 185 | fieldRef: 186 | fieldPath: spec.serviceAccountName 187 | ports: 188 | - name: http-web 189 | containerPort: 8080 190 | resources: 191 | requests: 192 | cpu: 100m 193 | memory: 16Mi 194 | limits: 195 | cpu: 200m 196 | memory: 32Mi 197 | - name: ok2 198 | image: txn2/ok:3.0.0 199 | imagePullPolicy: IfNotPresent # IfNotPresent for production 200 | env: 201 | - name: "MESSAGE" 202 | value: "ok2 container ss in kf-b" 203 | - name: PORT 204 | value: "8081" 205 | - name: NODE_NAME 206 | valueFrom: 207 | fieldRef: 208 | fieldPath: spec.nodeName 209 | - name: POD_NAME 210 | valueFrom: 211 | fieldRef: 212 | fieldPath: metadata.name 213 | - name: POD_NAMESPACE 214 | valueFrom: 215 | fieldRef: 216 | fieldPath: metadata.namespace 217 | - name: POD_IP 218 | valueFrom: 219 | fieldRef: 220 | fieldPath: status.podIP 221 | - name: SERVICE_ACCOUNT 222 | valueFrom: 223 | fieldRef: 224 | fieldPath: spec.serviceAccountName 225 | ports: 226 | - name: http-web2 227 | containerPort: 8081 228 | resources: 229 | requests: 230 | cpu: 100m 231 | memory: 16Mi 232 | limits: 233 | cpu: 200m 234 | memory: 32Mi -------------------------------------------------------------------------------- /kubefwd-mast2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txn2/kubefwd/cdb37d5a139b6f9287a940445e2e6ccdb0ef34e9/kubefwd-mast2.jpg -------------------------------------------------------------------------------- /kubefwd_ani.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txn2/kubefwd/cdb37d5a139b6f9287a940445e2e6ccdb0ef34e9/kubefwd_ani.gif -------------------------------------------------------------------------------- /kubefwd_sn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txn2/kubefwd/cdb37d5a139b6f9287a940445e2e6ccdb0ef34e9/kubefwd_sn.png -------------------------------------------------------------------------------- /pkg/fwdIp/fwdIp.go: -------------------------------------------------------------------------------- 1 | package fwdIp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | log "github.com/sirupsen/logrus" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type ForwardIPOpts struct { 17 | ServiceName string 18 | PodName string 19 | Context string 20 | ClusterN int 21 | NamespaceN int 22 | Namespace string 23 | Port string 24 | ForwardConfigurationPath string 25 | ForwardIPReservations []string 26 | } 27 | 28 | // Registry is a structure to create and hold all of the 29 | // IP address assignments 30 | type Registry struct { 31 | mutex *sync.Mutex 32 | inc map[int]map[int]int 33 | reg map[string]net.IP 34 | allocated map[string]bool 35 | } 36 | 37 | type ForwardConfiguration struct { 38 | BaseUnreservedIP string `yaml:"baseUnreservedIP"` 39 | ServiceConfigurations []*ServiceConfiguration `yaml:"serviceConfigurations"` 40 | } 41 | 42 | type ServiceConfiguration struct { 43 | Name string `yaml:"name"` 44 | IP string `yaml:"ip"` 45 | } 46 | 47 | var ipRegistry *Registry 48 | var forwardConfiguration *ForwardConfiguration 49 | var defaultConfiguration = &ForwardConfiguration{BaseUnreservedIP: "127.1.27.1"} 50 | 51 | // Init 52 | func init() { 53 | ipRegistry = &Registry{ 54 | mutex: &sync.Mutex{}, 55 | // counter for the service cluster and namespace 56 | inc: map[int]map[int]int{0: {0: 0}}, 57 | reg: make(map[string]net.IP), 58 | allocated: make(map[string]bool), 59 | } 60 | } 61 | 62 | func GetIp(opts ForwardIPOpts) (net.IP, error) { 63 | ipRegistry.mutex.Lock() 64 | defer ipRegistry.mutex.Unlock() 65 | 66 | regKey := fmt.Sprintf("%d-%d-%s-%s", opts.ClusterN, opts.NamespaceN, opts.ServiceName, opts.PodName) 67 | 68 | if ip, ok := ipRegistry.reg[regKey]; ok { 69 | return ip, nil 70 | } 71 | 72 | return determineIP(regKey, opts), nil 73 | } 74 | 75 | func determineIP(regKey string, opts ForwardIPOpts) net.IP { 76 | baseUnreservedIP := getBaseUnreservedIP(opts) 77 | 78 | // if a configuration exists use it 79 | svcConf := getConfigurationForService(opts) 80 | if svcConf != nil { 81 | if ip, err := ipFromString(svcConf.IP); err == nil { 82 | if err := addToRegistry(regKey, opts, ip); err == nil { 83 | return ip 84 | } 85 | } else { 86 | log.Errorf("Invalid service IP format %s %s", svcConf.String(), err) 87 | } 88 | } 89 | 90 | // fall back to previous implementation if svcConf not provided 91 | if ipRegistry.inc[opts.ClusterN] == nil { 92 | ipRegistry.inc[opts.ClusterN] = map[int]int{0: 0} 93 | } 94 | 95 | // bounds check 96 | if opts.ClusterN > 255 || 97 | opts.NamespaceN > 255 || 98 | ipRegistry.inc[opts.ClusterN][opts.NamespaceN] > 255 { 99 | panic("IP generation has run out of bounds.") 100 | } 101 | 102 | ip := baseUnreservedIP 103 | ip[1] += byte(opts.ClusterN) 104 | ip[2] += byte(opts.NamespaceN) 105 | ip[3] += byte(ipRegistry.inc[opts.ClusterN][opts.NamespaceN]) 106 | 107 | ipRegistry.inc[opts.ClusterN][opts.NamespaceN]++ 108 | if err := addToRegistry(regKey, opts, ip); err != nil { 109 | // this recursive call will continue to inc the ip offset until 110 | // an open slot is found or we go out of bounds 111 | return determineIP(regKey, opts) 112 | } 113 | return ip 114 | } 115 | 116 | func addToRegistry(regKey string, opts ForwardIPOpts, ip net.IP) error { 117 | allocationKey := ip.String() 118 | if _, ok := ipRegistry.allocated[allocationKey]; ok { 119 | // ip/port pair has allready ben allocated 120 | msg := fmt.Sprintf("Unable to forward service %s to requested IP %s due to collision. Will allocate next available", opts.ServiceName, allocationKey) 121 | log.Error(msg) 122 | return errors.New(msg) 123 | } 124 | 125 | // check for conflicting reservation 126 | if conflicting := hasConflictingReservations(opts, ip.String()); conflicting != nil { 127 | msg := fmt.Sprintf("Conflicting reservation for %s on %s when placing %s. Will allocate next available", 128 | conflicting.Name, allocationKey, opts.ServiceName) 129 | log.Debug(msg) 130 | return errors.New(msg) 131 | } 132 | 133 | ipRegistry.reg[regKey] = ip 134 | ipRegistry.allocated[allocationKey] = true 135 | return nil 136 | } 137 | 138 | func ipFromString(ipStr string) (net.IP, error) { 139 | ipParts := strings.Split(ipStr, ".") 140 | 141 | octet0, err := strconv.Atoi(ipParts[0]) 142 | if err != nil { 143 | return nil, fmt.Errorf("Unable to parse BaseIP octet 0") 144 | } 145 | octet1, err := strconv.Atoi(ipParts[1]) 146 | if err != nil { 147 | return nil, fmt.Errorf("Unable to parse BaseIP octet 1") 148 | } 149 | octet2, err := strconv.Atoi(ipParts[2]) 150 | if err != nil { 151 | return nil, fmt.Errorf("Unable to parse BaseIP octet 2") 152 | } 153 | octet3, err := strconv.Atoi(ipParts[3]) 154 | if err != nil { 155 | return nil, fmt.Errorf("Unable to parse BaseIP octet 3") 156 | } 157 | return net.IP{byte(octet0), byte(octet1), byte(octet2), byte(octet3)}.To4(), nil 158 | } 159 | 160 | func hasConflictingReservations(opts ForwardIPOpts, wantIP string) *ServiceConfiguration { 161 | fwdCfg := getForwardConfiguration(opts) 162 | for _, cfg := range fwdCfg.ServiceConfigurations { 163 | // if the IP we want is reserverd and the 164 | // target service is not the one listed in 165 | // the forward configuration 166 | if wantIP == cfg.IP && !cfg.Matches(opts) { 167 | return cfg 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | func getBaseUnreservedIP(opts ForwardIPOpts) []byte { 174 | fwdCfg := getForwardConfiguration(opts) 175 | ip, err := ipFromString(fwdCfg.BaseUnreservedIP) 176 | if err != nil { 177 | log.Fatal(err) 178 | } 179 | return ip 180 | } 181 | 182 | func getConfigurationForService(opts ForwardIPOpts) *ServiceConfiguration { 183 | fwdCfg := getForwardConfiguration(opts) 184 | for _, svcCfg := range fwdCfg.ServiceConfigurations { 185 | if svcCfg.Matches(opts) { 186 | return svcCfg 187 | } 188 | } 189 | return nil 190 | } 191 | 192 | func blockNonLoopbackIPs(f *ForwardConfiguration) { 193 | if ip, err := ipFromString(f.BaseUnreservedIP); err != nil || !ip.IsLoopback() { 194 | panic("BaseUnreservedIP is not in the range 127.0.0.0/8") 195 | } 196 | for _, svcCfg := range f.ServiceConfigurations { 197 | if ip, err := ipFromString(svcCfg.IP); err != nil || !ip.IsLoopback() { 198 | log.Fatal(fmt.Sprintf("IP %s for %s is not in the range 127.0.0.0/8", svcCfg.IP, svcCfg.Name)) 199 | } 200 | } 201 | } 202 | 203 | func notifyOfDuplicateIPReservations(f *ForwardConfiguration) { 204 | // Alerts the user 205 | requestedIPs := map[string]bool{} 206 | for _, svcCfg := range f.ServiceConfigurations { 207 | if _, ok := requestedIPs[svcCfg.IP]; ok { 208 | log.Fatal(fmt.Sprintf("IP %s cannot be used as a reservation for multiple services", svcCfg.IP)) 209 | } 210 | requestedIPs[svcCfg.IP] = true 211 | } 212 | } 213 | 214 | func validateForwardConfiguration(f *ForwardConfiguration) { 215 | blockNonLoopbackIPs(f) 216 | notifyOfDuplicateIPReservations(f) 217 | } 218 | 219 | func applyCLIPassedReservations(opts ForwardIPOpts, f *ForwardConfiguration) *ForwardConfiguration { 220 | for _, resStr := range opts.ForwardIPReservations { 221 | res := ServiceConfigurationFromReservation(resStr) 222 | 223 | overridden := false 224 | for _, svcCfg := range f.ServiceConfigurations { 225 | if svcCfg.MatchesName(res) { 226 | svcCfg.IP = res.IP 227 | overridden = true 228 | log.Infof("Cli reservation flag overriding config for %s now %s", svcCfg.Name, svcCfg.IP) 229 | } 230 | } 231 | if !overridden { 232 | f.ServiceConfigurations = append(f.ServiceConfigurations, res) 233 | } 234 | } 235 | validateForwardConfiguration(f) 236 | return f 237 | } 238 | 239 | func getForwardConfiguration(opts ForwardIPOpts) *ForwardConfiguration { 240 | if forwardConfiguration != nil { 241 | return forwardConfiguration 242 | } 243 | 244 | if opts.ForwardConfigurationPath == "" { 245 | forwardConfiguration = defaultConfiguration 246 | return applyCLIPassedReservations(opts, forwardConfiguration) 247 | } 248 | 249 | dat, err := os.ReadFile(opts.ForwardConfigurationPath) 250 | if err != nil { 251 | // fall back to existing kubefwd base 252 | log.Errorf("ForwardConfiguration read error %s", err) 253 | forwardConfiguration = defaultConfiguration 254 | return applyCLIPassedReservations(opts, forwardConfiguration) 255 | } 256 | 257 | conf := &ForwardConfiguration{} 258 | err = yaml.Unmarshal(dat, conf) 259 | if err != nil { 260 | // fall back to existing kubefwd base 261 | log.Errorf("ForwardConfiguration parse error %s", err) 262 | forwardConfiguration = defaultConfiguration 263 | return applyCLIPassedReservations(opts, forwardConfiguration) 264 | } 265 | 266 | forwardConfiguration = conf 267 | return applyCLIPassedReservations(opts, forwardConfiguration) 268 | } 269 | 270 | func (o ForwardIPOpts) MatchList() []string { 271 | if o.ClusterN == 0 && o.NamespaceN == 0 { 272 | return []string{ 273 | o.PodName, 274 | 275 | o.ServiceName, 276 | fmt.Sprintf("%s.svc", o.ServiceName), 277 | 278 | fmt.Sprintf("%s.%s", o.PodName, o.ServiceName), 279 | 280 | fmt.Sprintf("%s.%s", o.PodName, o.Context), 281 | 282 | fmt.Sprintf("%s.%s", o.ServiceName, o.Context), 283 | 284 | fmt.Sprintf("%s.%s", o.PodName, o.Namespace), 285 | fmt.Sprintf("%s.%s.svc", o.PodName, o.Namespace), 286 | fmt.Sprintf("%s.%s.svc.cluster.local", o.PodName, o.Namespace), 287 | 288 | fmt.Sprintf("%s.%s", o.ServiceName, o.Namespace), 289 | fmt.Sprintf("%s.%s.svc", o.ServiceName, o.Namespace), 290 | fmt.Sprintf("%s.%s.svc.cluster.local", o.ServiceName, o.Namespace), 291 | 292 | fmt.Sprintf("%s.%s.%s", o.PodName, o.ServiceName, o.Namespace), 293 | fmt.Sprintf("%s.%s.%s.svc", o.PodName, o.ServiceName, o.Namespace), 294 | fmt.Sprintf("%s.%s.%s.svc.cluster.local", o.PodName, o.ServiceName, o.Namespace), 295 | 296 | fmt.Sprintf("%s.%s.%s", o.PodName, o.ServiceName, o.Context), 297 | 298 | fmt.Sprintf("%s.%s.%s", o.PodName, o.Namespace, o.Context), 299 | fmt.Sprintf("%s.%s.svc.%s", o.PodName, o.Namespace, o.Context), 300 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.PodName, o.Namespace, o.Context), 301 | 302 | fmt.Sprintf("%s.%s.%s", o.ServiceName, o.Namespace, o.Context), 303 | fmt.Sprintf("%s.%s.svc.%s", o.ServiceName, o.Namespace, o.Context), 304 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.ServiceName, o.Namespace, o.Context), 305 | 306 | fmt.Sprintf("%s.%s.%s.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 307 | fmt.Sprintf("%s.%s.%s.svc.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 308 | fmt.Sprintf("%s.%s.%s.svc.cluster.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 309 | } 310 | } 311 | 312 | if o.ClusterN > 0 && o.NamespaceN == 0 { 313 | return []string{ 314 | fmt.Sprintf("%s.%s", o.PodName, o.Context), 315 | 316 | fmt.Sprintf("%s.%s.%s", o.PodName, o.ServiceName, o.Context), 317 | 318 | fmt.Sprintf("%s.%s", o.ServiceName, o.Context), 319 | 320 | fmt.Sprintf("%s.%s.%s", o.ServiceName, o.Namespace, o.Context), 321 | fmt.Sprintf("%s.%s.svc.%s", o.ServiceName, o.Namespace, o.Context), 322 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.ServiceName, o.Namespace, o.Context), 323 | 324 | fmt.Sprintf("%s.%s.%s", o.PodName, o.Namespace, o.Context), 325 | fmt.Sprintf("%s.%s.svc.%s", o.PodName, o.Namespace, o.Context), 326 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.PodName, o.Namespace, o.Context), 327 | 328 | fmt.Sprintf("%s.%s.%s.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 329 | fmt.Sprintf("%s.%s.%s.svc.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 330 | fmt.Sprintf("%s.%s.%s.svc.cluster.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 331 | } 332 | } 333 | 334 | if o.ClusterN == 0 && o.NamespaceN > 0 { 335 | return []string{ 336 | fmt.Sprintf("%s.%s", o.PodName, o.Namespace), 337 | fmt.Sprintf("%s.%s.svc", o.PodName, o.Namespace), 338 | 339 | fmt.Sprintf("%s.%s", o.ServiceName, o.Namespace), 340 | fmt.Sprintf("%s.%s.svc", o.ServiceName, o.Namespace), 341 | fmt.Sprintf("%s.%s.svc.cluster.local", o.ServiceName, o.Namespace), 342 | 343 | fmt.Sprintf("%s.%s.%s", o.PodName, o.ServiceName, o.Namespace), 344 | fmt.Sprintf("%s.%s.%s.svc", o.PodName, o.ServiceName, o.Namespace), 345 | fmt.Sprintf("%s.%s.%s.svc.cluster.local", o.PodName, o.ServiceName, o.Namespace), 346 | 347 | fmt.Sprintf("%s.%s.%s", o.PodName, o.Namespace, o.Context), 348 | fmt.Sprintf("%s.%s.svc.%s", o.PodName, o.Namespace, o.Context), 349 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.PodName, o.Namespace, o.Context), 350 | 351 | fmt.Sprintf("%s.%s.%s", o.ServiceName, o.Namespace, o.Context), 352 | fmt.Sprintf("%s.%s.svc.%s", o.ServiceName, o.Namespace, o.Context), 353 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.ServiceName, o.Namespace, o.Context), 354 | 355 | fmt.Sprintf("%s.%s.%s.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 356 | fmt.Sprintf("%s.%s.%s.svc.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 357 | fmt.Sprintf("%s.%s.%s.svc.cluster.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 358 | } 359 | } 360 | 361 | return []string{ 362 | fmt.Sprintf("%s.%s.%s", o.PodName, o.Namespace, o.Context), 363 | fmt.Sprintf("%s.%s.svc.%s", o.PodName, o.Namespace, o.Context), 364 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.PodName, o.Namespace, o.Context), 365 | 366 | fmt.Sprintf("%s.%s.%s", o.ServiceName, o.Namespace, o.Context), 367 | fmt.Sprintf("%s.%s.svc.%s", o.ServiceName, o.Namespace, o.Context), 368 | fmt.Sprintf("%s.%s.svc.cluster.%s", o.ServiceName, o.Namespace, o.Context), 369 | 370 | fmt.Sprintf("%s.%s.%s.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 371 | fmt.Sprintf("%s.%s.%s.svc.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 372 | fmt.Sprintf("%s.%s.%s.svc.cluster.%s", o.PodName, o.ServiceName, o.Namespace, o.Context), 373 | } 374 | } 375 | 376 | func ServiceConfigurationFromReservation(reservation string) *ServiceConfiguration { 377 | parts := strings.SplitN(reservation, ":", 2) 378 | if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { 379 | return nil 380 | } 381 | return &ServiceConfiguration{ 382 | Name: parts[0], 383 | IP: parts[1], 384 | } 385 | } 386 | 387 | func (c ServiceConfiguration) String() string { 388 | return fmt.Sprintf("Name: %s IP:%s", c.Name, c.IP) 389 | } 390 | 391 | func (c ServiceConfiguration) Matches(opts ForwardIPOpts) bool { 392 | matchList := opts.MatchList() 393 | for _, toMatch := range matchList { 394 | if c.Name == toMatch { 395 | return true 396 | } 397 | } 398 | return false 399 | } 400 | 401 | func (c ServiceConfiguration) MatchesName(otherCfg *ServiceConfiguration) bool { 402 | return c.Name == otherCfg.Name 403 | } 404 | -------------------------------------------------------------------------------- /pkg/fwdcfg/fwdcfg.go: -------------------------------------------------------------------------------- 1 | package fwdcfg 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | restclient "k8s.io/client-go/rest" 6 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 7 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 8 | ) 9 | 10 | // ConfigGetter 11 | type ConfigGetter struct { 12 | ConfigFlag *genericclioptions.ConfigFlags 13 | } 14 | 15 | // NewConfigGetter 16 | func NewConfigGetter() *ConfigGetter { 17 | configFlag := genericclioptions.NewConfigFlags(false) 18 | return &ConfigGetter{ 19 | ConfigFlag: configFlag, 20 | } 21 | } 22 | 23 | // GetClientConfig build the ClientConfig and return rawConfig 24 | // if cfgFilePath is set special, use it. otherwise search the 25 | // KUBECONFIG environment variable and merge it. if KUBECONFIG env 26 | // is blank, use the default kubeconfig in $HOME/.kube/config 27 | func (c *ConfigGetter) GetClientConfig(cfgFilePath string) (*clientcmdapi.Config, error) { 28 | if cfgFilePath != "" { 29 | c.ConfigFlag.KubeConfig = &cfgFilePath 30 | } 31 | configLoader := c.ConfigFlag.ToRawKubeConfigLoader() 32 | rawConfig, err := configLoader.RawConfig() 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &rawConfig, nil 37 | } 38 | 39 | // GetRestConfig uses the kubectl config file to connect to 40 | // a cluster. 41 | func (c *ConfigGetter) GetRestConfig(cfgFilePath string, context string) (*restclient.Config, error) { 42 | if cfgFilePath != "" { 43 | c.ConfigFlag.KubeConfig = &cfgFilePath 44 | } 45 | c.ConfigFlag.Context = &context 46 | configLoader := c.ConfigFlag.ToRawKubeConfigLoader() 47 | restConfig, err := configLoader.ClientConfig() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return restConfig, nil 53 | } 54 | 55 | // GetRestClient return the RESTClient 56 | func (c *ConfigGetter) GetRESTClient() (*restclient.RESTClient, error) { 57 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(c.ConfigFlag) 58 | f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 59 | RESTClient, err := f.RESTClient() 60 | if err != nil { 61 | return nil, err 62 | } 63 | return RESTClient, nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/fwdhost/fwdhost.go: -------------------------------------------------------------------------------- 1 | package fwdhost 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/txn2/txeh" 10 | ) 11 | 12 | // BackupHostFile will write a backup of the pre-modified host file 13 | // the users home directory, if it does already exist. 14 | func BackupHostFile(hostFile *txeh.Hosts) (string, error) { 15 | homeDirLocation, err := os.UserHomeDir() 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | backupHostsPath := homeDirLocation + "/hosts.original" 21 | if _, err := os.Stat(backupHostsPath); os.IsNotExist(err) { 22 | from, err := os.Open(hostFile.WriteFilePath) 23 | if err != nil { 24 | return "", err 25 | } 26 | defer func() { _ = from.Close() }() 27 | 28 | to, err := os.OpenFile(backupHostsPath, os.O_RDWR|os.O_CREATE, 0644) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer func() { _ = to.Close() }() 33 | 34 | _, err = io.Copy(to, from) 35 | if err != nil { 36 | return "", err 37 | } 38 | return fmt.Sprintf("Backing up your original hosts file %s to %s\n", hostFile.WriteFilePath, backupHostsPath), nil 39 | } 40 | 41 | return fmt.Sprintf("Original hosts backup already exists at %s\n", backupHostsPath), nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/fwdnet/fwdnet.go: -------------------------------------------------------------------------------- 1 | package fwdnet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | 11 | "github.com/txn2/kubefwd/pkg/fwdIp" 12 | ) 13 | 14 | // ReadyInterface prepares a local IP address on 15 | // the loopback interface. 16 | func ReadyInterface(opts fwdIp.ForwardIPOpts) (net.IP, error) { 17 | 18 | ip, _ := fwdIp.GetIp(opts) 19 | 20 | // lo means we are probably on linux and not mac 21 | _, err := net.InterfaceByName("lo") 22 | if err == nil || runtime.GOOS == "windows" { 23 | // if no error then check to see if the ip:port are in use 24 | _, err := net.Dial("tcp", ip.String()+":"+opts.Port) 25 | if err != nil { 26 | return ip, nil 27 | } 28 | 29 | return ip, errors.New("ip and port are in use") 30 | } 31 | 32 | networkInterface, err := net.InterfaceByName("lo0") 33 | if err != nil { 34 | return net.IP{}, err 35 | } 36 | 37 | addrs, err := networkInterface.Addrs() 38 | if err != nil { 39 | return net.IP{}, err 40 | } 41 | 42 | // check the addresses already assigned to the interface 43 | for _, addr := range addrs { 44 | 45 | // found a match 46 | if addr.String() == ip.String()+"/8" { 47 | // found ip, now check for unused port 48 | conn, err := net.Dial("tcp", ip.String()+":"+opts.Port) 49 | if err != nil { 50 | return ip, nil 51 | } 52 | _ = conn.Close() 53 | } 54 | } 55 | 56 | // ip is not in the list of addrs for networkInterface 57 | cmd := "ifconfig" 58 | args := []string{"lo0", "alias", ip.String(), "up"} 59 | if err := exec.Command(cmd, args...).Run(); err != nil { 60 | fmt.Println("Cannot ifconfig lo0 alias " + ip.String() + " up") 61 | fmt.Println("Error: " + err.Error()) 62 | os.Exit(1) 63 | } 64 | 65 | conn, err := net.Dial("tcp", ip.String()+":"+opts.Port) 66 | if err != nil { 67 | return ip, nil 68 | } 69 | _ = conn.Close() 70 | 71 | return net.IP{}, errors.New("unable to find an available IP/Port") 72 | } 73 | 74 | // RemoveInterfaceAlias can remove the Interface alias after port forwarding. 75 | // if -alias command get err, just print the error and continue. 76 | func RemoveInterfaceAlias(ip net.IP) { 77 | cmd := "ifconfig" 78 | args := []string{"lo0", "-alias", ip.String()} 79 | if err := exec.Command(cmd, args...).Run(); err != nil { 80 | // suppress for now 81 | // @todo research alternative to ifconfig 82 | // @todo suggest ifconfig or alternative 83 | // @todo research libs for interface management 84 | //fmt.Println("Cannot ifconfig lo0 -alias " + ip.String() + "\r\n" + err.Error()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/fwdport/fwdport.go: -------------------------------------------------------------------------------- 1 | package fwdport 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "k8s.io/apimachinery/pkg/util/httpstream" 15 | 16 | log "github.com/sirupsen/logrus" 17 | "github.com/txn2/kubefwd/pkg/fwdnet" 18 | "github.com/txn2/kubefwd/pkg/fwdpub" 19 | "github.com/txn2/txeh" 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/watch" 23 | "k8s.io/client-go/kubernetes" 24 | restclient "k8s.io/client-go/rest" 25 | "k8s.io/client-go/tools/portforward" 26 | "k8s.io/client-go/transport/spdy" 27 | ) 28 | 29 | // ServiceFWD PodSyncer interface is used to represent a 30 | // fwdservice.ServiceFWD reference, which cannot be used directly 31 | // due to circular imports. It's a reference from a pod to it's 32 | // parent service. 33 | type ServiceFWD interface { 34 | String() string 35 | SyncPodForwards(bool) 36 | } 37 | 38 | // HostFileWithLock 39 | type HostFileWithLock struct { 40 | Hosts *txeh.Hosts 41 | sync.Mutex 42 | } 43 | 44 | // HostsParams 45 | type HostsParams struct { 46 | localServiceName string 47 | nsServiceName string 48 | fullServiceName string 49 | svcServiceName string 50 | } 51 | 52 | // PortForwardOpts 53 | type PortForwardOpts struct { 54 | Out *fwdpub.Publisher 55 | Config restclient.Config 56 | ClientSet kubernetes.Clientset 57 | RESTClient restclient.RESTClient 58 | 59 | Service string 60 | ServiceFwd ServiceFWD 61 | PodName string 62 | PodPort string 63 | LocalIp net.IP 64 | LocalPort string 65 | // Timeout for the port-forwarding process 66 | Timeout int 67 | HostFile *HostFileWithLock 68 | 69 | // Context is a unique key (string) in kubectl config representing 70 | // a user/cluster combination. Kubefwd uses context as the 71 | // cluster name when forwarding to more than one cluster. 72 | Context string 73 | 74 | // Namespace is the current Kubernetes Namespace to locate services 75 | // and the pods that back them for port-forwarding 76 | Namespace string 77 | 78 | // ClusterN is the ordinal index of the cluster (from configuration) 79 | // cluster 0 is considered local while > 0 is remote 80 | ClusterN int 81 | 82 | // NamespaceN is the ordinal index of the namespace from the 83 | // perspective of the user. Namespace 0 is considered local 84 | // while > 0 is an external namespace 85 | NamespaceN int 86 | 87 | Domain string 88 | HostsParams *HostsParams 89 | Hosts []string 90 | ManualStopChan chan struct{} // Send a signal on this to stop the portforwarding 91 | DoneChan chan struct{} // Listen on this channel for when the shutdown is completed. 92 | } 93 | 94 | type pingingDialer struct { 95 | wrappedDialer httpstream.Dialer 96 | pingPeriod time.Duration 97 | pingStopChan chan struct{} 98 | pingTargetPodName string 99 | } 100 | 101 | func (p pingingDialer) stopPing() { 102 | p.pingStopChan <- struct{}{} 103 | } 104 | 105 | func (p pingingDialer) Dial(protocols ...string) (httpstream.Connection, string, error) { 106 | streamConn, streamProtocolVersion, dialErr := p.wrappedDialer.Dial(protocols...) 107 | if dialErr != nil { 108 | log.Warnf("Ping process will not be performed for %s, cannot dial", p.pingTargetPodName) 109 | } 110 | go func(streamConnection httpstream.Connection) { 111 | if streamConnection == nil || dialErr != nil { 112 | return 113 | } 114 | for { 115 | select { 116 | case <-time.After(p.pingPeriod): 117 | if pingStream, err := streamConnection.CreateStream(nil); err == nil { 118 | _ = pingStream.Reset() 119 | } 120 | case <-p.pingStopChan: 121 | log.Debug(fmt.Sprintf("Ping process stopped for %s", p.pingTargetPodName)) 122 | return 123 | } 124 | } 125 | }(streamConn) 126 | 127 | return streamConn, streamProtocolVersion, dialErr 128 | } 129 | 130 | // PortForward does the port-forward for a single pod. 131 | // It is a blocking call and will return when an error occurred 132 | // or after a cancellation signal has been received. 133 | func (pfo *PortForwardOpts) PortForward() error { 134 | defer close(pfo.DoneChan) 135 | 136 | transport, upgrader, err := spdy.RoundTripperFor(&pfo.Config) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | // check that pod port can be strconv.ParseUint 142 | _, err = strconv.ParseUint(pfo.PodPort, 10, 32) 143 | if err != nil { 144 | pfo.PodPort = pfo.LocalPort 145 | } 146 | 147 | fwdPorts := []string{fmt.Sprintf("%s:%s", pfo.LocalPort, pfo.PodPort)} 148 | 149 | // if need to set timeout, set it here. 150 | // restClient.Client.Timeout = 32 151 | req := pfo.RESTClient.Post(). 152 | Resource("pods"). 153 | Namespace(pfo.Namespace). 154 | Name(pfo.PodName). 155 | SubResource("portforward") 156 | 157 | pfStopChannel := make(chan struct{}, 1) // Signal that k8s forwarding takes as input for us to signal when to stop 158 | downstreamStopChannel := make(chan struct{}) // @TODO: can this be the same as pfStopChannel? 159 | 160 | localNamedEndPoint := fmt.Sprintf("%s:%s", pfo.Service, pfo.LocalPort) 161 | 162 | pfo.AddHosts() 163 | 164 | // Wait until the stop signal is received from above 165 | go func() { 166 | <-pfo.ManualStopChan 167 | close(downstreamStopChannel) 168 | pfo.removeHosts() 169 | pfo.removeInterfaceAlias() 170 | close(pfStopChannel) 171 | 172 | }() 173 | 174 | // Waiting until the pod is running 175 | pod, err := pfo.WaitUntilPodRunning(downstreamStopChannel) 176 | if err != nil { 177 | pfo.Stop() 178 | return err 179 | } else if pod == nil { 180 | // if err is not nil but pod is nil 181 | // mean service deleted but pod is not runnning. 182 | // No error, just return 183 | pfo.Stop() 184 | return nil 185 | } 186 | 187 | // Listen for pod is deleted 188 | // @TODO need a test for this, does not seem to work as intended 189 | go pfo.ListenUntilPodDeleted(downstreamStopChannel, pod) 190 | 191 | p := pfo.Out.MakeProducer(localNamedEndPoint) 192 | 193 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, req.URL()) 194 | dialerWithPing := pingingDialer{ 195 | wrappedDialer: dialer, 196 | pingPeriod: time.Second * 30, 197 | pingStopChan: make(chan struct{}), 198 | pingTargetPodName: pfo.PodName, 199 | } 200 | 201 | var address []string 202 | if pfo.LocalIp != nil { 203 | address = []string{pfo.LocalIp.To4().String(), pfo.LocalIp.To16().String()} 204 | } else { 205 | address = []string{"localhost"} 206 | } 207 | 208 | fw, err := portforward.NewOnAddresses(dialerWithPing, address, fwdPorts, pfStopChannel, make(chan struct{}), &p, &p) 209 | if err != nil { 210 | pfo.Stop() 211 | return err 212 | } 213 | 214 | // Blocking call 215 | if err = fw.ForwardPorts(); err != nil { 216 | log.Errorf("ForwardPorts error: %s", err.Error()) 217 | pfo.Stop() 218 | dialerWithPing.stopPing() 219 | return err 220 | } 221 | 222 | return nil 223 | } 224 | 225 | //// BuildHostsParams constructs the basic hostnames for the service 226 | //// based on the PortForwardOpts configuration 227 | //func (pfo *PortForwardOpts) BuildHostsParams() { 228 | // 229 | // localServiceName := pfo.Service 230 | // nsServiceName := pfo.Service + "." + pfo.Namespace 231 | // fullServiceName := fmt.Sprintf("%s.%s.svc.cluster.local", pfo.Service, pfo.Namespace) 232 | // svcServiceName := fmt.Sprintf("%s.%s.svc", pfo.Service, pfo.Namespace) 233 | // 234 | // // check if this is an additional cluster (remote from the 235 | // // perspective of the user / argument order) 236 | // if pfo.ClusterN > 0 { 237 | // fullServiceName = fmt.Sprintf("%s.%s.svc.cluster.%s", pfo.Service, pfo.Namespace, pfo.Context) 238 | // } 239 | // pfo.HostsParams.localServiceName = localServiceName 240 | // pfo.HostsParams.nsServiceName = nsServiceName 241 | // pfo.HostsParams.fullServiceName = fullServiceName 242 | // pfo.HostsParams.svcServiceName = svcServiceName 243 | //} 244 | 245 | // AddHost 246 | func (pfo *PortForwardOpts) addHost(host string) { 247 | // add to list of hostnames for this port-forward 248 | pfo.Hosts = append(pfo.Hosts, host) 249 | 250 | // remove host if it already exists in /etc/hosts 251 | pfo.HostFile.Hosts.RemoveHost(host) 252 | 253 | // add host to /etc/hosts 254 | pfo.HostFile.Hosts.AddHost(pfo.LocalIp.String(), host) 255 | 256 | sanitizedHost := sanitizeHost(host) 257 | if host != sanitizedHost { 258 | pfo.addHost(sanitizedHost) //should recurse only once 259 | } 260 | } 261 | 262 | // make sure any non-alphanumeric characters in the context name don't make it to the generated hostname 263 | func sanitizeHost(host string) string { 264 | hostnameIllegalChars := regexp.MustCompile(`[^a-zA-Z0-9\-]`) 265 | replacementChar := `-` 266 | sanitizedHost := strings.Trim(hostnameIllegalChars.ReplaceAllString(host, replacementChar), replacementChar) 267 | return sanitizedHost 268 | } 269 | 270 | // AddHosts adds hostname entries to /etc/hosts 271 | func (pfo *PortForwardOpts) AddHosts() { 272 | 273 | pfo.HostFile.Lock() 274 | 275 | // pfo.Service holds only the service name 276 | // start with the smallest allowable hostname 277 | 278 | // bare service name 279 | if pfo.ClusterN == 0 && pfo.NamespaceN == 0 { 280 | pfo.addHost(pfo.Service) 281 | 282 | if pfo.Domain != "" { 283 | pfo.addHost(fmt.Sprintf( 284 | "%s.%s", 285 | pfo.Service, 286 | pfo.Domain, 287 | )) 288 | } 289 | } 290 | 291 | // alternate cluster / first namespace 292 | if pfo.ClusterN > 0 && pfo.NamespaceN == 0 { 293 | pfo.addHost(fmt.Sprintf( 294 | "%s.%s", 295 | pfo.Service, 296 | pfo.Context, 297 | )) 298 | } 299 | 300 | // namespaced without cluster 301 | if pfo.ClusterN == 0 { 302 | pfo.addHost(fmt.Sprintf( 303 | "%s.%s", 304 | pfo.Service, 305 | pfo.Namespace, 306 | )) 307 | 308 | pfo.addHost(fmt.Sprintf( 309 | "%s.%s.svc", 310 | pfo.Service, 311 | pfo.Namespace, 312 | )) 313 | 314 | pfo.addHost(fmt.Sprintf( 315 | "%s.%s.svc.cluster.local", 316 | pfo.Service, 317 | pfo.Namespace, 318 | )) 319 | 320 | if pfo.Domain != "" { 321 | pfo.addHost(fmt.Sprintf( 322 | "%s.%s.svc.cluster.%s", 323 | pfo.Service, 324 | pfo.Namespace, 325 | pfo.Domain, 326 | )) 327 | } 328 | 329 | } 330 | 331 | pfo.addHost(fmt.Sprintf( 332 | "%s.%s.%s", 333 | pfo.Service, 334 | pfo.Namespace, 335 | pfo.Context, 336 | )) 337 | 338 | pfo.addHost(fmt.Sprintf( 339 | "%s.%s.svc.%s", 340 | pfo.Service, 341 | pfo.Namespace, 342 | pfo.Context, 343 | )) 344 | 345 | pfo.addHost(fmt.Sprintf( 346 | "%s.%s.svc.cluster.%s", 347 | pfo.Service, 348 | pfo.Namespace, 349 | pfo.Context, 350 | )) 351 | 352 | err := pfo.HostFile.Hosts.Save() 353 | if err != nil { 354 | log.Error("Error saving hosts file", err) 355 | } 356 | pfo.HostFile.Unlock() 357 | } 358 | 359 | // removeHosts removes hosts /etc/hosts 360 | // associated with a forwarded pod 361 | func (pfo *PortForwardOpts) removeHosts() { 362 | 363 | // we should lock the pfo.HostFile here 364 | // because sometimes other goroutine write the *txeh.Hosts 365 | pfo.HostFile.Lock() 366 | // other applications or process may have written to /etc/hosts 367 | // since it was originally updated. 368 | err := pfo.HostFile.Hosts.Reload() 369 | if err != nil { 370 | log.Error("Unable to reload /etc/hosts: " + err.Error()) 371 | return 372 | } 373 | 374 | // remove all hosts 375 | for _, host := range pfo.Hosts { 376 | log.Debugf("Removing host %s for pod %s in namespace %s from context %s", host, pfo.PodName, pfo.Namespace, pfo.Context) 377 | pfo.HostFile.Hosts.RemoveHost(host) 378 | } 379 | 380 | // fmt.Printf("Delete Host And Save !\r\n") 381 | err = pfo.HostFile.Hosts.Save() 382 | if err != nil { 383 | log.Errorf("Error saving /etc/hosts: %s\n", err.Error()) 384 | } 385 | pfo.HostFile.Unlock() 386 | } 387 | 388 | // removeInterfaceAlias called on stop signal to 389 | func (pfo *PortForwardOpts) removeInterfaceAlias() { 390 | fwdnet.RemoveInterfaceAlias(pfo.LocalIp) 391 | } 392 | 393 | // Waiting for the pod running 394 | func (pfo *PortForwardOpts) WaitUntilPodRunning(stopChannel <-chan struct{}) (*v1.Pod, error) { 395 | pod, err := pfo.ClientSet.CoreV1().Pods(pfo.Namespace).Get(context.TODO(), pfo.PodName, metav1.GetOptions{}) 396 | if err != nil { 397 | return nil, err 398 | } 399 | 400 | if pod.Status.Phase == v1.PodRunning { 401 | return pod, nil 402 | } 403 | 404 | watcher, err := pfo.ClientSet.CoreV1().Pods(pfo.Namespace).Watch(context.TODO(), metav1.SingleObject(pod.ObjectMeta)) 405 | if err != nil { 406 | return nil, err 407 | } 408 | 409 | // if the os.signal (we enter the Ctrl+C) 410 | // or ManualStop (service delete or some thing wrong) 411 | // or RunningChannel channel (the watch for pod runnings is done) 412 | // or timeout after 300s(default) 413 | // we'll stop the watcher 414 | go func() { 415 | defer watcher.Stop() 416 | select { 417 | case <-stopChannel: 418 | case <-time.After(time.Duration(pfo.Timeout) * time.Second): 419 | } 420 | }() 421 | 422 | // watcher until the pod status is running 423 | for { 424 | event, ok := <-watcher.ResultChan() 425 | if !ok || event.Type == "ERROR" { 426 | break 427 | } 428 | if event.Object != nil && event.Type == "MODIFIED" { 429 | changedPod := event.Object.(*v1.Pod) 430 | if changedPod.Status.Phase == v1.PodRunning { 431 | return changedPod, nil 432 | } 433 | } 434 | } 435 | return nil, nil 436 | } 437 | 438 | // listen for pod is deleted 439 | func (pfo *PortForwardOpts) ListenUntilPodDeleted(stopChannel <-chan struct{}, pod *v1.Pod) { 440 | 441 | watcher, err := pfo.ClientSet.CoreV1().Pods(pfo.Namespace).Watch(context.TODO(), metav1.SingleObject(pod.ObjectMeta)) 442 | if err != nil { 443 | return 444 | } 445 | 446 | // Listen for stop signal from above 447 | go func() { 448 | <-stopChannel 449 | watcher.Stop() 450 | }() 451 | 452 | // watcher until the pod is deleted, then trigger a syncpodforwards 453 | for { 454 | event, ok := <-watcher.ResultChan() 455 | if !ok { 456 | break 457 | } 458 | switch event.Type { 459 | case watch.Modified: 460 | log.Warnf("Pod %s modified, service %s pod new status %v", pod.ObjectMeta.Name, pfo.ServiceFwd, pod) 461 | if (event.Object.(*v1.Pod)).DeletionTimestamp != nil { 462 | log.Warnf("Pod %s marked for deletion, resyncing the %s service pods.", pod.ObjectMeta.Name, pfo.ServiceFwd) 463 | pfo.Stop() 464 | pfo.ServiceFwd.SyncPodForwards(false) 465 | } 466 | //return 467 | case watch.Deleted: 468 | log.Warnf("Pod %s deleted, resyncing the %s service pods.", pod.ObjectMeta.Name, pfo.ServiceFwd) 469 | // TODO - Disconnect / reconnect on the provided port 470 | log.Warnf("Pod %s deleted, resyncing the %s service pods.", pod.ObjectMeta.Name, pfo.ServiceFwd) 471 | pfo.Stop() 472 | pfo.ServiceFwd.SyncPodForwards(false) 473 | } 474 | } 475 | } 476 | 477 | // Stop sends the shutdown signal to the port-forwarding process. 478 | // In case the shutdown signal was already given before, this is a no-op. 479 | func (pfo *PortForwardOpts) Stop() { 480 | select { 481 | case <-pfo.DoneChan: 482 | return 483 | case <-pfo.ManualStopChan: 484 | return 485 | default: 486 | } 487 | close(pfo.ManualStopChan) 488 | } 489 | -------------------------------------------------------------------------------- /pkg/fwdport/fwdport_test.go: -------------------------------------------------------------------------------- 1 | package fwdport 2 | 3 | import "testing" 4 | 5 | func Test_sanitizeHost(t *testing.T) { 6 | tests := []struct { 7 | contextName string 8 | want string 9 | }{ 10 | { // This is how Openshift generates context names 11 | contextName: "service-name.namespace.project-name/cluster-name:6443/username", 12 | want: "service-name-namespace-project-name-cluster-name-6443-username", 13 | }, 14 | {contextName: "-----test-----", want: "test"}, 15 | } 16 | for _, tt := range tests { 17 | t.Run("Sanitize hostname generated from context and namespace: "+tt.contextName, func(t *testing.T) { 18 | if got := sanitizeHost(tt.contextName); got != tt.want { 19 | t.Errorf("sanitizeHost() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/fwdpub/fwdpub.go: -------------------------------------------------------------------------------- 1 | package fwdpub 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Publisher 9 | type Publisher struct { 10 | Output bool 11 | PublisherName string 12 | ProducerName string 13 | } 14 | 15 | // MakeProducer 16 | func (p *Publisher) MakeProducer(producer string) Publisher { 17 | p.ProducerName = producer 18 | return *p 19 | } 20 | 21 | // Write 22 | func (p *Publisher) Write(b []byte) (int, error) { 23 | outputString := string(b) 24 | outputString = strings.TrimSuffix(outputString, "\n") 25 | 26 | if p.Output { 27 | fmt.Printf("Out: %s, %s, %s\n", p.PublisherName, p.ProducerName, outputString) 28 | } 29 | return 0, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/fwdservice/fwdservice.go: -------------------------------------------------------------------------------- 1 | package fwdservice 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/txn2/kubefwd/pkg/fwdIp" 12 | "github.com/txn2/kubefwd/pkg/fwdnet" 13 | "github.com/txn2/kubefwd/pkg/fwdport" 14 | "github.com/txn2/kubefwd/pkg/fwdpub" 15 | v1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/api/errors" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/client-go/kubernetes" 19 | _ "k8s.io/client-go/plugin/pkg/client/auth" 20 | restclient "k8s.io/client-go/rest" 21 | ) 22 | 23 | // ServiceFWD Single service to forward, with a reference to 24 | // all the pods being forwarded for it 25 | type ServiceFWD struct { 26 | ClientSet kubernetes.Clientset 27 | ListOptions metav1.ListOptions 28 | Hostfile *fwdport.HostFileWithLock 29 | ClientConfig restclient.Config 30 | RESTClient restclient.RESTClient 31 | 32 | // Context is a unique key (string) in kubectl config representing 33 | // a user/cluster combination. Kubefwd uses context as the 34 | // cluster name when forwarding to more than one cluster. 35 | Context string 36 | 37 | // Namespace is the current Kubernetes Namespace to locate services 38 | // and the pods that back them for port-forwarding 39 | Namespace string 40 | 41 | // Timeout is specify a timeout seconds for the port forwarding. 42 | Timeout int 43 | 44 | // ClusterN is the ordinal index of the cluster (from configuration) 45 | // cluster 0 is considered local while > 0 is remote 46 | ClusterN int 47 | 48 | // NamespaceN is the ordinal index of the namespace from the 49 | // perspective of the user. Namespace 0 is considered local 50 | // while > 0 is an external namespace 51 | NamespaceN int 52 | 53 | // FwdInc the forward increment for ip 54 | FwdInc *int 55 | 56 | // Domain is specified by the user and used in place of .local 57 | Domain string 58 | 59 | PodLabelSelector string // The label selector to query for matching pods. 60 | NamespaceServiceLock *sync.Mutex // 61 | Svc *v1.Service // Reference to the k8s service. 62 | 63 | // Headless service will forward all of the pods, 64 | // while normally only a single pod is forwarded. 65 | Headless bool 66 | 67 | LastSyncedAt time.Time // When was the set of pods last synced 68 | PortMap *[]PortMap // port map array. 69 | 70 | // Use debouncer for listing pods so we don't hammer the k8s when a bunch of changes happen at once 71 | SyncDebouncer func(f func()) 72 | 73 | // A mapping of all the pods currently being forwarded. 74 | // key = podName 75 | PortForwards map[string]*fwdport.PortForwardOpts 76 | DoneChannel chan struct{} // After shutdown is complete, this channel will be closed 77 | 78 | ForwardConfigurationPath string // file path to IP reservation configuration 79 | ForwardIPReservations []string // cli passed IP reservations 80 | } 81 | 82 | /* 83 | * 84 | add port map 85 | @url https://github.com/txn2/kubefwd/issues/121 86 | */ 87 | type PortMap struct { 88 | SourcePort string 89 | TargetPort string 90 | } 91 | 92 | // String representation of a ServiceFWD returns a unique name 93 | // in the form SERVICE_NAME.NAMESPACE.CONTEXT 94 | func (svcFwd *ServiceFWD) String() string { 95 | return svcFwd.Svc.Name + "." + svcFwd.Namespace + "." + svcFwd.Context 96 | } 97 | 98 | // GetPodsForService queries k8s and returns all pods backing this service 99 | // which are eligible for port-forwarding; exclude some pods which are in final/failure state. 100 | func (svcFwd *ServiceFWD) GetPodsForService() []v1.Pod { 101 | listOpts := metav1.ListOptions{LabelSelector: svcFwd.PodLabelSelector} 102 | 103 | pods, err := svcFwd.ClientSet.CoreV1().Pods(svcFwd.Svc.Namespace).List(context.TODO(), listOpts) 104 | 105 | if err != nil { 106 | if errors.IsNotFound(err) { 107 | log.Warnf("WARNING: No Pods found for service %s: %s\n", svcFwd, err.Error()) 108 | } else { 109 | log.Warnf("WARNING: Error in List pods for %s: %s\n", svcFwd, err.Error()) 110 | } 111 | return nil 112 | } 113 | 114 | podsEligible := make([]v1.Pod, 0, len(pods.Items)) 115 | 116 | for _, pod := range pods.Items { 117 | if pod.Status.Phase == v1.PodPending || pod.Status.Phase == v1.PodRunning { 118 | podsEligible = append(podsEligible, pod) 119 | } 120 | } 121 | 122 | return podsEligible 123 | } 124 | 125 | // SyncPodForwards selects one or all pods behind a service, and invokes 126 | // the forwarding setup for that or those pod(s). It will remove pods in-mem 127 | // that are no longer returned by k8s, should these not be correctly deleted. 128 | func (svcFwd *ServiceFWD) SyncPodForwards(force bool) { 129 | sync := func() { 130 | 131 | defer func() { svcFwd.LastSyncedAt = time.Now() }() 132 | 133 | k8sPods := svcFwd.GetPodsForService() 134 | 135 | // If no pods are found currently. Will try again next re-sync period. 136 | if len(k8sPods) == 0 { 137 | log.Warnf("WARNING: No Running Pods returned for service %s", svcFwd) 138 | return 139 | } 140 | 141 | // Check if the pods currently being forwarded still exist in k8s and if 142 | // they are not in a (pre-)running state, if not: remove them 143 | for _, podName := range svcFwd.ListServicePodNames() { 144 | keep := false 145 | for _, pod := range k8sPods { 146 | if podName == pod.Name && (pod.Status.Phase == v1.PodPending || pod.Status.Phase == v1.PodRunning) { 147 | keep = true 148 | break 149 | } 150 | } 151 | if !keep { 152 | svcFwd.RemoveServicePod(podName) 153 | } 154 | } 155 | 156 | // Set up port-forwarding for one or all of these pods normal service 157 | // port-forward the first pod as service name. headless service not only 158 | // forward first Pod as service name, but also port-forward all pods. 159 | if len(k8sPods) != 0 { 160 | 161 | // if this is a headless service forward the first pod from the 162 | // service name, then subsequent pods from their pod name 163 | if svcFwd.Headless { 164 | svcFwd.LoopPodsToForward([]v1.Pod{k8sPods[0]}, false) 165 | svcFwd.LoopPodsToForward(k8sPods, true) 166 | return 167 | } 168 | 169 | // Check if currently we are forwarding a pod which is good to keep using 170 | podNameToKeep := "" 171 | for _, podName := range svcFwd.ListServicePodNames() { 172 | if podNameToKeep != "" { 173 | break 174 | } 175 | for _, pod := range k8sPods { 176 | if podName == pod.Name && (pod.Status.Phase == v1.PodPending || pod.Status.Phase == v1.PodRunning) { 177 | podNameToKeep = pod.Name 178 | break 179 | } 180 | } 181 | } 182 | 183 | // Stop forwarding others, should there be. In case none of the currently 184 | // forwarded pods are good to keep, podNameToKeep will be the empty string, 185 | // and the comparison will mean we will remove all pods, which is the desired behaviour. 186 | for _, podName := range svcFwd.ListServicePodNames() { 187 | if podName != podNameToKeep { 188 | svcFwd.RemoveServicePod(podName) 189 | } 190 | } 191 | 192 | // If no good pod was being forwarded already, start one 193 | if podNameToKeep == "" { 194 | svcFwd.LoopPodsToForward([]v1.Pod{k8sPods[0]}, false) 195 | } 196 | } 197 | } 198 | // When a whole set of pods gets deleted at once, they all will trigger a SyncPodForwards() call. 199 | // This would hammer k8s with load needlessly. We therefore use a debouncer to only update pods 200 | // if things have been stable for at least a few seconds. However, if things never stabilize we 201 | // will still reload this information at least once every 5 minutes. 202 | if force || time.Since(svcFwd.LastSyncedAt) > 5*time.Minute { 203 | // Replace current debounced function with no-op 204 | svcFwd.SyncDebouncer(func() {}) 205 | 206 | // Do the syncing work 207 | sync() 208 | } else { 209 | // Queue sync 210 | svcFwd.SyncDebouncer(sync) 211 | } 212 | } 213 | 214 | // LoopPodsToForward starts the port-forwarding for each 215 | // pod in the given list 216 | func (svcFwd *ServiceFWD) LoopPodsToForward(pods []v1.Pod, includePodNameInHost bool) { 217 | publisher := &fwdpub.Publisher{ 218 | PublisherName: "Services", 219 | Output: false, 220 | } 221 | 222 | // Ip address handout is a critical section for synchronization, 223 | // use a lock which synchronizes inside each namespace. 224 | svcFwd.NamespaceServiceLock.Lock() 225 | defer svcFwd.NamespaceServiceLock.Unlock() 226 | 227 | for _, pod := range pods { 228 | // If pod is already configured to be forwarded, skip it 229 | if _, found := svcFwd.PortForwards[pod.Name]; found { 230 | continue 231 | } 232 | 233 | podPort := "" 234 | 235 | serviceHostName := svcFwd.Svc.Name 236 | svcName := svcFwd.Svc.Name 237 | 238 | if includePodNameInHost { 239 | serviceHostName = pod.Name + "." + svcFwd.Svc.Name 240 | svcName = pod.Name + "." + svcFwd.Svc.Name 241 | } 242 | 243 | opts := fwdIp.ForwardIPOpts{ 244 | ServiceName: svcName, 245 | PodName: pod.Name, 246 | Context: svcFwd.Context, 247 | ClusterN: svcFwd.ClusterN, 248 | NamespaceN: svcFwd.NamespaceN, 249 | Namespace: svcFwd.Namespace, 250 | Port: podPort, 251 | ForwardConfigurationPath: svcFwd.ForwardConfigurationPath, 252 | ForwardIPReservations: svcFwd.ForwardIPReservations, 253 | } 254 | localIp, err := fwdnet.ReadyInterface(opts) 255 | if err != nil { 256 | log.Warnf("WARNING: error readying interface: %s\n", err) 257 | } 258 | 259 | // if this is not the first namespace on the 260 | // first cluster then append the namespace 261 | if svcFwd.NamespaceN > 0 { 262 | serviceHostName = serviceHostName + "." + pod.Namespace 263 | } 264 | 265 | // if this is not the first cluster append the full 266 | // host name 267 | if svcFwd.ClusterN > 0 { 268 | serviceHostName = serviceHostName + "." + svcFwd.Context 269 | } 270 | 271 | for _, port := range svcFwd.Svc.Spec.Ports { 272 | 273 | // Skip if pod port protocol is UDP - not supported in k8s port forwarding yet, see https://github.com/kubernetes/kubernetes/issues/47862 274 | if port.Protocol == v1.ProtocolUDP { 275 | log.Warnf("WARNING: Skipped Port-Forward for %s:%d to pod %s:%s - k8s port-forwarding doesn't support UDP protocol\n", 276 | serviceHostName, 277 | port.Port, 278 | pod.Name, 279 | port.TargetPort.String(), 280 | ) 281 | continue 282 | } 283 | 284 | podPort = port.TargetPort.String() 285 | localPort := svcFwd.getPortMap(port.Port) 286 | p, err := strconv.ParseInt(localPort, 10, 32) 287 | if err != nil { 288 | log.Fatal(err) 289 | } 290 | port.Port = int32(p) 291 | if _, err := strconv.Atoi(podPort); err != nil { 292 | // search a pods containers for the named port 293 | if namedPodPort, ok := portSearch(podPort, pod.Spec.Containers); ok { 294 | podPort = namedPodPort 295 | } 296 | } 297 | 298 | log.Debugf("Resolving: %s to %s (%s)\n", 299 | serviceHostName, 300 | localIp.String(), 301 | svcName, 302 | ) 303 | 304 | log.Printf("Port-Forward: %16s %s:%d to pod %s:%s\n", 305 | localIp.String(), 306 | serviceHostName, 307 | port.Port, 308 | pod.Name, 309 | podPort, 310 | ) 311 | 312 | pfo := &fwdport.PortForwardOpts{ 313 | Out: publisher, 314 | Config: svcFwd.ClientConfig, 315 | ClientSet: svcFwd.ClientSet, 316 | RESTClient: svcFwd.RESTClient, 317 | Context: svcFwd.Context, 318 | Namespace: pod.Namespace, 319 | Service: svcName, 320 | ServiceFwd: svcFwd, 321 | PodName: pod.Name, 322 | PodPort: podPort, 323 | LocalIp: localIp, 324 | LocalPort: localPort, 325 | HostFile: svcFwd.Hostfile, 326 | ClusterN: svcFwd.ClusterN, 327 | NamespaceN: svcFwd.NamespaceN, 328 | Domain: svcFwd.Domain, 329 | 330 | ManualStopChan: make(chan struct{}), 331 | DoneChan: make(chan struct{}), 332 | } 333 | 334 | // Fire and forget. The stopping is done in the service.Shutdown() method. 335 | go func() { 336 | svcFwd.AddServicePod(pfo) 337 | if err := pfo.PortForward(); err != nil { 338 | select { 339 | case <-pfo.ManualStopChan: // if shutdown was given, we don't bother with the error. 340 | default: 341 | log.Errorf("PortForward error on %s: %s", pfo.PodName, err.Error()) 342 | } 343 | } else { 344 | select { 345 | case <-pfo.ManualStopChan: // if shutdown was given, don't log a warning as it's an intented stopping. 346 | default: 347 | log.Warnf("Stopped forwarding pod %s for %s", pfo.PodName, svcFwd) 348 | } 349 | } 350 | }() 351 | 352 | } 353 | 354 | } 355 | } 356 | 357 | // AddServicePod 358 | func (svcFwd *ServiceFWD) AddServicePod(pfo *fwdport.PortForwardOpts) { 359 | svcFwd.NamespaceServiceLock.Lock() 360 | ServicePod := pfo.Service + "." + pfo.PodName 361 | if _, found := svcFwd.PortForwards[ServicePod]; !found { 362 | svcFwd.PortForwards[ServicePod] = pfo 363 | } 364 | svcFwd.NamespaceServiceLock.Unlock() 365 | } 366 | 367 | // ListServicePodNames 368 | func (svcFwd *ServiceFWD) ListServicePodNames() []string { 369 | svcFwd.NamespaceServiceLock.Lock() 370 | currentPodNames := make([]string, 0, len(svcFwd.PortForwards)) 371 | for podName := range svcFwd.PortForwards { 372 | currentPodNames = append(currentPodNames, podName) 373 | } 374 | svcFwd.NamespaceServiceLock.Unlock() 375 | return currentPodNames 376 | } 377 | 378 | func (svcFwd *ServiceFWD) RemoveServicePod(servicePodName string) { 379 | if pod, found := svcFwd.PortForwards[servicePodName]; found { 380 | pod.Stop() 381 | <-pod.DoneChan 382 | svcFwd.NamespaceServiceLock.Lock() 383 | delete(svcFwd.PortForwards, servicePodName) 384 | svcFwd.NamespaceServiceLock.Unlock() 385 | } 386 | } 387 | 388 | func portSearch(portName string, containers []v1.Container) (string, bool) { 389 | for _, container := range containers { 390 | for _, cp := range container.Ports { 391 | if cp.Name == portName { 392 | return fmt.Sprint(cp.ContainerPort), true 393 | } 394 | } 395 | } 396 | 397 | return "", false 398 | } 399 | 400 | // port exist port map return 401 | func (svcFwd *ServiceFWD) getPortMap(port int32) string { 402 | p := strconv.Itoa(int(port)) 403 | if svcFwd.PortMap != nil { 404 | for _, portMapInfo := range *svcFwd.PortMap { 405 | if p == portMapInfo.SourcePort { 406 | //use map port 407 | return portMapInfo.TargetPort 408 | } 409 | } 410 | } 411 | return p 412 | } 413 | -------------------------------------------------------------------------------- /pkg/fwdsvcregistry/fwdsvcregistry.go: -------------------------------------------------------------------------------- 1 | package fwdsvcregistry 2 | 3 | import ( 4 | "sync" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/txn2/kubefwd/pkg/fwdservice" 8 | _ "k8s.io/client-go/plugin/pkg/client/auth" 9 | ) 10 | 11 | // ServicesRegistry is a structure to hold all of the kubernetes 12 | // services to do port-forwarding for. 13 | type ServicesRegistry struct { 14 | mutex *sync.Mutex 15 | services map[string]*fwdservice.ServiceFWD 16 | shutDownSignal <-chan struct{} 17 | doneSignal chan struct{} // indicates when all services were succesfully shutdown 18 | } 19 | 20 | var svcRegistry *ServicesRegistry 21 | 22 | // Init 23 | func Init(shutDownSignal <-chan struct{}) { 24 | svcRegistry = &ServicesRegistry{ 25 | mutex: &sync.Mutex{}, 26 | services: make(map[string]*fwdservice.ServiceFWD), 27 | shutDownSignal: shutDownSignal, 28 | doneSignal: make(chan struct{}), 29 | } 30 | 31 | go func() { 32 | <-svcRegistry.shutDownSignal 33 | ShutDownAll() 34 | close(svcRegistry.doneSignal) 35 | }() 36 | } 37 | 38 | // Done 39 | func Done() <-chan struct{} { 40 | if svcRegistry != nil { 41 | return svcRegistry.doneSignal 42 | } 43 | // No registry initialized, return a dummy channel then and close it ourselves 44 | ch := make(chan struct{}) 45 | close(ch) 46 | return ch 47 | } 48 | 49 | // Add will add this service to the registry of services configured to do forwarding 50 | // (if it wasn't already configured) and start the port-forwarding process. 51 | func Add(serviceFwd *fwdservice.ServiceFWD) { 52 | // If we are already shutting down, don't add a new service anymore. 53 | select { 54 | case <-svcRegistry.shutDownSignal: 55 | return 56 | default: 57 | } 58 | 59 | svcRegistry.mutex.Lock() 60 | defer svcRegistry.mutex.Unlock() 61 | 62 | if _, found := svcRegistry.services[serviceFwd.String()]; found { 63 | log.Debugf("Registry: found existing service %s", serviceFwd.String()) 64 | return 65 | } 66 | 67 | svcRegistry.services[serviceFwd.String()] = serviceFwd 68 | log.Debugf("Registry: Start forwarding service %s", serviceFwd) 69 | 70 | // Start port forwarding 71 | go serviceFwd.SyncPodForwards(false) 72 | 73 | // Schedule a re sync every x minutes to deal with potential connection errors. 74 | // @TODO review the need for this, if we keep it make if configurable 75 | // @TODO this causes the services to try and bind a second time to the local ports and fails --cjimti 76 | // 77 | //go func() { 78 | // for { 79 | // select { 80 | // case <-time.After(10 * time.Minute): 81 | // serviceFwd.SyncPodForwards(false) 82 | // case <-serviceFwd.DoneChannel: 83 | // return 84 | // } 85 | // } 86 | //}() 87 | } 88 | 89 | // SyncAll does a pod sync for all known services. 90 | //func SyncAll() { 91 | // // If we are already shutting down, don't sync services anymore. 92 | // select { 93 | // case <-svcRegistry.shutDownSignal: 94 | // return 95 | // default: 96 | // } 97 | // 98 | // for _, svc := range svcRegistry.services { 99 | // svc.SyncPodForwards(true) 100 | // } 101 | //} 102 | 103 | // ShutDownAll will shutdown all active services and remove them from the registry 104 | func ShutDownAll() { 105 | for name := range svcRegistry.services { 106 | RemoveByName(name) 107 | } 108 | log.Debugf("Registry: All services have shut down") 109 | } 110 | 111 | // RemoveByName will shutdown and remove the service, identified by svcName.svcNamespace, 112 | // from the inventory of services, if it was currently being configured to do forwarding. 113 | func RemoveByName(name string) { 114 | 115 | log.Debugf("Registry: Removing service %s", name) 116 | 117 | // Pop the service from the registry 118 | svcRegistry.mutex.Lock() 119 | serviceFwd, found := svcRegistry.services[name] 120 | if !found { 121 | log.Debugf("Registry: Did not find service %s.", name) 122 | svcRegistry.mutex.Unlock() 123 | return 124 | } 125 | delete(svcRegistry.services, name) 126 | svcRegistry.mutex.Unlock() 127 | 128 | // Synchronously stop the forwarding of all active pods in it 129 | activePodForwards := serviceFwd.ListServicePodNames() 130 | log.Debugf("Registry: Stopping service %s with %d port-forward(s)", serviceFwd, len(activePodForwards)) 131 | 132 | podsAllDone := &sync.WaitGroup{} 133 | podsAllDone.Add(len(activePodForwards)) 134 | for _, podName := range activePodForwards { 135 | go func(podName string) { 136 | serviceFwd.RemoveServicePod(podName) 137 | podsAllDone.Done() 138 | }(podName) 139 | } 140 | podsAllDone.Wait() 141 | 142 | // Signal that the service has shut down 143 | close(serviceFwd.DoneChannel) 144 | } 145 | -------------------------------------------------------------------------------- /pkg/utils/root.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | /* 4 | Copyright 2018 Craig Johnston 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package utils 19 | 20 | import ( 21 | "os/exec" 22 | "strconv" 23 | ) 24 | 25 | // CheckRoot determines if we have administrative privileges. 26 | func CheckRoot() (bool, error) { 27 | cmd := exec.Command("id", "-u") 28 | 29 | output, err := cmd.Output() 30 | if err != nil { 31 | return false, err 32 | } 33 | 34 | i, err := strconv.Atoi(string(output[:len(output)-1])) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | if i == 0 { 40 | return true, nil 41 | } 42 | 43 | return false, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/root_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | /* 4 | Copyright 2018 Craig Johnston 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | package utils 19 | 20 | import ( 21 | "github.com/pkg/errors" 22 | "golang.org/x/sys/windows" 23 | ) 24 | 25 | // CheckRoot determines if we have administrative privileges. 26 | // Ref: https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/ 27 | func CheckRoot() (bool, error) { 28 | var sid *windows.SID 29 | 30 | // Although this looks scary, it is directly copied from the 31 | // official windows documentation. The Go API for this is a 32 | // direct wrap around the official C++ API. 33 | // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership 34 | err := windows.AllocateAndInitializeSid( 35 | &windows.SECURITY_NT_AUTHORITY, 36 | 2, 37 | windows.SECURITY_BUILTIN_DOMAIN_RID, 38 | windows.DOMAIN_ALIAS_RID_ADMINS, 39 | 0, 0, 0, 0, 0, 0, 40 | &sid) 41 | if err != nil { 42 | return false, errors.Errorf("sid error: %s", err) 43 | } 44 | 45 | // This appears to cast a null pointer so I'm not sure why this 46 | // works, but this guy says it does and it Works for Me™: 47 | // https://github.com/golang/go/issues/28804#issuecomment-438838144 48 | token := windows.Token(0) 49 | 50 | member, err := token.IsMember(sid) 51 | if err != nil { 52 | return false, errors.Errorf("token membership error: %s", err) 53 | } 54 | 55 | return member, nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Craig Johnston 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 | package utils 17 | 18 | import ( 19 | "sync" 20 | 21 | "github.com/txn2/kubefwd/pkg/fwdport" 22 | ) 23 | 24 | var Lock sync.Mutex 25 | 26 | func ThreadSafeAppend(a []*fwdport.PortForwardOpts, b ...*fwdport.PortForwardOpts) []*fwdport.PortForwardOpts { 27 | Lock.Lock() 28 | c := append(a, b...) 29 | Lock.Unlock() 30 | return c 31 | } 32 | --------------------------------------------------------------------------------