├── .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 | 
8 |
9 | [](https://travis-ci.com/txn2/kubefwd)
10 | [](https://github.com/txn2/kubefwd/blob/master/LICENSE)
11 | [](https://goreportcard.com/report/github.com/txn2/kubefwd)
12 | [](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 | 
23 |
24 |
25 |
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 | [](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 | 
6 |
7 | [](https://github.com/txn2/kubefwd/blob/master/LICENSE)
8 | [](https://codeclimate.com/github/txn2/kubefwd/maintainability)
9 | [](https://goreportcard.com/report/github.com/txn2/kubefwd)
10 | [](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 | 
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------