├── .github ├── CODEOWNERS └── workflows │ ├── ci.yaml │ ├── codeql-analysis.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENCE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── proxy ├── create.go ├── create_test.go ├── delete.go ├── http.go ├── http_test.go ├── istio.go ├── istio_mock_test.go ├── istio_test.go ├── list.go ├── route.go ├── route_test.go ├── testdata └── routes_create.json ├── version.go └── version_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @harsimranmaan 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | golangci: 9 | name: golangci-lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: golangci-lint 14 | uses: golangci/golangci-lint-action@v2 15 | with: 16 | version: v1.43 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.17 25 | id: go 26 | 27 | - uses: actions/checkout@v2 28 | 29 | - name: Prepare env 30 | run: echo "GO_VERSION=$(go version | awk '{ print $3}' | sed 's/^go//')" >> $GITHUB_ENV 31 | 32 | - name: Test 33 | run: make test 34 | 35 | - name: Test coverage 36 | uses: codecov/codecov-action@v2 37 | 38 | - name: goreleaser 39 | uses: goreleaser/goreleaser-action@v2 40 | with: 41 | args: release --snapshot --skip-publish --rm-dist 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '31 15 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - "!*" 6 | tags: 7 | - "v*.*.*" 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: goreleaser 12 | steps: 13 | - name: Set up Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.17 17 | id: go 18 | - uses: actions/checkout@v2 19 | - name: Prepare env 20 | run: echo "GO_VERSION=$(go version | awk '{ print $3}' | sed 's/^go//')" >> $GITHUB_ENV 21 | - name: Login to docker 22 | run: docker login -u harsimranmaan -p ${{ secrets.DOCKER_CD_TOKEN }} 23 | - name: Release via goreleaser 24 | uses: goreleaser/goreleaser-action@v2 25 | with: 26 | args: release --rm-dist 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | bin/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: jupyterhub-istio-proxy 3 | binary: jupyterhub-istio-proxy 4 | main: ./ 5 | ldflags: 6 | - -X github.com/splunk/jupyterhub-istio-proxy/proxy.version={{.Version}} -X github.com/splunk/jupyterhub-istio-proxy/proxy.commit={{.Commit}} -X github.com/splunk/jupyterhub-istio-proxy/proxy.goVersion={{.Env.GO_VERSION}} 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - amd64 15 | archives: 16 | - format: tar.gz 17 | format_overrides: 18 | - goos: windows 19 | format: zip 20 | dockers: 21 | - dockerfile: Dockerfile 22 | image_templates: 23 | - "splunk/jupyterhub-istio-proxy:{{ .Version }}" 24 | 25 | checksum: 26 | name_template: "sha256-checksums.txt" 27 | snapshot: 28 | name_template: "{{.Version}}-next" 29 | changelog: 30 | sort: asc 31 | filters: 32 | exclude: 33 | - "README.md" 34 | - ".gitignore" 35 | - "^docs:" 36 | - "^test:" 37 | - "^chore:" 38 | - Merge pull request 39 | - Merge branch 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static-debian10:nonroot 2 | COPY jupyterhub-istio-proxy /proxy/jupyterhub-istio-proxy 3 | ENTRYPOINT ["/proxy/jupyterhub-istio-proxy"] 4 | CMD [ "/proxy/jupyterhub-istio-proxy" ] 5 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Splunk Authors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: lint 3 | go test -coverprofile=coverage.txt -covermode=atomic -race ./... 4 | 5 | .PHONY: lint 6 | lint: check-format vet 7 | go get golang.org/x/lint/golint 8 | golint -set_exit_status=1 ./... 9 | 10 | .PHONY: lint-local 11 | lint-local: lint 12 | golangci-lint run 13 | 14 | .PHONY: vet 15 | vet: 16 | go vet ./... 17 | 18 | .PHONY: check-format 19 | check-format: 20 | @echo "Running gofmt..." 21 | $(eval unformatted=$(shell find . -name '*.go' | grep -v ./.git | grep -v vendor | xargs gofmt -l)) 22 | $(if $(strip $(unformatted)),\ 23 | $(error $(\n) Some files are not formatted properly! Run: \ 24 | $(foreach file,$(unformatted),$(\n) gofmt -w $(file))$(\n)),\ 25 | @echo All files are well formatted.\ 26 | ) 27 | 28 | .PHONY: install-ci 29 | install-ci: 30 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.43.0 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | > :warning: **DEPRECATION NOTICE** 5 | As of June 24th, 2025: 6 | > (Latest Release 0.3.0). 7 | > - Timeline: 8 | > - ~ 60 days for GitHub Code Archive -> https://github.com/splunk/jupyterhub-istio-proxy 9 | > - ~ 30 days for DockerHub Image Removal -> https://hub.docker.com/repository/docker/splunk/jupyterhub-istio-proxy/general 10 | > - Maintenance: 11 | > - Anyone actively using this code please Fork it. 12 | > - Anyone interested in maintaining the Repository, raise a Pull Request for CODEOWNERS. 13 | > - We will then proceed to review the request internally. 14 | 15 | --- 16 | 17 | # jupyterhub-istio-proxy 18 | 19 | `jupyterhub-istio-proxy` is a scalable solution for Jupyterhub's high network traffic demands. It implements the jupyterhub proxy api to configure istio based on requests from hub. 20 | 21 | The following requests are supported: 22 | 1. `GET /api/routes`: Gets all routes that have been configured on istio 23 | 2. `POST /api/routes/`: Add the route to istio 24 | 3. `DELETE /api/routes/`: Remove the route from istio 25 | 26 | Since the proxy is stateless, it can be scaled horizontally. Multiple replicas can be used to ensure uptime during deployments and handle pod failure. 27 | 28 | ## Prerequisites 29 | 30 | In order to use the `jupyterhub-istio-proxy` the following prerequisites need to be met. 31 | 1. The Kubernetes cluster should have istio enabled. 32 | 2. If an [istio gateway](https://istio.io/latest/docs/reference/config/networking/gateway/) is used, it should be setup to handle traffic for the FQDN where the jupyterhub instance is exposed. 33 | 3. The service account used for deploying the `jupyterhub-istio-proxy` should have ability to list, get, create and delete istio virtual services in the namespace where the deployment is done. Refer [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole) for details. 34 | 35 | ### Difference to the configurable-http-proxy 36 | 37 | Unlike the default `configurable-http-proxy` that ships with Jupyterhub, the traffic is not routed through the proxy itself. The proxy configures istio to handle all traffic to the notebook servers as well as Jupyterhub. As a result, the `proxy-public` service is not needed when using `jupyterhub-istio-proxy`. For more information see https://medium.com/@harsimran.maan/running-jupyterhub-with-istio-service-mesh-on-kubernetes-a-troubleshooting-journey-707039f36a7b 38 | 39 | ## Deployment 40 | 41 | The proxy can be deployed to a Kubernetes namespace running Jupyterhub by applying the following config: 42 | Change SUB_DOMAIN_HOST to a value to a hostname where jupyterhub is hosted. The ISTIO_GATEWAY value should be set to 43 | the gateway which handles traffic for jupyterhub. If your cluster has a non-default domain, you can specify that with CLUSTER_DOMAIN 44 | 45 | ```yaml 46 | apiVersion: apps/v1 47 | kind: Deployment 48 | metadata: 49 | labels: 50 | app: jupyterhub 51 | component: proxy 52 | name: proxy 53 | spec: 54 | replicas: 3 55 | selector: 56 | matchLabels: 57 | app: jupyterhub 58 | component: proxy 59 | release: RELEASE-NAME 60 | strategy: 61 | type: RollingUpdate 62 | template: 63 | metadata: 64 | labels: 65 | app: jupyterhub 66 | component: proxy 67 | release: RELEASE-NAME 68 | spec: 69 | affinity: 70 | podAntiAffinity: 71 | preferredDuringSchedulingIgnoredDuringExecution: 72 | - podAffinityTerm: 73 | labelSelector: 74 | matchExpressions: 75 | - key: name 76 | operator: In 77 | values: 78 | - proxy 79 | topologyKey: kubernetes.io/hostname 80 | weight: 100 81 | containers: 82 | - command: 83 | - /proxy/jupyterhub-istio-proxy 84 | env: 85 | - name: CONFIGPROXY_AUTH_TOKEN 86 | valueFrom: 87 | secretKeyRef: 88 | key: proxy.token 89 | name: hub-secret 90 | - name: ISTIO_GATEWAY 91 | value: jupyterhub-gateway 92 | - name: K8S_NAMESPACE 93 | valueFrom: 94 | fieldRef: 95 | fieldPath: metadata.namespace 96 | - name: SUB_DOMAIN_HOST 97 | value: '*' 98 | - name: VIRTUAL_SERVICE_PREFIX 99 | value: jupyterhub 100 | - name: WAIT_FOR_WARMUP 101 | value: "true" 102 | - name: CLUSTER_DOMAIN 103 | value: "cluster.local" 104 | image: splunk/jupyterhub-istio-proxy:0.3.0 105 | imagePullPolicy: IfNotPresent 106 | name: proxy 107 | ports: 108 | - containerPort: 8000 109 | name: proxy-api 110 | protocol: TCP 111 | resources: 112 | limits: 113 | cpu: "1" 114 | memory: 256M 115 | requests: 116 | cpu: 100m 117 | memory: 256M 118 | securityContext: 119 | allowPrivilegeEscalation: false 120 | securityContext: 121 | runAsNonRoot: true 122 | terminationGracePeriodSeconds: 60 123 | --- 124 | apiVersion: v1 125 | kind: Service 126 | metadata: 127 | name: proxy-api 128 | spec: 129 | ports: 130 | - name: http-proxy-api 131 | port: 8001 132 | protocol: TCP 133 | targetPort: 8000 134 | selector: 135 | component: proxy 136 | type: ClusterIP 137 | --- 138 | ``` 139 | 140 | Jupyterhub user pod creation flow when using `jupyterhub-istio-proxy`. 141 | ![jupyterhub-istio-proxy](http://www.plantuml.com/plantuml/png/jPD1IyGm48Nl-HN3tkjUPG-onOkB5r74ewJDYD6r2PF9Ol-zq-bkR2k227jgoVlUPDwZtIQsnFbZR-gM0y5ZGWAR8ClJH95ywwFj65OtkLaDocjkvi9RZZqZoNdb4_jGHGgVlRBwzcoZdpjkSuFK8ME2-cwdvFjbKiuOcGFLrRTr0xLpi8Rxa1bDEHP6JONGk-7WARFTGq8w-1RXHJAjpH5SpDsT79mdZfRGChfoqzBrP3sFOu66bO03D0ZYSltS94asXJgD7OejuiDGldQjroDf-ccoQxMDI0nkr7y2ixm57g6oIn7AChzqhTp_-bRlUVhMqN_hV48keWwAzAvbWtxxw2w0q7d2bcNkCS4MEoT_nHS0) 142 | 143 | ## Dev Setup 144 | 145 | The project requires Go 1.17.3+. Run the tests 146 | 147 | ```bash 148 | make test 149 | ``` 150 | 151 | ## Testing setup 152 | 153 | https://github.com/golang/mock is used for creating mocks for testing. 154 | 155 | ```bash 156 | mockgen --source=proxy/istio.go -destination=proxy/istio_mock_test.go -write_package_comment -package=proxy 157 | ``` 158 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splunk/jupyterhub-istio-proxy 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/cenkalti/backoff/v4 v4.0.2 7 | github.com/gin-gonic/gin v1.7.0 8 | github.com/golang/mock v1.4.4 9 | istio.io/api v0.0.0-20200817160544-291eb3ba8ada 10 | istio.io/client-go v0.0.0-20200817160837-c5f8590ec455 11 | k8s.io/apimachinery v0.18.8 12 | k8s.io/client-go v0.18.8 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.13.0 // indirect 19 | github.com/go-playground/universal-translator v0.17.0 // indirect 20 | github.com/go-playground/validator/v10 v10.4.1 // indirect 21 | github.com/gogo/protobuf v1.3.1 // indirect 22 | github.com/golang/protobuf v1.3.5 // indirect 23 | github.com/google/gofuzz v1.1.0 // indirect 24 | github.com/googleapis/gnostic v0.1.0 // indirect 25 | github.com/json-iterator/go v1.1.9 // indirect 26 | github.com/leodido/go-urn v1.2.0 // indirect 27 | github.com/mattn/go-isatty v0.0.12 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.1 // indirect 30 | github.com/ugorji/go/codec v1.1.7 // indirect 31 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 32 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect 33 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect 34 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect 35 | golang.org/x/text v0.3.2 // indirect 36 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 37 | google.golang.org/appengine v1.5.0 // indirect 38 | gopkg.in/inf.v0 v0.9.1 // indirect 39 | gopkg.in/yaml.v2 v2.2.8 // indirect 40 | istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect 41 | k8s.io/api v0.18.8 // indirect 42 | k8s.io/klog v1.0.0 // indirect 43 | k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 // indirect 44 | sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect 45 | sigs.k8s.io/yaml v1.2.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 5 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 6 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 7 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 8 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 9 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 10 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 11 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 12 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 13 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 14 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 15 | github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= 16 | github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 19 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 24 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 25 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 26 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 27 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 29 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 30 | github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= 31 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 32 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 33 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 34 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 35 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 36 | github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= 37 | github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 38 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 39 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 40 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 41 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 42 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 43 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 44 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 45 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 46 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 47 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 48 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 49 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 50 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 51 | github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 52 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 53 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 54 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 55 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 56 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 57 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 58 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 59 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 60 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 64 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 65 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 66 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 67 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 68 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 69 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 70 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 71 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 72 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 73 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 74 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 75 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 76 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 78 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 79 | github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI= 80 | github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 81 | github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 82 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 83 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 84 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 85 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 86 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 87 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 88 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 89 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 90 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 91 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 92 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 93 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 94 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 95 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 96 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 97 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 98 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 99 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 100 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 101 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 102 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 103 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 104 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 105 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 109 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 110 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 111 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 112 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 113 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 114 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 115 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 116 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 117 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 118 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 119 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 120 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 121 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 122 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 123 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 124 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 125 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 126 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 127 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 128 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 129 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 130 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 131 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 132 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 133 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 134 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 135 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 136 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 137 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 138 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 139 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 140 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 141 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 142 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 143 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 144 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 145 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 146 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 147 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 148 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 149 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 150 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 151 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 152 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 153 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 154 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 155 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= 156 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 157 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 158 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 159 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 160 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 161 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 162 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 169 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 176 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 178 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 179 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 181 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 182 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 183 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 184 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= 185 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 186 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 188 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 189 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 190 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 191 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 192 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 193 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 194 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 195 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 196 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 197 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 198 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 199 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 200 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 201 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 202 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 203 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 204 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 205 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 206 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 207 | google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 208 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 209 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 210 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 211 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 212 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 213 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 214 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 215 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 216 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 217 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 218 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 219 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 220 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 221 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 222 | istio.io/api v0.0.0-20200812202721-24be265d41c3/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64= 223 | istio.io/api v0.0.0-20200817160544-291eb3ba8ada h1:rflIBGWjeFkAgD5T5Zi1a/rI/cjG8c4kfo3uR4NQt7k= 224 | istio.io/api v0.0.0-20200817160544-291eb3ba8ada/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64= 225 | istio.io/client-go v0.0.0-20200817160837-c5f8590ec455 h1:tNwGdAA1H3XVnwCXgc8YvRF2dvZ4y06qs+DkuYf29AI= 226 | istio.io/client-go v0.0.0-20200817160837-c5f8590ec455/go.mod h1:SO65MWt7I45dvUwuDowoiB0SVcGpfWZfUTlopvYpbZc= 227 | istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE= 228 | istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs= 229 | k8s.io/api v0.18.1/go.mod h1:3My4jorQWzSs5a+l7Ge6JBbIxChLnY8HnuT58ZWolss= 230 | k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4= 231 | k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= 232 | k8s.io/apimachinery v0.18.1/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= 233 | k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0= 234 | k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= 235 | k8s.io/client-go v0.18.1/go.mod h1:iCikYRiXOj/yRRFE/aWqrpPtDt4P2JVWhtHkmESTcfY= 236 | k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM= 237 | k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU= 238 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 239 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 240 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 241 | k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= 242 | k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 243 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 244 | k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= 245 | k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= 246 | k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 247 | k8s.io/utils v0.0.0-20200414100711-2df71ebbae66 h1:Ly1Oxdu5p5ZFmiVT71LFgeZETvMfZ1iBIGeOenT2JeM= 248 | k8s.io/utils v0.0.0-20200414100711-2df71ebbae66/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 249 | sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= 250 | sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= 251 | sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= 252 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 253 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 254 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 255 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 | "context" 20 | "fmt" 21 | "log" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/gin-gonic/gin" 29 | "github.com/splunk/jupyterhub-istio-proxy/proxy" 30 | ) 31 | 32 | var sharedAPIToken string 33 | var subDomainHost string 34 | var gateway string 35 | var namespace string 36 | var waitForWarmup bool 37 | var vsNamePrefix string 38 | var clusterDomain string 39 | 40 | const ( 41 | gatewayEnvKey = "ISTIO_GATEWAY" 42 | proxyAuthTokenEnvKey = "CONFIGPROXY_AUTH_TOKEN" 43 | subDomainHostEnvKey = "SUB_DOMAIN_HOST" 44 | namespaceKey = "K8S_NAMESPACE" 45 | waitForWarmupKey = "WAIT_FOR_WARMUP" 46 | virtualServicePrefixKey = "VIRTUAL_SERVICE_PREFIX" 47 | virtualServicePrefixDefault = "jupyter" 48 | clusterDomainEnvKey = "CLUSTER_DOMAIN" 49 | clusterDomainDefault = "cluster.local" 50 | ) 51 | 52 | func main() { 53 | log.Println(proxy.VersionInfo()) 54 | gin.SetMode(gin.ReleaseMode) 55 | 56 | sharedAPIToken = os.Getenv(proxyAuthTokenEnvKey) 57 | err := validateRequired(proxyAuthTokenEnvKey, sharedAPIToken) 58 | if err != nil { 59 | log.Fatalln(err) 60 | } 61 | 62 | gateway = os.Getenv(gatewayEnvKey) 63 | err = validateRequired(gatewayEnvKey, gateway) 64 | if err != nil { 65 | log.Fatalln(err) 66 | } 67 | namespace = os.Getenv(namespaceKey) 68 | err = validateRequired(namespaceKey, namespace) 69 | if err != nil { 70 | log.Fatalln(err) 71 | } 72 | subDomainHost = os.Getenv(subDomainHostEnvKey) 73 | err = validateRequired(subDomainHostEnvKey, subDomainHost) 74 | if err != nil { 75 | log.Fatalln(err) 76 | } 77 | waitForWarmup = os.Getenv(waitForWarmupKey) != "false" 78 | var ok bool 79 | if vsNamePrefix, ok = os.LookupEnv(virtualServicePrefixKey); !ok || vsNamePrefix == "" { 80 | vsNamePrefix = virtualServicePrefixDefault 81 | } 82 | if clusterDomain, ok = os.LookupEnv(clusterDomainEnvKey); !ok || clusterDomain == "" { 83 | clusterDomain = clusterDomainDefault 84 | } 85 | var ic proxy.Istioer 86 | ic, err = proxy.NewIstioClient(namespace, gateway, subDomainHost, waitForWarmup, vsNamePrefix, clusterDomain) 87 | if err != nil { 88 | log.Fatalf("failed to create istio client: %s\n", err) 89 | } 90 | r := gin.Default() 91 | proxy.RegisterRoutes(r, ic, sharedAPIToken) 92 | 93 | srv := &http.Server{ 94 | Addr: ":8000", 95 | Handler: r, 96 | } 97 | 98 | go func() { 99 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 100 | log.Fatalf("listen: %s\n", err) 101 | } 102 | }() 103 | 104 | quit := make(chan os.Signal, 1) 105 | // Handle signals 106 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 107 | <-quit 108 | log.Println("Shutting down server") 109 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 110 | defer cancel() 111 | if err := srv.Shutdown(ctx); err != nil { 112 | log.Fatal("Server Shutdown: ", err) 113 | } 114 | 115 | log.Println("Server exiting") 116 | } 117 | 118 | func validateRequired(paramName string, paramValue string) error { 119 | if paramValue == "" { 120 | return fmt.Errorf("missing required param %s", paramName) 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestValidateRequired(t *testing.T) { 6 | err := validateRequired("a", "val") 7 | if err != nil { 8 | t.Errorf("Unexpected error %v", err) 9 | } 10 | err = validateRequired("a", "") 11 | if err == nil { 12 | t.Fatal("Expected error but found nil") 13 | } 14 | expected := "missing required param a" 15 | if expected != err.Error() { 16 | t.Errorf("expected %q, found %q", expected, err.Error()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /proxy/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "path" 25 | "strings" 26 | "time" 27 | 28 | "github.com/cenkalti/backoff/v4" 29 | "istio.io/api/networking/v1alpha3" 30 | networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | ) 33 | 34 | const ( 35 | jupyterProxyIDHeaderKey string = "jupyter-proxy-id" 36 | maxRetries uint64 = 6 37 | ) 38 | 39 | func (i *IstioClient) createVirtualService(r route) error { 40 | log.Println("creating route: ", r) 41 | var annotations, err = annotationForRoute(i, r) 42 | if err != nil { 43 | return err 44 | } 45 | destinationHost, destinationPort := r.splitTarget() 46 | 47 | // If the destinationHost already has its complete fqdn we don't need to append namespace of the hub, service and cluster domain again. 48 | // This could be the case if the notebook pod is running in an other namespace than the hub pod 49 | // and c.KubeSpawner.pod_connect_ip needs to be set to something like "jupyter-{username}..svc.cluster.local" 50 | if !strings.HasSuffix(destinationHost, fmt.Sprintf(".svc.%s", i.clusterDomain)) { 51 | destinationHost = fmt.Sprintf("%s.%s.svc.%s", destinationHost, i.namespace, i.clusterDomain) 52 | } 53 | 54 | vsName := i.virtualServiceNameWithPrefix(r.RouteSpec) 55 | vs := virtualService(vsName, i.gateway, i.host, destinationHost, destinationPort, r.RouteSpec, annotations) 56 | 57 | _, err = i.NetworkingV1alpha3().VirtualServices(i.namespace).Create(context.Background(), vs, metav1.CreateOptions{}) 58 | if err != nil { 59 | if strings.Contains(err.Error(), "already exists") { 60 | log.Printf("virtual service creation skipped as %s already exists\n", vsName) 61 | return nil 62 | } 63 | log.Println("virtual service creation failed", err) 64 | return err 65 | } 66 | log.Println("virtual service created") 67 | if i.waitForWarmup { 68 | log.Println("waiting for warmup") 69 | err = warmup(vsName, warmupURL(i.host, r.RouteSpec)) 70 | } 71 | if err != nil { 72 | log.Printf("warming up the servers did not return after %d tries. Continuing despite: %v", maxRetries+1, err) 73 | return nil 74 | } 75 | return nil 76 | } 77 | 78 | func warmupURL(host string, p string) string { 79 | return fmt.Sprintf("https://%s/", path.Join(host, p)) 80 | } 81 | 82 | func annotationForRoute(i *IstioClient, r route) (map[string]string, error) { 83 | var e, err = encodeRoute(r) 84 | if err != nil { 85 | return nil, err 86 | } 87 | var m = make(map[string]string) 88 | m[i.virtualServiceAnnotationNameWithPrefix()] = e 89 | return m, nil 90 | } 91 | 92 | func warmup(name string, url string) error { 93 | client := &http.Client{ 94 | Timeout: 3 * time.Second, 95 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 96 | return http.ErrUseLastResponse 97 | }, 98 | } 99 | fetchURL := func() error { 100 | log.Println("GETing url to check if it is up", url) 101 | req, _ := http.NewRequest("GET", url, nil) 102 | resp, err := client.Do(req) 103 | if err != nil { 104 | log.Println("error GETing ", url, err) 105 | return err 106 | } 107 | defer resp.Body.Close() 108 | if name == resp.Header.Get(jupyterProxyIDHeaderKey) { 109 | log.Println("virtual service is warmed up") 110 | return nil 111 | } 112 | return fmt.Errorf("desired header `%s` not found", name) 113 | } 114 | bf := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries) 115 | return backoff.Retry(fetchURL, bf) 116 | } 117 | 118 | func virtualService(name string, gateway string, host string, destinationHost string, destinationPort uint32, route string, annotations map[string]string) *networkingv1alpha3.VirtualService { 119 | return &networkingv1alpha3.VirtualService{ 120 | ObjectMeta: metav1.ObjectMeta{ 121 | Name: name, 122 | Annotations: annotations, 123 | }, 124 | Spec: v1alpha3.VirtualService{ 125 | Hosts: []string{host}, 126 | Gateways: []string{gateway}, 127 | Http: []*v1alpha3.HTTPRoute{ 128 | { 129 | Match: []*v1alpha3.HTTPMatchRequest{ 130 | { 131 | Uri: &v1alpha3.StringMatch{ 132 | MatchType: &v1alpha3.StringMatch_Prefix{ 133 | Prefix: route, 134 | }, 135 | }, 136 | }, 137 | }, 138 | Route: []*v1alpha3.HTTPRouteDestination{ 139 | { 140 | Destination: &v1alpha3.Destination{ 141 | Host: destinationHost, 142 | Port: &v1alpha3.PortSelector{ 143 | Number: destinationPort, 144 | }, 145 | }, 146 | }, 147 | }, 148 | Headers: &v1alpha3.Headers{ 149 | Response: &v1alpha3.Headers_HeaderOperations{ 150 | Set: map[string]string{ 151 | jupyterProxyIDHeaderKey: name, 152 | }, 153 | }, 154 | }, 155 | }, 156 | }, 157 | }, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /proxy/create_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestVirtualServiceNameWithPrefix(t *testing.T) { 24 | var name = "/" 25 | ic := IstioClient{vsNamePrefix: "jupyter"} 26 | actual := ic.virtualServiceNameWithPrefix(name) 27 | expected := "jupyter-8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1" 28 | if expected != actual { 29 | t.Errorf("expected %q, found %q", expected, actual) 30 | } 31 | } 32 | 33 | func TestWarmupURL(t *testing.T) { 34 | actual := warmupURL("example.com", "/my-path") 35 | expected := "https://example.com/my-path/" 36 | if expected != actual { 37 | t.Errorf("expected %q, found %q", expected, actual) 38 | } 39 | } 40 | 41 | func TestVirtualService(t *testing.T) { 42 | name := "my-vs" 43 | gateway := "hsm/gateway" 44 | host := "example.com" 45 | service := "mylocalservice" 46 | port := uint32(80) 47 | path := "/my-path" 48 | annotations := map[string]string{"test/annotation": "test"} 49 | vs := virtualService(name, gateway, host, service, port, path, annotations) 50 | 51 | if name != vs.Name { 52 | t.Errorf("expected %q, found %q", name, vs.Name) 53 | } 54 | if annotations["test/annotation"] != vs.Annotations["test/annotation"] { 55 | t.Errorf("expected %q, found %q", annotations, vs.Annotations) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /proxy/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "context" 21 | 22 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | func (i *IstioClient) deleteRoute(path string) error { 26 | name := i.virtualServiceNameWithPrefix(path) 27 | return i.NetworkingV1alpha3().VirtualServices(i.namespace).Delete(context.Background(), name, v1.DeleteOptions{}) 28 | } 29 | -------------------------------------------------------------------------------- /proxy/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "log" 21 | "net/http" 22 | "strings" 23 | 24 | "github.com/gin-gonic/gin" 25 | ) 26 | 27 | // RegisterRoutes bootstraps http routes 28 | func RegisterRoutes(r *gin.Engine, ic Istioer, apiToken string) { 29 | r.GET("/ping", func(c *gin.Context) { 30 | c.JSON(200, gin.H{ 31 | "message": "pong", 32 | }) 33 | }) 34 | 35 | authorized := r.Group("/", authorizedWithToken(apiToken)) 36 | authorized.GET("/api/routes", func(c *gin.Context) { 37 | var routes, err = ic.listRegisteredRoutes() 38 | if err != nil { 39 | log.Println(err) 40 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ 41 | "error": "failed to get all routes", 42 | }) 43 | } 44 | c.JSON(200, routes) 45 | }) 46 | 47 | authorized.POST("/api/routes/*path", func(c *gin.Context) { 48 | path := c.Param("path") 49 | var r *route 50 | var err error 51 | if r, err = unmarshalRoute(path, c.Request.Body); err != nil { 52 | log.Println(err) 53 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ 54 | "error": "improper json body", 55 | }) 56 | } 57 | if err := ic.createVirtualService(*r); err != nil { 58 | log.Println(err) 59 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ 60 | "error": "error processing route creation request", 61 | }) 62 | } 63 | c.Status(http.StatusCreated) 64 | }) 65 | 66 | authorized.DELETE("/api/routes/*path", func(c *gin.Context) { 67 | path := c.Param("path") 68 | if err := ic.deleteRoute(path); err != nil { 69 | log.Println(err) 70 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ 71 | "error": "failed to delete route", 72 | }) 73 | } 74 | c.Status(http.StatusNoContent) 75 | }) 76 | } 77 | 78 | func authorizedWithToken(apiToken string) gin.HandlerFunc { 79 | return func(c *gin.Context) { 80 | reqToken := c.Request.Header.Get("Authorization") 81 | // Not bearer :o 82 | bearerSplit := strings.Split(reqToken, "token ") 83 | if len(bearerSplit) == 2 && bearerSplit[1] == apiToken { 84 | c.Next() 85 | return 86 | } 87 | c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ 88 | "error": "invalid credentials", 89 | }) 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /proxy/http_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "net/http/httptest" 24 | "testing" 25 | 26 | "github.com/gin-gonic/gin" 27 | gomock "github.com/golang/mock/gomock" 28 | ) 29 | 30 | const ( 31 | validAuthToken string = "authtoken" 32 | ) 33 | 34 | func TestPingNotForbidden(t *testing.T) { 35 | var tests = []struct { 36 | useHeader bool 37 | authHeaderValue string 38 | expectedStatus int 39 | }{ 40 | {false, "", http.StatusOK}, 41 | {true, "", http.StatusOK}, 42 | {true, "invalid", http.StatusOK}, 43 | {true, validAuthToken, http.StatusOK}, 44 | } 45 | gin.DefaultWriter = ioutil.Discard 46 | gin.SetMode(gin.ReleaseMode) 47 | r := gin.Default() 48 | mockCtrl := gomock.NewController(t) 49 | ic := NewMockIstioer(mockCtrl) 50 | RegisterRoutes(r, ic, validAuthToken) 51 | ts := httptest.NewServer(r) 52 | defer ts.Close() 53 | 54 | client := &http.Client{} 55 | for i, test := range tests { 56 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 57 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/ping", ts.URL), nil) 58 | if err != nil { 59 | t.Fatalf("unexpected error %v", err) 60 | } 61 | if test.useHeader { 62 | req.Header.Add("Authorization", fmt.Sprintf("token %s", test.authHeaderValue)) 63 | } 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | t.Errorf("unexpected error %v", err) 67 | } 68 | if test.expectedStatus != resp.StatusCode { 69 | t.Errorf("expected http status to be %d but found %d", test.expectedStatus, resp.StatusCode) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /proxy/istio.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "crypto/sha256" 21 | "fmt" 22 | 23 | versionedclient "istio.io/client-go/pkg/clientset/versioned" 24 | "k8s.io/client-go/rest" 25 | ) 26 | 27 | // Istioer abstracts interactions with the kubernetes API for istio objects 28 | type Istioer interface { 29 | createVirtualService(route) error 30 | listRegisteredRoutes() (map[string]interface{}, error) 31 | deleteRoute(string) error 32 | } 33 | 34 | // IstioClient is an implementation of Istioer 35 | type IstioClient struct { 36 | *versionedclient.Clientset 37 | gateway string 38 | host string 39 | namespace string 40 | waitForWarmup bool 41 | vsNamePrefix string 42 | clusterDomain string 43 | } 44 | 45 | // NewIstioClient returns a new IstioClient 46 | func NewIstioClient(namespace string, gateway string, host string, waitForWarmup bool, vsNamePrefix string, clusterDomain string) (*IstioClient, error) { 47 | // creates the in-cluster config 48 | config, err := rest.InClusterConfig() 49 | if err != nil { 50 | return nil, err 51 | } 52 | ic, err := versionedclient.NewForConfig(config) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return &IstioClient{Clientset: ic, namespace: namespace, gateway: gateway, host: host, waitForWarmup: waitForWarmup, vsNamePrefix: vsNamePrefix, clusterDomain: clusterDomain}, nil 57 | } 58 | func (c IstioClient) virtualServiceAnnotationNameWithPrefix() string { 59 | return fmt.Sprintf("%s.splunk.io/proxy-data", c.virtualServicePrefix()) 60 | } 61 | 62 | func (c IstioClient) virtualServicePrefix() string { 63 | return c.vsNamePrefix 64 | } 65 | 66 | func (c IstioClient) virtualServiceNameWithPrefix(name string) string { 67 | sum := sha256.Sum256([]byte(name)) 68 | return fmt.Sprintf("%s-%x", c.virtualServicePrefix(), sum) 69 | } 70 | -------------------------------------------------------------------------------- /proxy/istio_mock_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: proxy/istio.go 3 | 4 | // Package proxy is a generated GoMock package. 5 | package proxy 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | reflect "reflect" 10 | ) 11 | 12 | // MockIstioer is a mock of Istioer interface 13 | type MockIstioer struct { 14 | ctrl *gomock.Controller 15 | recorder *MockIstioerMockRecorder 16 | } 17 | 18 | // MockIstioerMockRecorder is the mock recorder for MockIstioer 19 | type MockIstioerMockRecorder struct { 20 | mock *MockIstioer 21 | } 22 | 23 | // NewMockIstioer creates a new mock instance 24 | func NewMockIstioer(ctrl *gomock.Controller) *MockIstioer { 25 | mock := &MockIstioer{ctrl: ctrl} 26 | mock.recorder = &MockIstioerMockRecorder{mock} 27 | return mock 28 | } 29 | 30 | // EXPECT returns an object that allows the caller to indicate expected use 31 | func (m *MockIstioer) EXPECT() *MockIstioerMockRecorder { 32 | return m.recorder 33 | } 34 | 35 | // createVirtualService mocks base method 36 | func (m *MockIstioer) createVirtualService(arg0 route) error { 37 | m.ctrl.T.Helper() 38 | ret := m.ctrl.Call(m, "createVirtualService", arg0) 39 | ret0, _ := ret[0].(error) 40 | return ret0 41 | } 42 | 43 | // createVirtualService indicates an expected call of createVirtualService 44 | func (mr *MockIstioerMockRecorder) createVirtualService(arg0 interface{}) *gomock.Call { 45 | mr.mock.ctrl.T.Helper() 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createVirtualService", reflect.TypeOf((*MockIstioer)(nil).createVirtualService), arg0) 47 | } 48 | 49 | // listRegisteredRoutes mocks base method 50 | func (m *MockIstioer) listRegisteredRoutes() (map[string]interface{}, error) { 51 | m.ctrl.T.Helper() 52 | ret := m.ctrl.Call(m, "listRegisteredRoutes") 53 | ret0, _ := ret[0].(map[string]interface{}) 54 | ret1, _ := ret[1].(error) 55 | return ret0, ret1 56 | } 57 | 58 | // listRegisteredRoutes indicates an expected call of listRegisteredRoutes 59 | func (mr *MockIstioerMockRecorder) listRegisteredRoutes() *gomock.Call { 60 | mr.mock.ctrl.T.Helper() 61 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "listRegisteredRoutes", reflect.TypeOf((*MockIstioer)(nil).listRegisteredRoutes)) 62 | } 63 | 64 | // deleteRoute mocks base method 65 | func (m *MockIstioer) deleteRoute(arg0 string) error { 66 | m.ctrl.T.Helper() 67 | ret := m.ctrl.Call(m, "deleteRoute", arg0) 68 | ret0, _ := ret[0].(error) 69 | return ret0 70 | } 71 | 72 | // deleteRoute indicates an expected call of deleteRoute 73 | func (mr *MockIstioerMockRecorder) deleteRoute(arg0 interface{}) *gomock.Call { 74 | mr.mock.ctrl.T.Helper() 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "deleteRoute", reflect.TypeOf((*MockIstioer)(nil).deleteRoute), arg0) 76 | } 77 | -------------------------------------------------------------------------------- /proxy/istio_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestVirtualServiceAnnotationNameWithPrefix(t *testing.T) { 24 | i := IstioClient{vsNamePrefix: "jupyter"} 25 | actual := i.virtualServiceAnnotationNameWithPrefix() 26 | expected := "jupyter.splunk.io/proxy-data" 27 | if expected != actual { 28 | t.Errorf("expected %q, found %q. Changing this expectation would require manual recycling of exisitng VS in a deployment", expected, actual) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /proxy/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "context" 21 | "log" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | func (i *IstioClient) listRegisteredRoutes() (map[string]interface{}, error) { 27 | vsList, err := i.NetworkingV1alpha3().VirtualServices(i.namespace).List(context.Background(), metav1.ListOptions{}) 28 | if err != nil { 29 | return nil, err 30 | } 31 | var routes = make(map[string]interface{}) 32 | for _, vs := range vsList.Items { 33 | if a, ok := vs.Annotations[i.virtualServiceAnnotationNameWithPrefix()]; ok { 34 | rd, err := decodeRoute(a) 35 | if err != nil { 36 | log.Printf("error decoding annotation but continuing: %s\n", vs.Name) 37 | continue 38 | } 39 | name, mp := marshalRoute(*rd) 40 | log.Printf("Added %q to list\n", name) 41 | routes[name] = mp 42 | 43 | } else { 44 | log.Printf("skipping vs as it does not have the required annotation: %s\n", vs.Name) 45 | } 46 | } 47 | 48 | return routes, nil 49 | } 50 | -------------------------------------------------------------------------------- /proxy/route.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "encoding/base64" 21 | "encoding/json" 22 | "io" 23 | "net/url" 24 | "strconv" 25 | ) 26 | 27 | type route struct { 28 | //the route specification ([host]/path/) 29 | RouteSpec string `json:"route_spec"` 30 | //the target host URL (proto://host) for this route 31 | Target string `json:"target"` 32 | // Whether the route is a JH route 33 | Jupyterhub bool `json:"jupyterhub,omitempty"` 34 | //the attached data dict for this route 35 | Data map[string]interface{} `json:"data,omitempty"` // Rest of the fields should go here. 36 | } 37 | 38 | func (r *route) splitTarget() (hostname string, port uint32) { 39 | u, err := url.Parse(r.Target) 40 | if err != nil { 41 | return "", port 42 | } 43 | 44 | p, _ := strconv.ParseUint(u.Port(), 10, 32) 45 | return u.Hostname(), uint32(p) 46 | } 47 | 48 | func unmarshalRoute(name string, in io.Reader) (*route, error) { 49 | var r route 50 | var m map[string]interface{} 51 | if err := json.NewDecoder(in).Decode(&m); err != nil { 52 | return nil, err 53 | } 54 | if n, ok := m["target"].(string); ok { 55 | r.Target = n 56 | } 57 | if n, ok := m["jupyterhub"].(bool); ok { 58 | r.Jupyterhub = n 59 | } 60 | r.RouteSpec = name 61 | delete(m, "jupyterhub") 62 | delete(m, "target") 63 | delete(m, "routespec") 64 | r.Data = m 65 | return &r, nil 66 | } 67 | 68 | func marshalRoute(r route) (name string, body map[string]interface{}) { 69 | var m = make(map[string]interface{}) 70 | name = r.RouteSpec 71 | m["target"] = r.Target 72 | m["jupyterhub"] = r.Jupyterhub 73 | if r.Data != nil { 74 | for k, v := range r.Data { 75 | m[k] = v 76 | } 77 | } 78 | return name, m 79 | } 80 | 81 | func encodeRoute(r route) (string, error) { 82 | var b, err = json.Marshal(r) 83 | if err != nil { 84 | return "", err 85 | } 86 | return base64.StdEncoding.EncodeToString(b), nil 87 | } 88 | 89 | func decodeRoute(s string) (*route, error) { 90 | var b, err = base64.StdEncoding.DecodeString(s) 91 | if err != nil { 92 | return nil, err 93 | } 94 | var r = route{} 95 | err = json.Unmarshal(b, &r) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return &r, nil 100 | } 101 | -------------------------------------------------------------------------------- /proxy/route_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "bufio" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func TestUnmarshalRoute(t *testing.T) { 26 | var f, err = os.Open("testdata/routes_create.json") 27 | if err != nil { 28 | t.Errorf("unexpected error %v", err) 29 | } 30 | var route = "/hsm/test" 31 | var in = bufio.NewReader(f) 32 | r, err := unmarshalRoute(route, in) 33 | if err != nil { 34 | t.Errorf("unexpected error %v", err) 35 | } 36 | if route != r.RouteSpec { 37 | t.Errorf("expected %q, found %q", route, r.RouteSpec) 38 | } 39 | if "http://jupyter-hsm:8888" != r.Target { 40 | t.Errorf("expected %q, found %q", "http://jupyter-hsm:8888", r.Target) 41 | } 42 | if !r.Jupyterhub { 43 | t.Errorf("expected %t, found %t", true, r.Jupyterhub) 44 | } 45 | if 2 != len(r.Data) { 46 | t.Errorf("expected %d, found %d", 2, len(r.Data)) 47 | } 48 | } 49 | 50 | func TestEncodeDecodeRoute(t *testing.T) { 51 | var r = route{} 52 | r.RouteSpec = "/hsm/test" 53 | r.Target = "http://jupyter-hsm:8888" 54 | r.Jupyterhub = true 55 | var m = make(map[string]interface{}) 56 | m["name"] = "hsm" 57 | r.Data = m 58 | 59 | s, err := encodeRoute(r) 60 | if err != nil { 61 | t.Errorf("unexpected error %v", err) 62 | } 63 | var expected = `eyJyb3V0ZV9zcGVjIjoiL2hzbS90ZXN0IiwidGFyZ2V0IjoiaHR0cDovL2p1cHl0ZXItaHNtOjg4ODgiLCJqdXB5dGVyaHViIjp0cnVlLCJkYXRhIjp7Im5hbWUiOiJoc20ifX0=` 64 | if expected != s { 65 | t.Errorf("expected %q, found %q", r.Target, s) 66 | } 67 | 68 | rd, err := decodeRoute(s) 69 | if err != nil { 70 | t.Errorf("unexpected error %v", err) 71 | } 72 | if r.RouteSpec != rd.RouteSpec { 73 | t.Errorf("expected %q, found %q", r.RouteSpec, rd.RouteSpec) 74 | } 75 | if r.Target != rd.Target { 76 | t.Errorf("expected %q, found %q", r.Target, rd.Target) 77 | } 78 | if r.Jupyterhub != rd.Jupyterhub { 79 | t.Errorf("expected %t, found %t", r.Jupyterhub, rd.Jupyterhub) 80 | } 81 | if r.Data["name"] != rd.Data["name"] { 82 | t.Errorf("expected %q, found %q", r.Data["name"], rd.Data["name"]) 83 | } 84 | } 85 | 86 | func TestMarshalRoute(t *testing.T) { 87 | var r = route{} 88 | r.RouteSpec = "/hsm/test" 89 | r.Target = "http://jupyter-hsm:8888" 90 | r.Jupyterhub = true 91 | var m = make(map[string]interface{}) 92 | m["name"] = "hsm" 93 | r.Data = m 94 | var name, body = marshalRoute(r) 95 | if r.RouteSpec != name { 96 | t.Errorf("expected %q, found %q", r.RouteSpec, name) 97 | } 98 | if r.Data["name"] != body["name"] { 99 | t.Errorf("expected %q, found %q", r.Data["name"], body["name"]) 100 | } 101 | if r.Target != body["target"] { 102 | t.Errorf("expected %q, found %q", r.Target, body["target"]) 103 | } 104 | if r.Jupyterhub != body["jupyterhub"] { 105 | t.Errorf("expected %t, found %t", r.Jupyterhub, body["jupyterhub"]) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /proxy/testdata/routes_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "http://jupyter-hsm:8888", 3 | "jupyterhub": true, 4 | "custom": { 5 | "name": "hsm" 6 | }, 7 | "someother": "something" 8 | } 9 | -------------------------------------------------------------------------------- /proxy/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import "fmt" 20 | 21 | var ( 22 | version = "dev" 23 | commit = "HEAD" 24 | goVersion = "Unknown" 25 | ) 26 | 27 | // VersionInfo returns a string representing the version 28 | func VersionInfo() string { 29 | return fmt.Sprintf("Version: %s Commit: %s GoVersion: %s", version, commit, goVersion) 30 | } 31 | -------------------------------------------------------------------------------- /proxy/version_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 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 proxy 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestVersionInfo(t *testing.T) { 24 | actual := VersionInfo() 25 | expected := "Version: dev Commit: HEAD GoVersion: Unknown" 26 | if expected != actual { 27 | t.Errorf("expected %q, found %q", expected, actual) 28 | } 29 | } 30 | --------------------------------------------------------------------------------