├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab ├── issue_templates │ └── issue_template.md └── merge_request_templates │ └── pull_request_template.md ├── .travis.yml ├── CHANGELOG ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── multikube │ └── main.go ├── deploy └── k8s.yaml ├── docs └── examples │ ├── README.md │ ├── dex-example.md │ ├── docker-example.md │ ├── jwt-auth-example.md │ ├── kubeconfig-example.md │ ├── kubernetes-example.md │ ├── non-tls-example.md │ └── tls-example.md ├── go.mod ├── go.sum └── pkg ├── cache ├── cache.go └── cache_test.go ├── proxy ├── empty.go ├── empty_test.go ├── header.go ├── header_test.go ├── jwt.go ├── jwt_test.go ├── logging.go ├── logging_test.go ├── metrics.go ├── oidc.go ├── oidc_test.go ├── proxy.go ├── proxy_test.go ├── rs256.go ├── rs256_test.go ├── transport.go └── transport_test.go └── server ├── server.go └── server_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 16 | 17 | 21 | **Description** 22 | 23 | 24 | 27 | **Steps to reproduce the issue:** 28 | 1. 29 | 2. 30 | 3. 31 | 32 | 33 | 37 | **Describe the results you received:** 38 | 39 | 40 | **Describe the results you expected:** 41 | 42 | 43 | 46 | **Additional information:** 47 | 48 | 49 | **Output of `multikube --version`:** 50 | 51 | ``` 52 | (paste your output here) 53 | ``` 54 | 55 | 59 | **Additional environment details:** -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature/enhancement 4 | 5 | --- 6 | 16 | 17 | 20 | - [ ] This feature/enhancement benefits everyone 21 | 22 | 23 | 27 | **Description** 28 | 29 | 32 | **Additional information** -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | **- What I did** 11 | 12 | **- How I did it** 13 | 14 | **- How to verify it** 15 | 16 | 20 | **- Description for the CHANGELOG** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | out/ 3 | bin/ 4 | coverage/ 5 | test/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | image: golang:1.12 3 | 4 | services: 5 | - docker:dind 6 | 7 | cache: 8 | key: ${CI_COMMIT_REF_SLUG} 9 | paths: 10 | - .cache 11 | 12 | before_script: 13 | - mkdir -p .cache 14 | - export GOPATH="$CI_PROJECT_DIR/.cache" 15 | - make dep 16 | 17 | stages: 18 | - test 19 | - build 20 | - release 21 | 22 | verify: 23 | stage: test 24 | script: 25 | - make checkfmt 26 | - make fmt 27 | - make vet 28 | - make race 29 | #- make msan 30 | - make gocyclo 31 | - make golint 32 | - make ineffassign 33 | - make misspell 34 | 35 | unit_test: 36 | stage: test 37 | script: 38 | - make test 39 | artifacts: 40 | paths: 41 | - coverage/ 42 | 43 | compile: 44 | stage: build 45 | script: 46 | - make 47 | artifacts: 48 | paths: 49 | - bin/multikube-linux-amd64 50 | - bin/multikube-linux-arm 51 | - bin/multikube-darwin-amd64 52 | - bin/multikube-windows-amd64.exe 53 | 54 | docker_hub: 55 | image: docker:18 56 | stage: release 57 | before_script: 58 | - '' 59 | script: 60 | - apk add --update make git 61 | - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY 62 | - make docker_build 63 | - make docker_push 64 | only: 65 | - tags 66 | except: 67 | - branches -------------------------------------------------------------------------------- /.gitlab/issue_templates/issue_template.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | **Description** 13 | 14 | 18 | 19 | 22 | **Steps to reproduce the issue:** 23 | 1. 24 | 2. 25 | 3. 26 | 27 | 28 | 32 | **Describe the results you received:** 33 | 34 | 35 | **Describe the results you expected:** 36 | 37 | 38 | **Additional information you deem important (e.g. issue happens only occasionally):** 39 | 40 | 41 | **Output of `multikube --version`:** 42 | 43 | ``` 44 | (paste your output here) 45 | ``` 46 | 47 | **Additional environment details (AWS, VirtualBox, physical, etc.):** -------------------------------------------------------------------------------- /.gitlab/merge_request_templates/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | **- What I did** 11 | 12 | **- How I did it** 13 | 14 | **- How to verify it** 15 | 16 | **- Description for the CHANGELOG** 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: xenial 3 | language: go 4 | 5 | go: 6 | - "1.13" 7 | 8 | stages: 9 | - test 10 | - build 11 | - deploy 12 | 13 | jobs: 14 | include: 15 | - stage: test 16 | name: "Verify" 17 | script: 18 | - make checkfmt 19 | - make fmt 20 | - make vet 21 | - make race 22 | - make gocyclo 23 | - make lint 24 | - make ineffassign 25 | - make misspell 26 | 27 | - name: "Unit Test" 28 | script: 29 | - make test 30 | - make benchmark 31 | - make coverage 32 | - bash <(curl -s https://codecov.io/bash) 33 | 34 | - name: "Compile" 35 | stage: build 36 | script: 37 | - make 38 | 39 | - stage: deploy 40 | name: "GitHub Releases" 41 | script: 42 | - GOOS=linux GOARCH=amd64 BUILDPATH=./bin/multikube-linux-amd64 make 43 | - GOOS=linux GOARCH=arm BUILDPATH=./bin/multikube-linux-arm make 44 | - GOOS=linux GOARCH=arm64 BUILDPATH=./bin/multikube-linux-arm64 make 45 | - GOOS=windows GOARCH=amd64 BUILDPATH=./bin/multikube-windows-amd64.exe make 46 | - GOOS=darwin GOARCH=amd64 BUILDPATH=./bin/multikube-darwin-amd64 make 47 | deploy: 48 | provider: releases 49 | api_key: ${GITHUB_API_KEY} 50 | file: 51 | - bin/multikube-linux-amd64 52 | - bin/multikube-linux-arm 53 | - bin/multikube-linux-arm64 54 | - bin/multikube-windows-amd64.exe 55 | - bin/multikube-darwin-amd64 56 | skip_cleanup: true 57 | draft: true 58 | on: 59 | tags: true -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amimof/multikube/4b8b62fcad4e7767b07194c8f697fb47ed3060c8/CHANGELOG -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build-env 2 | RUN apk add --no-cache git make ca-certificates 3 | LABEL maintaner="@amimof (amir.mofasser@gmail.com)" 4 | COPY . /go/src/github.com/amimof/multikube 5 | WORKDIR /go/src/github.com/amimof/multikube 6 | RUN make 7 | 8 | FROM scratch 9 | COPY --from=build-env /go/src/github.com/amimof/multikube/bin/multikube /go/bin/multikube 10 | COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 11 | ENTRYPOINT ["/go/bin/multikube"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Amir Mofasser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 2 | DATE ?= $(shell date +%FT%T%z) 3 | VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \ 4 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 5 | COMMIT=$(shell git rev-parse HEAD) 6 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 7 | GOVERSION=$(shell go version | awk -F\go '{print $$3}' | awk '{print $$1}') 8 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 9 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ 10 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 11 | $(PKGS)) 12 | BUILDPATH ?= $(BIN)/$(shell basename $(MODULE)) 13 | SRC_FILES=find . -name "*.go" -type f -not -path "./vendor/*" -not -path "./.git/*" -not -path "./.cache/*" -print0 | xargs -0 14 | BIN = $(CURDIR)/bin 15 | TBIN = $(CURDIR)/test/bin 16 | GO = go 17 | TIMEOUT = 15 18 | V = 0 19 | Q = $(if $(filter 1,$V),,@) 20 | M = $(shell printf "\033[34;1m➜\033[0m") 21 | 22 | export GO111MODULE=on 23 | export CGO_ENABLED=0 24 | 25 | # Build 26 | 27 | .PHONY: all 28 | all: | $(BIN) ; $(info $(M) building executable to $(BUILDPATH)) @ ## Build program binary 29 | $Q $(GO) build \ 30 | -tags release \ 31 | -ldflags '-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BRANCH=${BRANCH} -X main.GOVERSION=${GOVERSION}' \ 32 | -o $(BUILDPATH) cmd/multikube/main.go 33 | 34 | # Tools 35 | 36 | $(BIN): 37 | @mkdir -p $(BIN) 38 | $(TBIN): 39 | @mkdir -p $@ 40 | $(TBIN)/%: | $(TBIN) ; $(info $(M) building $(PACKAGE)) 41 | $Q tmp=$$(mktemp -d); \ 42 | env GO111MODULE=off GOPATH=$$tmp GOBIN=$(TBIN) $(GO) get $(PACKAGE) \ 43 | || ret=$$?; \ 44 | rm -rf $$tmp ; exit $$ret 45 | 46 | GOLINT = $(TBIN)/golint 47 | $(BIN)/golint: PACKAGE=golang.org/x/lint/golint 48 | 49 | GOCYCLO = $(TBIN)/gocyclo 50 | $(TBIN)/gocyclo: PACKAGE=github.com/fzipp/gocyclo 51 | 52 | INEFFASSIGN = $(TBIN)/ineffassign 53 | $(TBIN)/ineffassign: PACKAGE=github.com/gordonklaus/ineffassign 54 | 55 | MISSPELL = $(TBIN)/misspell 56 | $(TBIN)/misspell: PACKAGE=github.com/client9/misspell/cmd/misspell 57 | 58 | GOLINT = $(TBIN)/golint 59 | $(TBIN)/golint: PACKAGE=golang.org/x/lint/golint 60 | 61 | GOCOV = $(TBIN)/gocov 62 | $(TBIN)/gocov: PACKAGE=github.com/axw/gocov/... 63 | 64 | # Tests 65 | 66 | .PHONY: lint 67 | lint: | $(GOLINT) ; $(info $(M) running golint) @ ## Runs the golint command 68 | $Q $(GOLINT) -set_exit_status $(PKGS) 69 | 70 | .PHONY: gocyclo 71 | gocyclo: | $(GOCYCLO) ; $(info $(M) running gocyclo) @ ## Calculates cyclomatic complexities of functions in Go source code 72 | $Q $(GOCYCLO) -over 25 . 73 | 74 | .PHONY: ineffassign 75 | ineffassign: | $(INEFFASSIGN) ; $(info $(M) running ineffassign) @ ## Detects ineffectual assignments in Go code 76 | $Q $(INEFFASSIGN) . 77 | 78 | .PHONY: misspell 79 | misspell: | $(MISSPELL) ; $(info $(M) running misspell) @ ## Finds commonly misspelled English words 80 | $Q $(MISSPELL) . 81 | 82 | .PHONY: test 83 | test: ; $(info $(M) running go test) @ ## Runs unit tests 84 | $Q $(GO) test -v ${PKGS} 85 | 86 | .PHONY: fmt 87 | fmt: ; $(info $(M) running gofmt) @ ## Formats Go code 88 | $Q $(GO) fmt $(PKGS) 89 | 90 | .PHONY: vet 91 | vet: ; $(info $(M) running go vet) @ ## Examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 92 | $Q $(GO) vet $(PKGS) 93 | 94 | .PHONY: race 95 | race: ; $(info $(M) running go race) @ ## Runs tests with data race detection 96 | $Q CGO_ENABLED=1 $(GO) test -race -short $(PKGS) 97 | 98 | .PHONY: benchmark 99 | benchmark: ; $(info $(M) running go benchmark test) @ ## Benchmark tests to examine performance 100 | $Q $(GO) test -run=__absolutelynothing__ -bench=. $(PKGS) 101 | 102 | .PHONY: coverage 103 | coverage: ; $(info $(M) running go coverage) @ ## Runs tests and generates code coverage report at ./test/coverage.out 104 | $Q mkdir -p $(CURDIR)/test/ 105 | $Q $(GO) test -coverprofile="$(CURDIR)/test/coverage.out" $(PKGS) 106 | 107 | .PHONY: checkfmt 108 | checkfmt: ; $(info $(M) running checkfmt) @ ## Checks if code is formatted with go fmt and errors out if not 109 | @test "$(shell $(SRC_FILES) gofmt -l)" = "" \ 110 | || { echo "Code not formatted, please run 'make fmt'"; exit 2; } 111 | 112 | # Misc 113 | 114 | .PHONY: help 115 | help: 116 | @grep -hE '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 117 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m∙ %s:\033[0m %s\n", $$1, $$2}' 118 | 119 | .PHONY: version 120 | version: ## Print version information 121 | @echo App: $(VERSION) 122 | @echo Go: $(GOVERSION) 123 | @echo Commit: $(COMMIT) 124 | @echo Branch: $(BRANCH) 125 | 126 | .PHONY: clean 127 | clean: ; $(info $(M) cleaning) @ ## Cleanup everything 128 | @rm -rfv $(BIN) 129 | @rm -rfv $(TBIN) 130 | @rm -rfv $(CURDIR)/test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multikube 2 | [![Build Status](https://travis-ci.org/amimof/multikube.svg?branch=master)](https://travis-ci.org/amimof/multikube) [![huego](https://godoc.org/github.com/amimof/multikube?status.svg)](https://godoc.org/github.com/amimof/multikube) [![Go Report Card](https://goreportcard.com/badge/github.com/amimof/multikube)](https://goreportcard.com/report/github.com/amimof/multikube) [![codecov](https://codecov.io/gh/amimof/multikube/branch/master/graph/badge.svg)](https://codecov.io/gh/amimof/multikube) 3 | 4 | Multikube is a modern HTTP reverse proxy for [Kubernetes](http://kubernetes.io/) API server. 5 | 6 | ## Features 7 | * Validates JSON Web Tokens (JWT) 8 | * OIDC (OpenID Connect) provider support 9 | * Off-loads API calls by re-using connections and serving data from cache 10 | * Split network security domains 11 | * Total transparency means compatibility with any kubectl command 12 | * Minimal configuration required 13 | * Audit logs 14 | * Prometheus metrics 15 | * No database or dependencies makes scaling multikube a breeze 16 | * Configured with a kubeconfig 17 | 18 | ## Overview 19 | 20 | A client, wether it is kubectl, cURL or a browser, may make requests to Multikube as if it was a Kubernetes API. Multikube will validate the client access token and intelligently route the request to an upstream API-server, impersonating that user. The client can target a cluster, or *context*, using either a URL path or an HTTP header. 21 | 22 | Multikube communicates with Kubernetes clusters over separate TCP connections than those established from clients to Multikube. This means that connections can be re-used, shared and cached for better performance. It also means that client connections are never used to communicate with the Kubernetes API. 23 | 24 | As an example, kubectl uses a context that is configured to use the server `https://127.0.0.1:6443/k8s-dev-cluster` which happens to be Multikube running locally. Note the leading path in the URL which is the context name. Multikube will try to match that path with a context in it's kubeconfig. All traffic from kubectl will be routed through Multikube to the apiserver k8s-dev-cluster. 25 | 26 | ## Getting started 27 | 28 | Download the latest binary from the [release page](https://github.com/amimof/multikube/releases) for your target platform. Below is for Linux. 29 | ``` 30 | curl -LOs https://github.com/amimof/multikube/releases/latest/download/multikube-linux-amd64 31 | ``` 32 | 33 | Or use the official Docker scratch image 34 | ``` 35 | docker pull amimof/multikube:latest 36 | ``` 37 | 38 | ## Configuration 39 | 40 | You configure Multikube on the command line. There is no configuration file. However Multikube needs a [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) in order to communicate with upstream API servers. The kubeconfig is where you will configure the clusters (contexts) that Multikube will use for routing. 41 | 42 | ## Examples 43 | 44 | Examples are found under [docs/examples](https://github.com/amimof/multikube/blob/master/docs/examples) 45 | 46 | ## Contributing 47 | 48 | Multikube has not yet exited alpha. It is still under heavy development. All help in any form is highly appreciated and your are welcome participate in developing together. To contribute submit a Pull Request. If you want to provide feedback, open up a Github Issue or contact me personally. -------------------------------------------------------------------------------- /cmd/multikube/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | "github.com/SermoDigital/jose/crypto" 9 | "github.com/amimof/multikube/pkg/proxy" 10 | "github.com/amimof/multikube/pkg/server" 11 | "github.com/opentracing/opentracing-go" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | "github.com/spf13/pflag" 15 | "github.com/uber/jaeger-client-go" 16 | "github.com/uber/jaeger-client-go/config" 17 | "io/ioutil" 18 | "k8s.io/client-go/tools/clientcmd" 19 | "log" 20 | "os" 21 | "time" 22 | ) 23 | 24 | var ( 25 | // VERSION of the app. Is set when project is built and should never be set manually 26 | VERSION string 27 | // COMMIT is the Git commit currently used when compiling. Is set when project is built and should never be set manually 28 | COMMIT string 29 | // BRANCH is the Git branch currently used when compiling. Is set when project is built and should never be set manually 30 | BRANCH string 31 | // GOVERSION used to compile. Is set when project is built and should never be set manually 32 | GOVERSION string 33 | 34 | enabledListeners []string 35 | cleanupTimeout time.Duration 36 | maxHeaderSize uint64 37 | 38 | socketPath string 39 | 40 | host string 41 | port int 42 | metricsHost string 43 | metricsPort int 44 | listenLimit int 45 | keepAlive time.Duration 46 | readTimeout time.Duration 47 | writeTimeout time.Duration 48 | 49 | oidcPollInterval time.Duration 50 | oidcIssuerURL string 51 | oidcUsernameClaim string 52 | oidcCaFile string 53 | oidcInsecureSkipVerify bool 54 | tlsHost string 55 | tlsPort int 56 | tlsListenLimit int 57 | tlsKeepAlive time.Duration 58 | tlsReadTimeout time.Duration 59 | tlsWriteTimeout time.Duration 60 | tlsCertificate string 61 | tlsCertificateKey string 62 | tlsCACertificate string 63 | 64 | rs256PublicKey string 65 | kubeconfigPath string 66 | cacheTTL time.Duration 67 | ) 68 | 69 | func init() { 70 | pflag.StringVar(&socketPath, "socket-path", "/var/run/multikube.sock", "the unix socket to listen on") 71 | pflag.StringVar(&host, "host", "localhost", "The host address on which to listen for the --port port") 72 | pflag.StringVar(&tlsHost, "tls-host", "localhost", "The host address on which to listen for the --tls-port port") 73 | pflag.StringVar(&tlsCertificate, "tls-certificate", "", "the certificate to use for secure connections") 74 | pflag.StringVar(&tlsCertificateKey, "tls-key", "", "the private key to use for secure conections") 75 | pflag.StringVar(&tlsCACertificate, "tls-ca", "", "the certificate authority file to be used with mutual tls auth") 76 | pflag.StringVar(&rs256PublicKey, "rs256-public-key", "", "the RS256 public key used to validate the signature of client JWT's") 77 | pflag.StringVar(&kubeconfigPath, "kubeconfig", "/etc/multikube/kubeconfig", "absolute path to a kubeconfig file") 78 | pflag.StringVar(&metricsHost, "metrics-host", "localhost", "The host address on which to listen for the --metrics-port port") 79 | pflag.StringVar(&oidcIssuerURL, "oidc-issuer-url", "", "The URL of the OpenID issuer, only HTTPS scheme will be accepted. If set, it will be used to verify the OIDC JSON Web Token (JWT)") 80 | pflag.StringVar(&oidcUsernameClaim, "oidc-username-claim", "sub", " The OpenID claim to use as the user name. Note that claims other than the default is not guaranteed to be unique and immutable") 81 | pflag.StringVar(&oidcCaFile, "oidc-ca-file", "", "the certificate authority file to be used for verifyign the OpenID server") 82 | pflag.StringSliceVar(&enabledListeners, "scheme", []string{"https"}, "the listeners to enable, this can be repeated and defaults to the schemes in the swagger spec") 83 | 84 | pflag.IntVar(&port, "port", 8080, "the port to listen on for insecure connections, defaults to 8080") 85 | pflag.IntVar(&tlsPort, "tls-port", 8443, "the port to listen on for secure connections, defaults to 8443") 86 | pflag.IntVar(&metricsPort, "metrics-port", 8888, "the port to listen on for Prometheus metrics, defaults to 8888") 87 | pflag.IntVar(&listenLimit, "listen-limit", 0, "limit the number of outstanding requests") 88 | pflag.IntVar(&tlsListenLimit, "tls-listen-limit", 0, "limit the number of outstanding requests") 89 | pflag.Uint64Var(&maxHeaderSize, "max-header-size", 1000000, "controls the maximum number of bytes the server will read parsing the request header's keys and values, including the request line. It does not limit the size of the request body") 90 | 91 | pflag.DurationVar(&cleanupTimeout, "cleanup-timeout", 10*time.Second, "grace period for which to wait before shutting down the server") 92 | pflag.DurationVar(&keepAlive, "keep-alive", 3*time.Minute, "sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)") 93 | pflag.DurationVar(&readTimeout, "read-timeout", 30*time.Second, "maximum duration before timing out read of the request") 94 | pflag.DurationVar(&writeTimeout, "write-timeout", 30*time.Second, "maximum duration before timing out write of the response") 95 | pflag.DurationVar(&tlsKeepAlive, "tls-keep-alive", 3*time.Minute, "sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)") 96 | pflag.DurationVar(&tlsReadTimeout, "tls-read-timeout", 30*time.Second, "maximum duration before timing out read of the request") 97 | pflag.DurationVar(&tlsWriteTimeout, "tls-write-timeout", 30*time.Second, "maximum duration before timing out write of the response") 98 | pflag.DurationVar(&oidcPollInterval, "oidc-poll-interval", 2*time.Second, "maximum duration between intervals in which the oidc issuer url (--oidc-issuer-url) is polled") 99 | pflag.DurationVar(&cacheTTL, "cache-ttl", 1*time.Second, "maximum duration before cached responses are invalidated. Set this value to 0s to disable the cache") 100 | 101 | pflag.BoolVar(&oidcInsecureSkipVerify, "oidc-insecure-skip-verify", false, "") 102 | 103 | // Create build_info metrics 104 | if err := prometheus.Register(prometheus.NewGaugeFunc( 105 | prometheus.GaugeOpts{ 106 | Name: "multikube_build_info", 107 | Help: "A constant gauge with build info labels.", 108 | ConstLabels: prometheus.Labels{ 109 | "branch": BRANCH, 110 | "goversion": GOVERSION, 111 | "commit": COMMIT, 112 | "version": VERSION, 113 | }, 114 | }, 115 | func() float64 { return 1 }, 116 | )); err != nil { 117 | log.Printf("Unable to register 'multikube_build_info metric %s'", err.Error()) 118 | } 119 | 120 | } 121 | 122 | func main() { 123 | 124 | showver := pflag.Bool("version", false, "Print version") 125 | 126 | pflag.Usage = func() { 127 | fmt.Fprint(os.Stderr, "Usage:\n") 128 | fmt.Fprint(os.Stderr, " multikube [OPTIONS]\n\n") 129 | 130 | title := "Kubernetes multi-cluster manager" 131 | fmt.Fprint(os.Stderr, title+"\n\n") 132 | desc := "Manages multiple Kubernetes clusters and provides a single API to clients" 133 | if desc != "" { 134 | fmt.Fprintf(os.Stderr, desc+"\n\n") 135 | } 136 | fmt.Fprintln(os.Stderr, pflag.CommandLine.FlagUsages()) 137 | } 138 | 139 | // parse the CLI flags 140 | pflag.Parse() 141 | 142 | // Show version if requested 143 | if *showver { 144 | fmt.Printf("Version: %s\nCommit: %s\nBranch: %s\nGoVersion: %s\n", VERSION, COMMIT, BRANCH, GOVERSION) 145 | return 146 | } 147 | 148 | // Only allow one of the flags rs256-public-key and oidc-issuer-url 149 | if rs256PublicKey != "" && oidcIssuerURL != "" { 150 | log.Fatalf("Both flags `--rs256-public-key` and `--oidc-issue-url` cannot be set") 151 | } 152 | 153 | // Read provided kubeconfig file 154 | c, err := clientcmd.LoadFromFile(kubeconfigPath) 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | 159 | // Create the proxy 160 | p, err := proxy.New(c) 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | p.CacheTTL(cacheTTL) 165 | 166 | p.Use( 167 | proxy.WithEmpty(), 168 | proxy.WithLogging(), 169 | proxy.WithJWT(), 170 | proxy.WithHeader(), 171 | ) 172 | 173 | // Add JWK validation middleware if issuer url is provided on cmd line 174 | if oidcIssuerURL != "" { 175 | oidcConfig := proxy.OIDCConfig{ 176 | OIDCIssuerURL: oidcIssuerURL, 177 | OIDCPollInterval: oidcPollInterval, 178 | OIDCUsernameClaim: oidcUsernameClaim, 179 | OIDCInsecureSkipVerify: oidcInsecureSkipVerify, 180 | OIDCCa: readCert(oidcCaFile), 181 | } 182 | //middlewares = append(middlewares, proxy.WithOIDC(oidcConfig)) 183 | p.Use(proxy.WithOIDC(oidcConfig)) 184 | } 185 | 186 | // // Add x509 public key validation middleware if cert provided on cmd line 187 | if rs256PublicKey != "" { 188 | rs256Config := proxy.RS256Config{ 189 | PublicKey: readPublicKey(rs256PublicKey), 190 | } 191 | p.Use(proxy.WithRS256(rs256Config)) 192 | } 193 | 194 | // Create the server 195 | s := &server.Server{ 196 | EnabledListeners: enabledListeners, 197 | CleanupTimeout: cleanupTimeout, 198 | MaxHeaderSize: maxHeaderSize, 199 | SocketPath: socketPath, 200 | Host: host, 201 | Port: port, 202 | ListenLimit: listenLimit, 203 | KeepAlive: keepAlive, 204 | ReadTimeout: readTimeout, 205 | WriteTimeout: writeTimeout, 206 | TLSHost: tlsHost, 207 | TLSPort: tlsPort, 208 | TLSCertificate: tlsCertificate, 209 | TLSCertificateKey: tlsCertificateKey, 210 | TLSCACertificate: tlsCACertificate, 211 | TLSListenLimit: tlsListenLimit, 212 | TLSKeepAlive: tlsKeepAlive, 213 | TLSReadTimeout: tlsReadTimeout, 214 | TLSWriteTimeout: tlsWriteTimeout, 215 | Handler: p.Chain(), 216 | } 217 | 218 | // Metrics server 219 | ms := server.NewServer() 220 | ms.Port = metricsPort 221 | ms.Host = metricsHost 222 | ms.Name = "metrics" 223 | ms.Handler = promhttp.Handler() 224 | go ms.Serve() 225 | 226 | // Setup opentracing 227 | cfg := config.Configuration{ 228 | ServiceName: "multikube", 229 | Sampler: &config.SamplerConfig{ 230 | Type: "const", 231 | Param: 1, 232 | }, 233 | Reporter: &config.ReporterConfig{ 234 | LogSpans: true, 235 | BufferFlushInterval: 1 * time.Second, 236 | }, 237 | } 238 | tracer, closer, err := cfg.New("multikube", config.Logger(jaeger.StdLogger)) 239 | if err != nil { 240 | log.Fatal(err) 241 | } 242 | opentracing.SetGlobalTracer(tracer) 243 | defer closer.Close() 244 | 245 | // Listen and serve! 246 | err = s.Serve() 247 | if err != nil { 248 | log.Fatal(err) 249 | } 250 | 251 | } 252 | 253 | // Reads an x509 certificate from the filesystem and returns an instance of x509.Certiticate. Returns nil on errors 254 | func readCert(p string) *x509.Certificate { 255 | signer, err := ioutil.ReadFile(p) 256 | if err != nil { 257 | return nil 258 | } 259 | block, _ := pem.Decode(signer) 260 | cert, err := x509.ParseCertificate(block.Bytes) 261 | if err != nil { 262 | return nil 263 | } 264 | return cert 265 | } 266 | 267 | // Reads a RSA public key file from the filesystem and parses it into an instance of rsa.PublicKey 268 | func readPublicKey(p string) *rsa.PublicKey { 269 | f, err := ioutil.ReadFile(p) 270 | if err != nil { 271 | log.Fatal(err) 272 | return nil 273 | } 274 | pubkey, err := crypto.ParseRSAPublicKeyFromPEM(f) 275 | if err != nil { 276 | return nil 277 | } 278 | return pubkey 279 | } 280 | -------------------------------------------------------------------------------- /deploy/k8s.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: multikube 6 | namespace: multikube 7 | labels: 8 | app: multikube 9 | --- 10 | apiVersion: v1 11 | kind: Service 12 | metadata: 13 | name: multikube 14 | namespace: multikube 15 | labels: 16 | app: multikube 17 | spec: 18 | ports: 19 | - name: https-8443 20 | port: 8443 21 | protocol: TCP 22 | targetPort: https 23 | - name: metrics 24 | port: 8888 25 | protocol: TCP 26 | targetPort: metrics 27 | selector: 28 | app: multikube 29 | type: ClusterIP 30 | --- 31 | apiVersion: v1 32 | kind: Secret 33 | type: kubernetes.io/tls 34 | metadata: 35 | name: multikube-tls 36 | namespace: multikube 37 | labels: 38 | app: multikube 39 | data: 40 | tls.crt: >- 41 | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURZakNDQWtvQ0NRQ2d1aDdsNVdBUnB6QU5CZ2txaGtpRzl3MEJBUXNGQURCek1Rc3dDUVlEVlFRR0V3SlQKUlRFU01CQUdBMVVFQ0F3SlJYSnBhM05pWlhKbk1STXdFUVlEVlFRSERBcEhiM1JvWlc1aWRYSm5NUTh3RFFZRApWUVFLREFaaGJXbHRiMll4RWpBUUJnTlZCQXNNQ1cxMWJIUnBhM1ZpWlRFV01CUUdBMVVFQXd3TktpNWxlR0Z0CmNHeGxMbU52YlRBZUZ3MHlNREF6TWpVeE5ETTRNekphRncweU5UQXpNalV4TkRNNE16SmFNSE14Q3pBSkJnTlYKQkFZVEFsTkZNUkl3RUFZRFZRUUlEQWxGY21scmMySmxjbWN4RXpBUkJnTlZCQWNNQ2tkdmRHaGxibUoxY21jeApEekFOQmdOVkJBb01CbUZ0YVcxdlpqRVNNQkFHQTFVRUN3d0piWFZzZEdscmRXSmxNUll3RkFZRFZRUUREQTBxCkxtVjRZVzF3YkdVdVkyOXRNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTVwb2sKN1ZuUkJBUTlxbkNOSzNMOEZLM2p4MGVyaTFhcmY1SmNMa3g1UmpOL0gxUUVuRFFFUFg5MUtxWkNFUXVtUmFFOQoxYzVKMGtNVFJJcHBXNHRUdXhMMk1RNWxzWmI4MDE2YUR5dnVGbCtyZ2tMVE1OaXkyOWwxTlhaZzJhV1RhRE1ZCmlWZVZod1B3bU1jR3dnVmtEeXhZcTduYVNwWUdTU3RBQ3dMbUk0Sjk4U2E2WGpxUmV2YmlYOGxYY1QxWDhqVkMKK1FFU0xEYWs4d1ptN0Y2UGJKY2JUWTZkaTlaZFhtbThIYTZwKzRSYlRqQUJXa0wxVU1aSFFETUlmaFFBMWh3VwpuWVk5SmM1ZXZUcTJxT3ZQNWl3Z0VadEJneU1uT2JzelB5ZG04ZjNydUMzbUM3cWVaSWNvUlpQeDRLbmg4eFJOCjAzb1p4MWozQ1RvVy9zb0hyUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQWdXSDBPUDJFOXNtT3kKK3gwQlJMZFlGbC90a1NXTkVFd3k1aStOQ0NFYmRtOFlzWkJ3Vi9ZWFFaMWdwYTR3OUF3c3RrSHNDRnltbGZSaApEWWkrVEtrY2hRTndWKysybGJHTUN5NjdmMENabGQvcFUzTG5Bd2FNWFZrRkx4OXFCSXkrSWlJd1grQUxySTV5CisxWUxKdmEvVWFRa2JEc21LQkU1bzFKWmpKMUxCRGNIUGVkMWpsU2VXVlhTNTB6OThjSDFPYmdVVm12Y2xEcmcKOUQ1dG56RGVuS21iK29pT3p2Zlc0d2QrcWF4WHBQN3VYaUJLcUNueWVnRzJuRGc0blhEMjdTUUZDNGhPbllHdApVSVYybzl4SGdLQXErRGlsNnZ4S2NmK041NDVhdGtLNWx6TXY0RmJ5dGhkWTFoOHFlV0RobFR4YURsaEZKYWgrCldodXBMa2t6Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K 42 | tls.key: >- 43 | LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNXBvazdWblJCQVE5cW5DTkszTDhGSzNqeDBlcmkxYXJmNUpjTGt4NVJqTi9IMVFFCm5EUUVQWDkxS3FaQ0VRdW1SYUU5MWM1SjBrTVRSSXBwVzR0VHV4TDJNUTVsc1piODAxNmFEeXZ1RmwrcmdrTFQKTU5peTI5bDFOWFpnMmFXVGFETVlpVmVWaHdQd21NY0d3Z1ZrRHl4WXE3bmFTcFlHU1N0QUN3TG1JNEo5OFNhNgpYanFSZXZiaVg4bFhjVDFYOGpWQytRRVNMRGFrOHdabTdGNlBiSmNiVFk2ZGk5WmRYbW04SGE2cCs0UmJUakFCCldrTDFVTVpIUURNSWZoUUExaHdXbllZOUpjNWV2VHEycU92UDVpd2dFWnRCZ3lNbk9ic3pQeWRtOGYzcnVDM20KQzdxZVpJY29SWlB4NEtuaDh4Uk4wM29aeDFqM0NUb1cvc29IclFJREFRQUJBb0lCQUQ5SWMvUFMzeUpNQzZRMwphUFpRZ3M4bC9VbFY2TVMvVHljZVFqL09hc1dTSWtPMXFlSTRBQ1hrVVlJWHlDK25VOFR0LytzcHhtWjJVOVpPCmFhVmVzZWEzeUdvaitsSm5EaER2ZURYTHAzWHFZVVhKRXUzbnVnWE5RblllZTJ1Zkhibk5zK0VVOFFyeFVOaU0KWGRWbTUxT21wdHJSa1E5QTVvNnoyTXBzekQxWDc5akZoLzBlWjdUejJoWTlURDB0NlY4NSt2UDJ6MHVERzJ3MQpzVDhSbjFlOXJZUXhsR0c4R1Z1YzBiYlRacHFYbXpHWEJ1bGZGbllHR0pER1pycUxVaXJiY1pHWS9OSmJFVWZiCjN6RGtxZzByOU53ZDBoM0NpaG1oYkduVTQyZ1lodzgxMjFIOWJTSGZ4Y2prZWwwaS9ja1Y0T21uclhxM0REeGMKR0kyNzBWMENnWUVBOTV1M3hkSnNUeVJCL1NqZzc3YnJacHFnVnloeXpDdi9Vck0yaWlueDR0dXBqbVNQT2x4cQp5aFBYbDIxM242N1lXNmJab2J1YzNvb1FBWk5iTCtzYmVGSGNkQzZRb05yNnNBTzl6SW0wMmF6QWEyVldpZDlCCnh6dFBleTV6b2FGSUd4V2tzdUxObXFnNC9xTFRFOEpIb3drSTh5OXAwTlhEVndpQkxHT0M5ajhDZ1lFQTdtcmcKK1VNNGNxaUh5WGlRYm9aZ3RicFJqSFlaL2RSWFJ4bDJ0VEV1aVZUalpkcUl6ekJobkRTNEVvOGx5RXZRbVBkWQpFeEFxWmF5Vm82YUNhNHF6TzB0TnRMbEEzOWVsY085NXhLSDRscjZROUxZRitGNVdhTnYzZ3VXMkJDLzhxNGZHCjdPNWJMMFg2am5YbDVxM0pnRHU5RVE2cklyZzJyekx6SHU3My94TUNnWUVBdUJZWlp3V3VhSmg0UnhNaFNKY2kKSkVKSmV3TWpkaEF2M3ZoR1VDb25INVpZVlBrR1UwbHFTNXE4Nm1RRVQ3L2FNeW0xRzcwMFAyODl2eTRpMlZsZQpMdklOaUlNbUc3RXlON0pRWmNUcm8vbi9oTmp1b2NwT1NTVWJUbVdXcWxBU0g3RFRwZnRoQ042UW9SL0U5aWUyCndzdHd6L3dzRzRzUms1OUxDZ2xoMEZrQ2dZRUFzM3l1NWJpZmRHSS81NExtYU82dE8rS0twZzM3UDBadWRrNUwKT2RsT3lZQ291UE11YjV4aXY3QklxMkFzOTM5c0NOeWM1NjBSM2YxeG9nUW14ME5oNzArZnJtQ1E0SE4rVDJsWgo2Smh2aHp6cjcrNWd6RHhwSFFRTWIvVHpkRytUN3FhSE1iTzMzZnoyeGUwb0tPUElnTXQ0YUxEOFVXVVJkaEFMClM4eG4weFVDZ1lCb0ZvdmpYSTZNOWt0ZjRzdnhGUTVjRUQ3NWdDL2dndmQvcHZYWE9lbk96Z2hEQXZCN1RtVVUKVWRLWWRLNFEvVk5UK0w2bUViNWprY0pZSndpVlJpRUxQcVlrTkg5bko5OXBvN1V3UkxuK1dLbUxNbTRwUlZQYgpTOXRKZVEyaDk4WnFSNmo4Y1dRKytGc0toNDZkWWtnZ3ZXUFBod1NqWHNDR25sQ21nc25wRUE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= 44 | --- 45 | apiVersion: apps/v1 46 | kind: Deployment 47 | metadata: 48 | name: multikube 49 | namespace: multikube 50 | labels: 51 | app: multikube 52 | spec: 53 | replicas: 1 54 | selector: 55 | matchLabels: 56 | app: multikube 57 | template: 58 | metadata: 59 | creationTimestamp: null 60 | labels: 61 | app: multikube 62 | annotations: 63 | prometheus.io/port: "8888" 64 | prometheus.io/scrape: "true" 65 | spec: 66 | volumes: 67 | - name: multikube-tls 68 | secret: 69 | secretName: multikube-tls 70 | defaultMode: 420 71 | - name: kubeconfig 72 | secret: 73 | secretName: kubeconfig 74 | defaultMode: 420 75 | containers: 76 | - name: multikube 77 | image: "amimof/multikube:latest" 78 | args: 79 | - "--tls-certificate=/etc/multikube/tls/tls.crt" 80 | - "--tls-key=/etc/multikube/tls/tls.key" 81 | - "--tls-host=0.0.0.0" 82 | - "--metrics-host=0.0.0.0" 83 | - "--kubeconfig=/etc/multikube/config/config" 84 | ports: 85 | - name: https 86 | containerPort: 8443 87 | protocol: TCP 88 | - name: metrics 89 | containerPort: 8888 90 | protocol: TCP 91 | resources: 92 | limits: 93 | cpu: 250m 94 | memory: 256Mi 95 | requests: 96 | cpu: 250m 97 | memory: 256Mi 98 | volumeMounts: 99 | - name: multikube-tls 100 | readOnly: true 101 | mountPath: /etc/multikube/tls 102 | - name: kubeconfig 103 | readOnly: true 104 | mountPath: /etc/multikube/config 105 | imagePullPolicy: Always 106 | serviceAccount: multikube 107 | serviceAccountName: multikube 108 | --- 109 | apiVersion: extensions/v1beta1 110 | kind: Ingress 111 | metadata: 112 | name: multikube 113 | namespace: multikube 114 | annotations: 115 | nginx.ingress.kubernetes.io/ssl-passthrough: "true" 116 | spec: 117 | rules: 118 | - host: multikube.apps.mdlwr.se 119 | http: 120 | paths: 121 | - path: / 122 | backend: 123 | serviceName: multikube 124 | servicePort: https-8443 125 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | These are some examples of how to configure and run Multikube based on the use-case. More content is added here regularly. Please create an issue or open a PR if you feel that something is missing or can be improved. 4 | 5 | * [JWT RS256 Authentication](https://github.com/amimof/multikube/blob/master/docs/examples/jwt-auth-example.md) 6 | * [Dex OIDC configuration](https://github.com/amimof/multikube/blob/master/docs/examples/dex-example.md) 7 | * [TLS configuration](https://github.com/amimof/multikube/blob/master/docs/examples/tls-example.md) 8 | * [Non-TLS configuration](https://github.com/amimof/multikube/blob/master/docs/examples/non-tls-example.md) 9 | * [Configuring clusters](https://github.com/amimof/multikube/blob/master/docs/examples/kubeconfig-example.md) 10 | * [Run Multikube in Kubernetes](https://github.com/amimof/multikube/blob/master/docs/examples/kubernetes-example.md) 11 | * [Run Multikube with Docker](https://github.com/amimof/multikube/blob/master/docs/examples/docker-example.md) -------------------------------------------------------------------------------- /docs/examples/dex-example.md: -------------------------------------------------------------------------------- 1 | # Configuring Multikube to use Dex as OIDC provider 2 | 3 | Multikube supports OIDC providers that support [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) since it uses the `/.well-known/openid-configuration` endpoint for automatic configuration. We can use [Dex](https://github.com/dexidp/dex/) for that, which is an excellent OIDC Provider written in Go. 4 | 5 | If you have not already created a kubeconfig for Multikube then make sure to do so first. Read [kubeconfig-example](https://github.com/amimof/multikube/blob/master/docs/examples/kubeconfig-example.md) of how to create it. 6 | 7 | 1. Follow Dex's [Getting Started](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md) guide of how to download and run Dex with the included config-dev configuration. 8 | 2. Run the example application included in Dex, also available in the getting started guide. 9 | 3. Run Multikube with Dex as OIDC provider 10 | ``` 11 | multikube \ 12 | --oidc-issuer-url="http://localhost:5556/dex/" \ 13 | --tls-certificate=/etc/multikube/server.pem \ 14 | --tls-key=/etc/multikube/server-key.pem 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /docs/examples/docker-example.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | You may use the tiny official Docker images to run Multikube. Following will run Multikube (non-tls) in a Docker container. 4 | 5 | ``` 6 | docker run -v $PWD:/etc/multikube/kubeconfig \ 7 | amimof/multikube:latest \ 8 | --scheme http 9 | ``` -------------------------------------------------------------------------------- /docs/examples/jwt-auth-example.md: -------------------------------------------------------------------------------- 1 | # Mutual Authentication 2 | 3 | Multikube supports mutual authentication using JWT RS256 signing method. Multikube is to be configured with a RSA public key and the JWT needs to be issued using the private key. The JWT token from clients is then verified by multikube using the public key. 4 | 5 | ## Generate RSA key pairs 6 | 7 | Use `openssl` to create a key pair 8 | 9 | ``` 10 | openssl genrsa -out rsa.key 4096 11 | openssl rsa -in rsa.key -pubout > rsa.key.pub 12 | ``` 13 | 14 | ## Sign a JWT 15 | 16 | Generate a signed JWT token using the rsa private key created previously. There are many ways of doing this but the fastest is to use https://jwt.io/. 17 | 18 | 1. Browse to https://jwt.io 19 | 2. Change algorithm to RS256 20 | 3. Change the `sub` claim in the payload to a desired username 21 | 4. Paste the content of rsa.key.pub in the public key text box 22 | 5. Paste the content of rsa.key in the private key text box 23 | 24 | The JWT access token generated by jwt.io can now be used by any HTTP client to make requests to multikube. 25 | 26 | ## Run Multikube 27 | 28 | Now we can run multikube with RS256 validation enabled using the flag `--rs256-public-key`. If you haven't already, follow [this guide](https://github.com/amimof/multikube/blob/master/docs/examples/tls-example.md) in order configure TLS since it's required for mutual auth. 29 | 30 | ``` 31 | multikube \ 32 | --tls-certificate=server.pem \ 33 | --tls-key=server-key.pem \ 34 | --rs256-public-key=rsa.key.pub 35 | ``` -------------------------------------------------------------------------------- /docs/examples/kubeconfig-example.md: -------------------------------------------------------------------------------- 1 | # kubeconfig 2 | 3 | Multikube uses the well-known [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) to communicate with upstream apiservers. 4 | 5 | A request from a client with the URL path `/k8s-dev-cluster/api/v1/namespaces` will tell Multikube to find a context in its configured kubeconfig named 'k8s-dev-cluster'. 6 | 7 | ## Create a kubeconfig for Multikube 8 | 9 | Create a cluster 10 | ``` 11 | kubeconfig config set-cluster k8s-dev-cluster \ 12 | --server=https://k8s-dev.domain.com:8443 \ 13 | --certificate-authority=ca.crt \ 14 | --embed-certs=true \ 15 | --kubeconfig=/etc/multikube/kubeconfig 16 | ``` 17 | 18 | Create the credentials. This example uses client certificate pairs 19 | ``` 20 | kubectl config set-credentials k8s-dev-cluster \ 21 | --client-certificate=client.crt \ 22 | --client-key=client.key \ 23 | --embed-certs=true \ 24 | --kubeconfig=/etc/multikube/kubeconfig 25 | ``` 26 | 27 | Create the context 28 | ``` 29 | kubectl config set-context k8s-dev-cluster \ 30 | --cluster=k8s-dev-cluster \ 31 | --user=k8s-dev-cluster \ 32 | --kubeconfig=/etc/multikube/kubeconfig 33 | ``` 34 | 35 | ## Run Multikube 36 | 37 | We can now use the kubeconfig we created with Multikube. Clients may target request to the cluster named `k8s-dev-cluster` either by using URL path or an HTTP header. 38 | 39 | ``` 40 | ./multikube 41 | ``` -------------------------------------------------------------------------------- /docs/examples/kubernetes-example.md: -------------------------------------------------------------------------------- 1 | # Multikube on Kubernetes 2 | 3 | Although Multikube is designed to "speak" with multiple Kubernetes cluster, it can run within a Kubernetes cluster itself. As a matter of fact it is the best way of deploying and running Multikube. This is perfect for large environments where Multikube is deployed in a management cluster and routes to multiple application clusters. 4 | 5 | This example sets up Multikube with TLS disabled, which isn't recommended but it is the fastest way of getting started. 6 | 7 | ## Deploy 8 | 9 | Create a namespace 10 | ``` 11 | kubectl create namespace multikube 12 | ``` 13 | 14 | Create a secret which holds the kubeconfig. This example will use the one kubectl uses by default on your local computer `~/.kube/config` 15 | ``` 16 | kubectl create secret generic kubeconfig --from-file ~/.kube/config -n multikube 17 | ``` 18 | 19 | Deploy kubernetes manifest 20 | ``` 21 | kubectl apply -f https://raw.githubusercontent.com/amimof/multikube/master/deploy/k8s.yaml 22 | ``` 23 | 24 | Port-forward 8080 to test it locally. You can of course create an ingress as well 25 | ``` 26 | kubectl port-forward deployment/multikube -n multikube 8443:8443 27 | curl -k http://localhost:8443/ 28 | no token present in request 29 | ``` -------------------------------------------------------------------------------- /docs/examples/non-tls-example.md: -------------------------------------------------------------------------------- 1 | ## Non-TLS example (not recommended) 2 | This method of serving multikube is not recommended for obvious reasons. Additionally, this method prevents kubectl compatibility due to how kubectl discards the `token` field in kubectl when the cluster has a http scheme over https. 3 | 4 | ``` 5 | multikube --scheme=http 6 | ``` -------------------------------------------------------------------------------- /docs/examples/tls-example.md: -------------------------------------------------------------------------------- 1 | ## TLS example (recommended) 2 | 3 | Generate private keys 4 | ``` 5 | sudo openssl ecparam -name secp521r1 -genkey -noout -out ca-key.pem 6 | sudo openssl ecparam -name secp521r1 -genkey -noout -out server-key.pem 7 | ``` 8 | 9 | Generate a CA certificate using the CA private key 10 | ``` 11 | sudo openssl req -x509 -new -sha256 -nodes -key ca-key.pem -out ca.pem -subj '/CN=multikube-ca' 12 | ``` 13 | 14 | Generate the server certificate by creating a CSR and signing it with the CA key 15 | ``` 16 | sudo openssl req -new -sha256 -key server-key.pem -subj '/CN=localhost' -out server.csr 17 | sudo openssl x509 -req -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server.pem 18 | ``` 19 | 20 | Run multikube 21 | ``` 22 | multikube \ 23 | --tls-certificate=server.pem \ 24 | --tls-key=server-key.pem 25 | ``` 26 | 27 | ## TLS example with mutual authentication 28 | 29 | Generate private key 30 | ``` 31 | sudo openssl ecparam -name secp521r1 -genkey -noout -out client-key.pem 32 | ``` 33 | 34 | Generate the client certificate by creating CSR and signing it with the CA key 35 | ``` 36 | sudo openssl req -new -sha256 -key client-key.pem -subj '/CN=multikube' -out client.csr 37 | sudo openssl x509 -req -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client.pem 38 | ``` 39 | 40 | Run multikube 41 | ``` 42 | multikube \ 43 | --tls-ca=ca.pem \ 44 | --tls-certificate=server.pem \ 45 | --tls-key=server-key.pem 46 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amimof/multikube 2 | 3 | go 1.12 4 | 5 | require ( 6 | cloud.google.com/go v0.44.3 // indirect 7 | github.com/Azure/go-autorest v13.0.0+incompatible // indirect 8 | github.com/NYTimes/gziphandler v1.1.1 // indirect 9 | github.com/SermoDigital/jose v0.9.2-0.20180104202408-a0450ddff675 10 | github.com/coreos/go-oidc v2.2.1+incompatible 11 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect 12 | github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect 13 | github.com/emicklei/go-restful v2.9.6+incompatible // indirect 14 | github.com/evanphx/json-patch v4.5.0+incompatible // indirect 15 | github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 // indirect 16 | github.com/go-openapi/spec v0.19.2 // indirect 17 | github.com/go-openapi/swag v0.19.5 18 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect 19 | github.com/golang/protobuf v1.4.0 // indirect 20 | github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 // indirect 21 | github.com/googleapis/gnostic v0.3.1 // indirect 22 | github.com/gophercloud/gophercloud v0.3.0 // indirect 23 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 // indirect 24 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 25 | github.com/hashicorp/golang-lru v0.5.3 // indirect 26 | github.com/imdario/mergo v0.3.7 // indirect 27 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 28 | github.com/kr/pty v1.1.8 // indirect 29 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect 30 | github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30 // indirect 31 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 32 | github.com/onsi/ginkgo v1.9.0 // indirect 33 | github.com/onsi/gomega v1.6.0 // indirect 34 | github.com/opentracing/opentracing-go v1.1.0 35 | github.com/pelletier/go-toml v1.6.0 // indirect 36 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect 37 | github.com/prometheus/client_golang v1.5.1 38 | github.com/prometheus/procfs v0.0.11 // indirect 39 | github.com/spf13/cast v1.3.1 // indirect 40 | github.com/spf13/cobra v0.0.6 41 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 42 | github.com/spf13/pflag v1.0.5 43 | github.com/spf13/viper v1.6.2 // indirect 44 | github.com/stretchr/testify v1.4.0 45 | github.com/tylerb/graceful v1.2.15 46 | github.com/uber/jaeger-client-go v2.16.0+incompatible 47 | github.com/uber/jaeger-lib v2.0.0+incompatible // indirect 48 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect 49 | golang.org/x/mobile v0.0.0-20190814143026-e8b3e6111d02 // indirect 50 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 51 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 52 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect 53 | golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd // indirect 54 | google.golang.org/api v0.9.0 // indirect 55 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect 56 | google.golang.org/grpc v1.23.0 // indirect 57 | gopkg.in/inf.v0 v0.9.1 // indirect 58 | gopkg.in/ini.v1 v1.52.0 // indirect 59 | gopkg.in/square/go-jose.v2 v2.3.1 60 | gopkg.in/yaml.v2 v2.2.8 61 | honnef.co/go/tools v0.0.1-2019.2.2 // indirect 62 | k8s.io/api v0.0.0-20190820101039-d651a1528133 // indirect 63 | k8s.io/apimachinery v0.0.0-20190820100751-ac02f8882ef6 64 | k8s.io/client-go v0.0.0-20190620085101-78d2af792bab 65 | k8s.io/gengo v0.0.0-20190813173942-955ffa8fcfc9 // indirect 66 | k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf // indirect 67 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a // indirect 68 | sigs.k8s.io/structured-merge-diff v0.0.0-20190820212518-960c3cc04183 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /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 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 7 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 8 | github.com/Azure/go-autorest v13.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 9 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 10 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 11 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 12 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 13 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 14 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 15 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 16 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 17 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 18 | github.com/SermoDigital/jose v0.9.1 h1:atYaHPD3lPICcbK1owly3aPm0iaJGSGPi0WD4vLznv8= 19 | github.com/SermoDigital/jose v0.9.1/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= 20 | github.com/SermoDigital/jose v0.9.2-0.20180104202408-a0450ddff675 h1:etDiWPdUHORCMZ1MUPPHhwQU/PZB/XWlwQnFxBe2dOc= 21 | github.com/SermoDigital/jose v0.9.2-0.20180104202408-a0450ddff675/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= 22 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 23 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 24 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 25 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 26 | github.com/amimof/multikube v1.0.0-alpha.8 h1:12vujcAoA5EF4/yqGDW7vE4PcEVJstLKTx85vldIduc= 27 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 28 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 29 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 30 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 31 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 32 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 33 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 34 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 35 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 36 | github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= 37 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 38 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 39 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 40 | github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= 41 | github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= 42 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 43 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 44 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 45 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 46 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 47 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 52 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 53 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 54 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 55 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 56 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 57 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 58 | github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 59 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 60 | github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 61 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 62 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 63 | github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 64 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 65 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 66 | github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs= 67 | github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8= 68 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 69 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 70 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 71 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 72 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 73 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 74 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 75 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 76 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 77 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 78 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 79 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 80 | github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= 81 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 82 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 83 | github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= 84 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 85 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 86 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 87 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 88 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 89 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 90 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 91 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 92 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 93 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 94 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 95 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 96 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 97 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 98 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 99 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 100 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 101 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 102 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 104 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 105 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 106 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 107 | github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= 108 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 109 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 110 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 111 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 112 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 113 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 114 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 115 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 117 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 118 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 119 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 120 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 121 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 122 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 123 | github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 124 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 125 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 126 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 127 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 128 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 129 | github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 130 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 131 | github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= 132 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 133 | github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 134 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 135 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 h1:ehVe1P3MbhHjeN/Rn66N2fGLrP85XXO1uxpLhv0jtX8= 136 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 137 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 138 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 139 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 140 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 141 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 142 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 143 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 144 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 145 | github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 146 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 147 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 148 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 149 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 150 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 151 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 152 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 153 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 154 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 155 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 156 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 157 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 158 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 159 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 160 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 161 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 162 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 163 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 164 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 165 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 166 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 167 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 168 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 169 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 170 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 171 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 172 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 173 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 174 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 175 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 176 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 177 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 178 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 179 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 180 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 181 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= 182 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 183 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= 184 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 185 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 186 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 187 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 188 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 189 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 190 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 191 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 192 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 193 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 194 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 195 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 196 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 197 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 198 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 199 | github.com/munnerz/goautoneg v0.0.0-20190414153302-2ae31c8b6b30/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 200 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 201 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 202 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 203 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 204 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 205 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 206 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 207 | github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 208 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 209 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 210 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 211 | github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 212 | github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 213 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 214 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 215 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 216 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= 217 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 218 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 219 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 220 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 221 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 222 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 223 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 224 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 225 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= 226 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= 227 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 228 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 229 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 230 | github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= 231 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 232 | github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= 233 | github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 234 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 235 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 236 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 237 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 238 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 239 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 240 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 241 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 242 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 243 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 244 | github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= 245 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 246 | github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= 247 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 248 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 249 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 250 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 251 | github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= 252 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 253 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 254 | github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= 255 | github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 256 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 257 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 258 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 259 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 260 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 261 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 262 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 263 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 264 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 265 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 266 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 267 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 268 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 269 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 270 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 271 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 272 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 273 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 274 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 275 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 276 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 277 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 278 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 279 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 280 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 281 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 282 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 283 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 284 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 285 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 286 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 287 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 288 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 289 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 290 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 291 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 292 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 293 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 294 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 295 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 296 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 297 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 298 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 299 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 300 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 301 | github.com/tylerb/graceful v1.2.15 h1:B0x01Y8fsJpogzZTkDg6BDi6eMf03s01lEKGdrv83oA= 302 | github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II= 303 | github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= 304 | github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 305 | github.com/uber/jaeger-lib v2.0.0+incompatible h1:iMSCV0rmXEogjNWPh2D0xk9YVKvrtGoHJNe9ebLu/pw= 306 | github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 307 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 308 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 309 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 310 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 311 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 312 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 313 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 314 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 315 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 316 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 317 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 318 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 319 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 320 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 321 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 322 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 323 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 324 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= 325 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 326 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM= 327 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 328 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 329 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 330 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 331 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 332 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 333 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 334 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 335 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 336 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 337 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 338 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 339 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= 340 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 341 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw= 342 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 343 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 344 | golang.org/x/mobile v0.0.0-20190814143026-e8b3e6111d02/go.mod h1:z5wpDCy2wbnXyFdvEuY3LhY9gBUL86/IOILm+Hsjx+E= 345 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 346 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 347 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 348 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 349 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 350 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 351 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 352 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 353 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 354 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 355 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 356 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 358 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 359 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 360 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 361 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 362 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 363 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 364 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 365 | golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 366 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 367 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 368 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= 369 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 371 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 372 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 373 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 374 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 375 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 376 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 377 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 378 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 379 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 380 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 381 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 382 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 383 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 384 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 385 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 386 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 387 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 388 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 389 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= 398 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 400 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20190902133755-9109b7679e13 h1:tdsQdquKbTNMsSZLqnLELJGzCANp9oXhu6zFBW6ODx4= 402 | golang.org/x/sys v0.0.0-20190902133755-9109b7679e13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= 406 | golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= 408 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 410 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 411 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 412 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 413 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 414 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 415 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 416 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 417 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 418 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 419 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 420 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 421 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 422 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 423 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 424 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 425 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 426 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 427 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 428 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 429 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 430 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 431 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 432 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 433 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 434 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 435 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 436 | golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 437 | golang.org/x/tools v0.0.0-20190820205717-547ecf7b1ef1 h1:gROLVIjNoz8KE0CWNRbGt7mC7AIatjLKOMy8BN3fcys= 438 | golang.org/x/tools v0.0.0-20190820205717-547ecf7b1ef1/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 439 | golang.org/x/tools v0.0.0-20190822000311-fc82fb2afd64 h1:4EN1tY9aQxwLGYHWT5WdQN56Xzbwlg2UTINDbZ04l10= 440 | golang.org/x/tools v0.0.0-20190822000311-fc82fb2afd64/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 441 | golang.org/x/tools v0.0.0-20190830223141-573d9926052a h1:XAHT1kdPpnU8Hk+FPi42KZFhtNFEk4vBg1U4OmIeHTU= 442 | golang.org/x/tools v0.0.0-20190830223141-573d9926052a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 443 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 444 | golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs= 445 | golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 446 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 447 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 448 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 449 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 450 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 451 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 452 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 453 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 454 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 455 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 456 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 457 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 458 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 459 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 460 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 461 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 462 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 463 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 464 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 465 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 466 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 467 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 468 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 469 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 470 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 471 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 472 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 473 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 474 | google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= 475 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 476 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 477 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 478 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 479 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 480 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 481 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 482 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= 483 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 484 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 485 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 486 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 487 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 488 | gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4= 489 | gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 490 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 491 | gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= 492 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 493 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 494 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 495 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 496 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 497 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 498 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 499 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 500 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 501 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 502 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 503 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 504 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 505 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 506 | honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 507 | k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= 508 | k8s.io/api v0.0.0-20190820101039-d651a1528133/go.mod h1:AlhL1I0Xqh5Tyz0HsxjEhy+iKci9l1Qy3UMDFW7iG3A= 509 | k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= 510 | k8s.io/apimachinery v0.0.0-20190820100750-21ddcbbef9e1/go.mod h1:EZoIMuAgG/4v58YL+bz0kqnivqupk28fKYxFCa5e6X8= 511 | k8s.io/apimachinery v0.0.0-20190820100751-ac02f8882ef6 h1:lpB4MEqLgFxnb5zcDsI3MPBoXQ89pFcg8MQLwrwyL2s= 512 | k8s.io/apimachinery v0.0.0-20190820100751-ac02f8882ef6/go.mod h1:EZoIMuAgG/4v58YL+bz0kqnivqupk28fKYxFCa5e6X8= 513 | k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g= 514 | k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= 515 | k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= 516 | k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 517 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 518 | k8s.io/gengo v0.0.0-20190813173942-955ffa8fcfc9/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 519 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 520 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 521 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 522 | k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= 523 | k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 524 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 525 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= 526 | k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 527 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= 528 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a h1:uy5HAgt4Ha5rEMbhZA+aM1j2cq5LmR6LQ71EYC2sVH4= 529 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 530 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 531 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 532 | sigs.k8s.io/structured-merge-diff v0.0.0-20190820212518-960c3cc04183/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= 533 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 534 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 535 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Cache is a simple key value store where the value is always a []byte. 9 | // It is mostly used in Multikube to store http responses in-memory in order to 10 | // cache requests and serve content to clients to decrease number of http calls 11 | // made to upstream servers. 12 | type Cache struct { 13 | Store map[string]*Item 14 | TTL time.Duration 15 | mux sync.Mutex 16 | } 17 | 18 | // Item represents a unit stored in the cache 19 | type Item struct { 20 | Key string 21 | Value []byte 22 | expires time.Time 23 | created time.Time 24 | } 25 | 26 | // ListKeys returns the keys of all items in the cache as a string array 27 | func (c *Cache) ListKeys() []string { 28 | c.mux.Lock() 29 | defer c.mux.Unlock() 30 | 31 | keys := make([]string, 0, len(c.Store)) 32 | for key := range c.Store { 33 | keys = append(keys, key) 34 | } 35 | return keys 36 | } 37 | 38 | // Get returns an item from the cache by key 39 | func (c *Cache) Get(key string) *Item { 40 | c.mux.Lock() 41 | defer c.mux.Unlock() 42 | 43 | if !c.Exists(key) { 44 | return nil 45 | } 46 | 47 | item := c.Store[key] 48 | if !item.expires.IsZero() && item.Age() > c.TTL { 49 | // Item age exceeded time to live 50 | delete(c.Store, key) 51 | return nil 52 | } 53 | return item 54 | } 55 | 56 | // Set instantiates and allocates a key in the cache and overwrites any previously set item 57 | func (c *Cache) Set(key string, val []byte) *Item { 58 | c.mux.Lock() 59 | defer c.mux.Unlock() 60 | 61 | // Delete item if already exists in the cache 62 | if c.Exists(key) { 63 | delete(c.Store, key) 64 | } 65 | 66 | item := &Item{ 67 | Key: key, 68 | Value: val, 69 | expires: time.Now().Add(c.TTL), 70 | created: time.Now(), 71 | } 72 | 73 | c.Store[key] = item 74 | return item 75 | } 76 | 77 | // Delete removes an item by key 78 | func (c *Cache) Delete(key string) { 79 | c.mux.Lock() 80 | defer c.mux.Unlock() 81 | delete(c.Store, key) 82 | } 83 | 84 | // Exists returns true if an item with the given exists is non-nil. Otherwise returns false 85 | func (c *Cache) Exists(key string) bool { 86 | if _, ok := c.Store[key]; ok { 87 | return true 88 | } 89 | return false 90 | } 91 | 92 | // Len returns the number of items stored in cache 93 | func (c *Cache) Len() int { 94 | c.mux.Lock() 95 | defer c.mux.Unlock() 96 | 97 | return len(c.Store) 98 | } 99 | 100 | // Size return the sum of all bytes in the cache 101 | func (c *Cache) Size() int { 102 | c.mux.Lock() 103 | defer c.mux.Unlock() 104 | 105 | l := 0 106 | for _, val := range c.Store { 107 | l += val.Bytes() 108 | } 109 | return l 110 | } 111 | 112 | // Age returns the duration elapsed since creation 113 | func (i *Item) Age() time.Duration { 114 | return time.Now().Sub(i.created) 115 | } 116 | 117 | // ExpiresAt return the time when the item was created plus the configured TTL 118 | func (i *Item) ExpiresAt() time.Time { 119 | return i.expires 120 | } 121 | 122 | // Bytes returns the number of bytes of i. Shorthand for len(i.Value) 123 | func (i *Item) Bytes() int { 124 | return len(i.Value) 125 | } 126 | 127 | // New return a new empty cache instance 128 | func New() *Cache { 129 | return &Cache{ 130 | Store: make(map[string]*Item), 131 | TTL: time.Second * 1, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var key = "somekey" 8 | var val = "hello world" 9 | 10 | func contains(s []string, e string) bool { 11 | for _, a := range s { 12 | if a == e { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | 19 | func TestCacheGetNilItem(t *testing.T) { 20 | cache := New() 21 | item := cache.Get(key) 22 | if item != nil { 23 | t.Fatalf("Item with key %s should be nil", key) 24 | } 25 | } 26 | 27 | func TestCacheSetItem(t *testing.T) { 28 | cache := New() 29 | item := cache.Set(key, []byte(val)) 30 | if item.Key != key { 31 | t.Fatalf("Item key is not %s", key) 32 | } 33 | if string(item.Value) != val { 34 | t.Fatalf("Item val is not %s", val) 35 | } 36 | } 37 | 38 | func TestCacheSetGetItem(t *testing.T) { 39 | cache := New() 40 | cache.Set(key, []byte(val)) 41 | item := cache.Get(key) 42 | if item.Key != key { 43 | t.Fatalf("Item key is not %s", key) 44 | } 45 | if string(item.Value) != val { 46 | t.Fatalf("Item val is not %s", val) 47 | } 48 | } 49 | 50 | func TestCacheDeleteItem(t *testing.T) { 51 | cache := New() 52 | item := cache.Set(key, []byte(val)) 53 | 54 | cache.Delete(item.Key) 55 | item = cache.Get(key) 56 | if item != nil { 57 | t.Fatalf("Item with key %s should be nil", key) 58 | } 59 | } 60 | 61 | func TestCacheListKeys(t *testing.T) { 62 | cache := New() 63 | items := []string{"/namespaces/", "/namespaces/pods", "/namespaces/pods/pod-1"} 64 | 65 | cache.Set(items[0], []byte{'a'}) 66 | cache.Set(items[1], []byte{'b'}) 67 | cache.Set(items[2], []byte{'c'}) 68 | 69 | for i, k := range cache.ListKeys() { 70 | if !contains(items, k) { 71 | t.Fatalf("Key should be %s but got %s", items[i], k) 72 | } 73 | } 74 | 75 | } 76 | 77 | func TestCacheSize(t *testing.T) { 78 | cache := New() 79 | cache.Set(key, []byte("a")) 80 | if cache.Size() != 1 { 81 | t.Fatalf("Expected cache size to be %d but got %d", 1, cache.Size()) 82 | } 83 | } 84 | 85 | func TestCacheItemBytes(t *testing.T) { 86 | cache := New() 87 | cache.Set("A", []byte("a")) 88 | cache.Set("B", []byte("b")) 89 | cache.Set("C", []byte("c")) 90 | 91 | a := cache.Get("A") 92 | b := cache.Get("B") 93 | c := cache.Get("C") 94 | 95 | items := []*Item{a, b, c} 96 | 97 | for _, item := range items { 98 | if item.Bytes() != 1 { 99 | t.Fatalf("Expected item bytes to be %d but got %d", 1, item.Bytes()) 100 | } 101 | } 102 | 103 | } 104 | 105 | func TestCacheLen(t *testing.T) { 106 | cache := New() 107 | cache.Set("A", []byte("alpha")) 108 | cache.Set("B", []byte("bravo")) 109 | cache.Set("C", []byte("charlie")) 110 | 111 | if cache.Len() != 3 { 112 | t.Logf("Expected cache length to be %d but got %d", 3, cache.Len()) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /pkg/proxy/empty.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // WithEmpty is an empty handler that does nothing 8 | func WithEmpty() MiddlewareFunc { 9 | return func(next http.Handler) http.Handler { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | next.ServeHTTP(w, r) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/proxy/empty_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | -------------------------------------------------------------------------------- /pkg/proxy/header.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // WithHeader is a middleware that reads the value of the HTTP header "Multikube-Context" 9 | // in the request and, if found, sets it's value in the request context. 10 | func WithHeader() MiddlewareFunc { 11 | return func(next http.Handler) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | req := r 14 | header := r.Header.Get("Multikube-Context") 15 | if header != "" { 16 | ctx := context.WithValue(r.Context(), contextKey, header) 17 | req = r.WithContext(ctx) 18 | } 19 | next.ServeHTTP(w, req) 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/proxy/header_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestMiddlewareWithHeader(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | p, err := New(kubeConf) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | p.Use(WithHeader()) 18 | 19 | req, err := http.NewRequest("GET", "/api/v1/pods/default", nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | rr := httptest.NewRecorder() 24 | 25 | req.Header.Set("Multikube-Context", "dev-cluster-1") 26 | req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbWlyQG1pZGRsZXdhcmUuc2UiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.nSyFTR7SZ95-pkt_PcjbmVX7rZDizLxONOnF9HWhBIe1R6ir-rrzmOaXjVxfdcVlBKEFE9bz6PJMwD8-tqsZUqlOeXSLNXXeCGhdmhluBJrJMi-Ewyzmvm7yJ2L8bVfhhBJ3z_PivSbxMKLpWz7VkbwaJrk8950QkQ5oB_CV0ysoppTybGzvU1e8tc5h5wRKimju3BA3mA5HxN8K7-2lM_JZ8cbxBToGMBMsHKSy4VXAxm-lmvSwletLXqdSlqDQZejjJYYGaPpvDih1voTJ_FJnYFzx_NWq5qN416IGJrr1RAe92B2gfRUmzftFMMw8NEYBLDNXgKx3d9OOO9xKi9DxZ9wkFrZlwNZBj-VPTgNt5zeNgME8CJqgxvCaESuDAMWkjnfdyhBYAu9uUvbRSjFowFdQFumnVlKNfAlhKOQFOZpifFIwRFYda8lzvlJv1CzHEt500HgL2qofoIOTzFQNeJ_XkOQvRBy4eBkwxKvbHlwUAObxzZrCBjaAeQRGrMU926zpujSFQ_9KzUqNsNrxJWkBybOFViQp5mMZGFIWJbdt_oiROwZLG-NDK2i932hepUfr0i52mrTX-M9vTwy4uQsiMh2eSI7Ntghw0_xgrqqp6HZON7RPdKo2ldC5_rt9TFKKmyXvhZFLgxwsm8bzvqlIbV4KwNbEZIhh-n0") 27 | 28 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 29 | // directly and pass in our Request and ResponseRecorder. 30 | p.Chain().ServeHTTP(rr, req) 31 | 32 | // Check the status code is what we expect. 33 | if status := rr.Code; status != http.StatusOK { 34 | t.Fatalf("Received status code '%d'. Response: '%s'", status, rr.Body.String()) 35 | } 36 | 37 | // Check the response body is what we expect. 38 | expected := string(`{"apiVersion":"v1","items":[],"kind":"List","metadata":{"resourceVersion":"","selfLink":""}}`) 39 | assert.JSONEq(expected, rr.Body.String(), "Got unexpected response body") 40 | 41 | } 42 | -------------------------------------------------------------------------------- /pkg/proxy/jwt.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "github.com/SermoDigital/jose/jws" 6 | "net/http" 7 | ) 8 | 9 | // WithJWT is a middleware that parses a JWT token from the requests and propagates 10 | // the request context with a claim value. 11 | func WithJWT() MiddlewareFunc { 12 | return func(next http.Handler) http.Handler { 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | 15 | // Get the JWT from the request 16 | t, err := jws.ParseJWTFromRequest(r) 17 | if err != nil { 18 | http.Error(w, err.Error(), http.StatusUnauthorized) 19 | return 20 | } 21 | 22 | // Check if request has empty credentials 23 | if t == nil { 24 | http.Error(w, "No valid access token", http.StatusUnauthorized) 25 | return 26 | } 27 | 28 | // Set context 29 | //username, ok := t.Claims().Get(c.OIDCUsernameClaim).(string) 30 | username, ok := t.Claims().Get("sub").(string) 31 | if !ok { 32 | username = "" 33 | } 34 | 35 | ctx := context.WithValue(r.Context(), subjectKey, username) 36 | 37 | next.ServeHTTP(w, r.WithContext(ctx)) 38 | 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/proxy/jwt_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestMiddlewareJWT(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | p, err := New(kubeConf) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | p.Use(WithJWT()) 18 | 19 | req, err := http.NewRequest("GET", "/dev-cluster-1/api/v1/pods/default", nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | rr := httptest.NewRecorder() 24 | 25 | /* 26 | / Test without auth header 27 | */ 28 | p.Chain().ServeHTTP(rr, req) 29 | 30 | // Check the status code is what we expect. 31 | assert.Equal(http.StatusUnauthorized, rr.Code, "Got unexpected status code") 32 | 33 | // Check the response body is what we expect. 34 | expected := "no token present in request\n" 35 | assert.Equal(expected, rr.Body.String(), "Got unexpected response body") 36 | 37 | /* 38 | / Test with a JWT in the auth header 39 | */ 40 | rr = httptest.NewRecorder() 41 | req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbWlyQG1pZGRsZXdhcmUuc2UiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.nSyFTR7SZ95-pkt_PcjbmVX7rZDizLxONOnF9HWhBIe1R6ir-rrzmOaXjVxfdcVlBKEFE9bz6PJMwD8-tqsZUqlOeXSLNXXeCGhdmhluBJrJMi-Ewyzmvm7yJ2L8bVfhhBJ3z_PivSbxMKLpWz7VkbwaJrk8950QkQ5oB_CV0ysoppTybGzvU1e8tc5h5wRKimju3BA3mA5HxN8K7-2lM_JZ8cbxBToGMBMsHKSy4VXAxm-lmvSwletLXqdSlqDQZejjJYYGaPpvDih1voTJ_FJnYFzx_NWq5qN416IGJrr1RAe92B2gfRUmzftFMMw8NEYBLDNXgKx3d9OOO9xKi9DxZ9wkFrZlwNZBj-VPTgNt5zeNgME8CJqgxvCaESuDAMWkjnfdyhBYAu9uUvbRSjFowFdQFumnVlKNfAlhKOQFOZpifFIwRFYda8lzvlJv1CzHEt500HgL2qofoIOTzFQNeJ_XkOQvRBy4eBkwxKvbHlwUAObxzZrCBjaAeQRGrMU926zpujSFQ_9KzUqNsNrxJWkBybOFViQp5mMZGFIWJbdt_oiROwZLG-NDK2i932hepUfr0i52mrTX-M9vTwy4uQsiMh2eSI7Ntghw0_xgrqqp6HZON7RPdKo2ldC5_rt9TFKKmyXvhZFLgxwsm8bzvqlIbV4KwNbEZIhh-n0") 42 | p.Chain().ServeHTTP(rr, req) 43 | 44 | // Check the status code is what we expect. 45 | assert.Equal(http.StatusOK, rr.Code, "Got unexpected status code") 46 | 47 | // Check the response body is what we expect. 48 | expected = string(`{"apiVersion":"v1","items":[],"kind":"List","metadata":{"resourceVersion":"","selfLink":""}}`) 49 | assert.JSONEq(expected, rr.Body.String(), "Got unexpected response body") 50 | 51 | } 52 | -------------------------------------------------------------------------------- /pkg/proxy/logging.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bufio" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // responseWriter implements http.ResponseWriter and adds status code and response length bytes 14 | // so that WithLogging middleware can log response status codes 15 | type responseWriter struct { 16 | http.ResponseWriter 17 | status int 18 | length int 19 | } 20 | 21 | // WriteHeader sends and sets an HTTP response header with the provided 22 | // status code. Implements the http.ResponseWriter interface 23 | func (r *responseWriter) WriteHeader(statusCode int) { 24 | r.status = statusCode 25 | r.ResponseWriter.WriteHeader(statusCode) 26 | } 27 | 28 | // Write implements the http.ResponseWriter interface 29 | func (r *responseWriter) Write(b []byte) (int, error) { 30 | if r.status == 0 { 31 | r.status = 200 32 | } 33 | n, err := r.ResponseWriter.Write(b) 34 | r.length += n 35 | return n, err 36 | } 37 | 38 | // Hijack implements the http.Hijacker interface 39 | func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 40 | if r.length < 0 { 41 | r.length = 0 42 | } 43 | return r.ResponseWriter.(http.Hijacker).Hijack() 44 | } 45 | 46 | // WithLogging applies access log style logging to the HTTP server 47 | func WithLogging() MiddlewareFunc { 48 | return func(next http.Handler) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | ctx := ParseContextFromRequest(r, false) 51 | start := time.Now() 52 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(ctx, r.Method, r.Proto)) 53 | lrw := &responseWriter{ResponseWriter: w} 54 | next.ServeHTTP(lrw, r) 55 | httpRequests.WithLabelValues(ctx, r.Method, r.Proto, strconv.Itoa(lrw.status)).Inc() 56 | timer.ObserveDuration() 57 | var isResCached bool 58 | if lrw.Header().Get("Multikube-Cache-Age") != "" { 59 | isResCached = true 60 | httpRequestsCached.WithLabelValues(ctx, r.Method, r.Proto, strconv.Itoa(lrw.status)).Inc() 61 | } 62 | duration := time.Now().Sub(start) 63 | log.Printf("%s %s %s %s %s %d %d %s %t", r.Method, r.URL.Path, r.URL.RawQuery, r.RemoteAddr, r.Proto, lrw.status, lrw.length, duration.String(), isResCached) 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/proxy/logging_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | -------------------------------------------------------------------------------- /pkg/proxy/metrics.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | var ( 8 | 9 | // http 10 | httpDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 11 | Name: "multikube_http_duration_seconds", 12 | Help: "A histogram of http request durations.", 13 | }, 14 | []string{"context", "method", "protocol"}, 15 | ) 16 | httpRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ 17 | Name: "multikube_http_requests_total", 18 | Help: "A counter for total http requests.", 19 | }, 20 | []string{"context", "method", "protocol", "code"}, 21 | ) 22 | httpRequestsCached = prometheus.NewCounterVec(prometheus.CounterOpts{ 23 | Name: "multikube_http_requests_cached_total", 24 | Help: "A counter for total cached http requests.", 25 | }, 26 | []string{"context", "method", "protocol", "code"}, 27 | ) 28 | 29 | // Backend 30 | backendHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 31 | Name: "multikube_backend_request_duration_seconds", 32 | Help: "A histogram of request latencies to backends", 33 | Buckets: prometheus.DefBuckets, 34 | }, 35 | []string{}, 36 | ) 37 | backendCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ 38 | Name: "multikube_backend_requests_total", 39 | Help: "A counter for requests to backends.", 40 | }, 41 | []string{}, 42 | ) 43 | backendGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 44 | Name: "multikube_backend_live_requests", 45 | Help: "A gauge of live requests currently in flight to backends", 46 | }) 47 | 48 | // OIDC 49 | oidcIssuerUp = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 50 | Name: "multikube_oidc_provider_up", 51 | Help: "", 52 | }, 53 | []string{"context", "issuer"}, 54 | ) 55 | oidcReqsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 56 | Name: "multikube_oidc_requests_total", 57 | Help: "A counter for total http requests.", 58 | }, 59 | []string{"context"}, 60 | ) 61 | oidcReqsAuthorized = prometheus.NewCounterVec(prometheus.CounterOpts{ 62 | Name: "multikube_oidc_requests_authorized_total", 63 | Help: "A counter for successfully authorized requests.", 64 | }, 65 | []string{"context"}, 66 | ) 67 | oidcReqsUnauthorized = prometheus.NewCounterVec(prometheus.CounterOpts{ 68 | Name: "multikube_oidc_requests_unauthorized_total", 69 | Help: "A counter for unauthorized requests.", 70 | }, 71 | []string{"context"}, 72 | ) 73 | 74 | // RS256 75 | rs256ReqsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 76 | Name: "multikube_rs256_requests_total", 77 | Help: "A counter for total http requests.", 78 | }, 79 | []string{"context"}, 80 | ) 81 | rs256ReqsAuthorized = prometheus.NewCounterVec(prometheus.CounterOpts{ 82 | Name: "multikube_rs256_requests_authorized_total", 83 | Help: "A counter for successfully authorized requests.", 84 | }, 85 | []string{"context"}, 86 | ) 87 | rs256ReqsUnauthorized = prometheus.NewCounterVec(prometheus.CounterOpts{ 88 | Name: "multikube_rs256_requests_unauthorized_total", 89 | Help: "A counter for unauthorized requests.", 90 | }, 91 | []string{"context"}, 92 | ) 93 | 94 | // Cache 95 | cacheLen = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 96 | Name: "multikube_cache_items_total", 97 | Help: "A gauge for the total amount of cached items.", 98 | }, 99 | []string{"context"}, 100 | ) 101 | cacheTTL = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 102 | Name: "multikube_cache_ttl_seconds", 103 | Help: "A gauge for the cache TTL in seconds.", 104 | }, 105 | []string{"context"}, 106 | ) 107 | ) 108 | 109 | func init() { 110 | prometheus.MustRegister( 111 | httpDuration, 112 | httpRequests, 113 | httpRequestsCached, 114 | backendHistogram, 115 | backendCounter, 116 | backendGauge, 117 | oidcIssuerUp, 118 | oidcReqsTotal, 119 | oidcReqsAuthorized, 120 | oidcReqsUnauthorized, 121 | rs256ReqsTotal, 122 | rs256ReqsAuthorized, 123 | rs256ReqsUnauthorized, 124 | cacheTTL, 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /pkg/proxy/oidc.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/json" 10 | "encoding/pem" 11 | "fmt" 12 | "github.com/SermoDigital/jose/crypto" 13 | "github.com/SermoDigital/jose/jws" 14 | "gopkg.in/square/go-jose.v2/jwt" 15 | "io/ioutil" 16 | "log" 17 | "math/big" 18 | "net/http" 19 | "net/url" 20 | "path" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | // OIDCConfig is configuration for OIDC middleware 26 | type OIDCConfig struct { 27 | OIDCIssuerURL string 28 | OIDCUsernameClaim string 29 | OIDCPollInterval time.Duration 30 | OIDCInsecureSkipVerify bool 31 | OIDCCa *x509.Certificate 32 | JWKS *JWKS 33 | } 34 | 35 | // JWKS is a representation of Json Web Key Store. It holds multiple JWK's in an array 36 | type JWKS struct { 37 | Keys []JSONWebKey `json:"keys"` 38 | } 39 | 40 | // JSONWebKey is a representation of a Json Web Key 41 | type JSONWebKey struct { 42 | Kty string `json:"kty"` 43 | Kid string `json:"kid"` 44 | Use string `json:"use"` 45 | N string `json:"n"` 46 | E string `json:"e"` 47 | X5c []string `json:"x5c"` 48 | } 49 | 50 | // openIDConfiguration is an internal type used to marshal/unmarshal openid connect configuration 51 | // from the provider. 52 | type openIDConfiguration struct { 53 | Issuer string `json:"issuer"` 54 | JwksURI string `json:"jwks_uri"` 55 | } 56 | 57 | // Find will loop through the keys on the JWKS and return that which has a matching key id 58 | func (j *JWKS) Find(s string) *JSONWebKey { 59 | for _, v := range j.Keys { 60 | if s == v.Kid { 61 | return &v 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // GetJWKSFromURL fetches the keys of an OpenID Connect endpoint in a go routine. It polls the endpoint 68 | // every n seconds. Returns a cancel function which can be called to stop polling and close the channel. 69 | // The endpoint must support OpenID Connect discovery as per https://openid.net/specs/openid-connect-discovery-1_0.html 70 | func (p *OIDCConfig) getJWKSFromURL() func() { 71 | 72 | // Make sure config has non-nil fields 73 | p.JWKS = &JWKS{ 74 | Keys: []JSONWebKey{}, 75 | } 76 | 77 | // Run a function in a go routine that continuously fetches from remote oidc provider 78 | quit := make(chan int) 79 | go func() { 80 | for { 81 | time.Sleep(p.OIDCPollInterval) 82 | select { 83 | case <-quit: 84 | close(quit) 85 | return 86 | default: 87 | // Make a request and fetch content of .well-known url (http://some-url/.well-known/openid-configuration) 88 | w, err := getWellKnown(p.OIDCIssuerURL, p.OIDCCa, p.OIDCInsecureSkipVerify) 89 | if err != nil { 90 | log.Printf("ERROR retrieving openid-configuration: %s", err) 91 | oidcIssuerUp.WithLabelValues(p.OIDCIssuerURL).Set(0) 92 | continue 93 | } 94 | // Get content of jwks_keys field 95 | j, err := getKeys(w.JwksURI, p.OIDCCa, p.OIDCInsecureSkipVerify) 96 | if err != nil { 97 | log.Printf("ERROR retrieving JWKS from provider: %s", err) 98 | oidcIssuerUp.WithLabelValues(p.OIDCIssuerURL).Set(0) 99 | continue 100 | } 101 | oidcIssuerUp.WithLabelValues(p.OIDCIssuerURL).Set(1) 102 | p.JWKS = j 103 | } 104 | } 105 | }() 106 | 107 | return func() { 108 | quit <- 1 109 | } 110 | 111 | } 112 | 113 | // WithOIDC is a middleware that validates a JWT token in the http request using an OIDC provider configured in c 114 | func WithOIDC(c OIDCConfig) MiddlewareFunc { 115 | 116 | c.getJWKSFromURL() 117 | 118 | return func(next http.Handler) http.Handler { 119 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 120 | 121 | ctxName := ParseContextFromRequest(r, false) 122 | oidcReqsTotal.WithLabelValues(ctxName).Inc() 123 | 124 | t, err := jws.ParseJWTFromRequest(r) 125 | if err != nil { 126 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 127 | http.Error(w, err.Error(), http.StatusUnauthorized) 128 | return 129 | } 130 | 131 | raw := string(getTokenFromRequest(r)) 132 | tok, err := jwt.ParseSigned(raw) 133 | if err != nil { 134 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 135 | http.Error(w, err.Error(), http.StatusUnauthorized) 136 | return 137 | } 138 | 139 | // Try to find a JWK using the kid 140 | kid := tok.Headers[0].KeyID 141 | jwk := c.JWKS.Find(kid) 142 | if jwk == nil { 143 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 144 | http.Error(w, "key id invalid", http.StatusUnauthorized) 145 | return 146 | } 147 | if jwk.Kty != "RSA" { 148 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 149 | http.Error(w, fmt.Sprintf("Invalid key type. Expected 'RSA' got '%s'", jwk.Kty), http.StatusUnauthorized) 150 | return 151 | } 152 | 153 | // decode the base64 bytes for n 154 | nb, err := base64.RawURLEncoding.DecodeString(jwk.N) 155 | if err != nil { 156 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 157 | http.Error(w, err.Error(), http.StatusUnauthorized) 158 | return 159 | } 160 | 161 | // Check if E is big-endian int 162 | if jwk.E != "AQAB" && jwk.E != "AAEAAQ" { 163 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 164 | http.Error(w, fmt.Sprintf("Expected E to be one of 'AQAB' and 'AAEAAQ' but got '%s'", jwk.E), http.StatusUnauthorized) 165 | return 166 | } 167 | 168 | pk := &rsa.PublicKey{ 169 | N: new(big.Int).SetBytes(nb), 170 | E: 65537, 171 | } 172 | 173 | err = t.Validate(pk, crypto.SigningMethodRS256) 174 | if err != nil { 175 | oidcReqsUnauthorized.WithLabelValues(ctxName).Inc() 176 | http.Error(w, err.Error(), http.StatusUnauthorized) 177 | return 178 | } 179 | 180 | username, ok := t.Claims().Get(c.OIDCUsernameClaim).(string) 181 | if !ok { 182 | username = "" 183 | } 184 | 185 | oidcReqsAuthorized.WithLabelValues(ctxName).Inc() 186 | 187 | ctx := context.WithValue(r.Context(), subjectKey, username) 188 | next.ServeHTTP(w, r.WithContext(ctx)) 189 | 190 | }) 191 | } 192 | } 193 | 194 | // getTokenFromRequest returns a []byte representation of JWT from an HTTP Authorization Bearer header 195 | func getTokenFromRequest(req *http.Request) []byte { 196 | if ah := req.Header.Get("Authorization"); len(ah) > 7 && strings.EqualFold(ah[0:7], "BEARER ") { 197 | return []byte(ah[7:]) 198 | } 199 | return nil 200 | } 201 | 202 | // dials an url which returns an array of Json Web Keys. The URL is typically 203 | // an OpenID Connect .well-formed URL as per https://openid.net/specs/openid-connect-discovery-1_0.html 204 | // Unmarshals it's json content into JWKS and returns it 205 | func getKeys(u string, ca *x509.Certificate, i bool) (*JWKS, error) { 206 | 207 | req, err := http.NewRequest("GET", u, nil) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | resp, err := tlsClient(ca, i).Do(req) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | defer resp.Body.Close() 218 | 219 | body, err := ioutil.ReadAll(resp.Body) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | var jwks *JWKS 225 | err = json.Unmarshal(body, &jwks) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | return jwks, nil 231 | } 232 | 233 | // dials the .well-known url and unmarshals it's json content into an OpenIDConfiguration 234 | // see https://openid.net/specs/openid-connect-discovery-1_0.html. 235 | // Accepts a trusted CA certificate as well as a bool to skip tls verification 236 | func getWellKnown(u string, ca *x509.Certificate, i bool) (*openIDConfiguration, error) { 237 | 238 | ul, err := url.Parse(u) 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | ul.Path = path.Join(ul.Path, ".well-known/openid-configuration") 244 | 245 | //wellKnownURL := fmt.Sprintf("%s/%s", u, "/.well-known/openid-configuration") 246 | req, err := http.NewRequest("GET", ul.String(), nil) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | resp, err := tlsClient(ca, i).Do(req) 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | defer resp.Body.Close() 257 | 258 | body, err := ioutil.ReadAll(resp.Body) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | var c *openIDConfiguration 264 | err = json.Unmarshal(body, &c) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | return c, nil 270 | 271 | } 272 | 273 | // Creates an http client with TLS configuration. If ca is nil then client without TLS configuration is returned instead 274 | // Set i to true to skip tls verification for this client 275 | func tlsClient(ca *x509.Certificate, i bool) *http.Client { 276 | tlsConfig := &tls.Config{ 277 | InsecureSkipVerify: true, 278 | } 279 | 280 | // Add tls config to client if ca isn't nil 281 | if ca != nil { 282 | caPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}) 283 | rootCAs, _ := x509.SystemCertPool() 284 | if rootCAs == nil { 285 | rootCAs = x509.NewCertPool() 286 | } 287 | rootCAs.AppendCertsFromPEM(caPem) 288 | tlsConfig.RootCAs = rootCAs 289 | } 290 | 291 | return &http.Client{ 292 | Transport: &http.Transport{ 293 | TLSClientConfig: tlsConfig, 294 | }, 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /pkg/proxy/oidc_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | -------------------------------------------------------------------------------- /pkg/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "github.com/amimof/multikube/pkg/cache" 8 | "io/ioutil" 9 | "k8s.io/client-go/tools/clientcmd/api" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // MiddlewareFunc defines a function to process middleware. 18 | type MiddlewareFunc func(http.Handler) http.Handler 19 | 20 | type ctxKey string 21 | 22 | var ( 23 | contextKey = ctxKey("Context") 24 | subjectKey = ctxKey("Subject") 25 | ) 26 | 27 | // Proxy implements an HTTP handler. It has a built-in transport with in-mem cache capabilities. 28 | type Proxy struct { 29 | kubeConfig *api.Config 30 | transports map[string]http.RoundTripper 31 | middleware []MiddlewareFunc 32 | } 33 | 34 | // New creates a new Proxy instance 35 | func New(c *api.Config) (*Proxy, error) { 36 | 37 | var transports = make(map[string]http.RoundTripper) 38 | 39 | for ctxKey := range c.Contexts { 40 | cluster := getClusterByContextName(c, ctxKey) 41 | auth := getAuthByContextName(c, ctxKey) 42 | tlsConfig, err := configureTLS(auth, cluster) 43 | if err != nil { 44 | continue 45 | } 46 | transports[ctxKey] = &Transport{ 47 | TLSClientConfig: tlsConfig, 48 | Cache: cache.New(), 49 | } 50 | } 51 | 52 | return &Proxy{ 53 | kubeConfig: c, 54 | transports: transports, 55 | }, nil 56 | } 57 | 58 | // WithHandler takes any http.Handler and returns it as a MiddlewareFunc so that it can be used in proxy 59 | func WithHandler(next http.Handler) MiddlewareFunc { 60 | return func(inner http.Handler) http.Handler { 61 | return next 62 | } 63 | } 64 | 65 | // Apply chains all middlewares and resturns a MiddlewareFunc that can wrap an http.Handler 66 | func (p *Proxy) Apply(middleware ...MiddlewareFunc) MiddlewareFunc { 67 | return func(final http.Handler) http.Handler { 68 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | last := final 70 | for i := len(p.middleware) - 1; i >= 0; i-- { 71 | last = p.middleware[i](last) 72 | } 73 | last.ServeHTTP(w, r) 74 | }) 75 | } 76 | } 77 | 78 | // Use adds a middleware 79 | func (p *Proxy) Use(middleware ...MiddlewareFunc) *Proxy { 80 | p.middleware = append(p.middleware, middleware...) 81 | return p 82 | } 83 | 84 | // Chain is a convenience function that chains all applied middleware and wraps proxy handler with it 85 | func (p *Proxy) Chain() http.Handler { 86 | h := p.Apply(p.middleware...) 87 | return h(p) 88 | } 89 | 90 | // CacheTTL sets the TTL value of all transports to d 91 | func (p *Proxy) CacheTTL(d time.Duration) { 92 | for key := range p.transports { 93 | if p.transports[key].(*Transport).Cache != nil { 94 | p.transports[key].(*Transport).Cache.TTL = d 95 | cacheTTL.WithLabelValues(key).Set(d.Seconds()) 96 | } 97 | } 98 | } 99 | 100 | // ServeHTTP routes the request to an apiserver. It determines, resolves an apiserver using 101 | // data in the request itsel such as certificate data, authorization bearer tokens, http headers etc. 102 | func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 103 | 104 | // Get the k8s context from the request 105 | ctx := ParseContextFromRequest(r, true) 106 | if ctx == "" { 107 | http.Error(w, "Not route: context not found", http.StatusBadGateway) 108 | return 109 | } 110 | 111 | // Get the subject from the request 112 | sub := ParseSubjectFromRequest(r) 113 | 114 | // // Get k8s cluster and authinfo from kubeconfig using the ctx name 115 | cluster := getClusterByContextName(p.kubeConfig, ctx) 116 | if cluster == nil { 117 | http.Error(w, fmt.Sprintf("no route: cluster not found for '%s'", ctx), http.StatusBadGateway) 118 | return 119 | } 120 | 121 | // Create an instance of golang reverse proxy and attach our own transport to it 122 | proxy := httputil.NewSingleHostReverseProxy(parseURL(cluster.Server)) 123 | proxy.Transport = p.transports[ctx] 124 | 125 | // Add some headers to the client request 126 | r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) 127 | r.Header.Set("Impersonate-User", sub) 128 | 129 | proxy.ServeHTTP(w, r) 130 | 131 | } 132 | 133 | // configureTLS composes a TLS configuration (tls.Config) from the provided Options parameter. 134 | // This is useful when building HTTP requests (for example with the net/http package) 135 | // and the TLS data is configured elsewhere. 136 | func configureTLS(a *api.AuthInfo, c *api.Cluster) (*tls.Config, error) { 137 | 138 | tlsConfig := &tls.Config{ 139 | InsecureSkipVerify: c.InsecureSkipTLSVerify, 140 | } 141 | 142 | // Load CA from file 143 | if c.CertificateAuthority != "" { 144 | caCert, err := ioutil.ReadFile(c.CertificateAuthority) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | caCertPool := x509.NewCertPool() 150 | caCertPool.AppendCertsFromPEM(caCert) 151 | tlsConfig.RootCAs = caCertPool 152 | } 153 | 154 | // Load CA from block 155 | if c.CertificateAuthorityData != nil { 156 | caCertPool := x509.NewCertPool() 157 | caCertPool.AppendCertsFromPEM(c.CertificateAuthorityData) 158 | tlsConfig.RootCAs = caCertPool 159 | } 160 | 161 | // Load certs from file 162 | if a.ClientCertificate != "" && a.ClientKey != "" { 163 | cert, err := tls.LoadX509KeyPair(a.ClientCertificate, a.ClientKey) 164 | if err != nil { 165 | return nil, err 166 | } 167 | tlsConfig.Certificates = []tls.Certificate{cert} 168 | tlsConfig.BuildNameToCertificate() 169 | } 170 | 171 | // Load certs from block 172 | if a.ClientCertificateData != nil && a.ClientKeyData != nil { 173 | cert, err := tls.X509KeyPair(a.ClientCertificateData, a.ClientKeyData) 174 | if err != nil { 175 | return nil, err 176 | } 177 | tlsConfig.Certificates = []tls.Certificate{cert} 178 | tlsConfig.BuildNameToCertificate() 179 | } 180 | 181 | return tlsConfig, nil 182 | } 183 | 184 | // parseURL is a helper function that tries to parse a string and return an url.URL. 185 | // Will return nil if errors occur. 186 | func parseURL(str string) *url.URL { 187 | u, err := url.Parse(str) 188 | if err != nil { 189 | return nil 190 | } 191 | return u 192 | } 193 | 194 | // ParseContextFromRequest tries to find the requested context name either by URL or HTTP header. 195 | // Will return the value of 'Multikube-Context' HTTP header. Will return the first part 196 | // of the URL path if no headers are set. Set replace to true if the URL path in provided request 197 | // should be replaced with a path without context name. 198 | func ParseContextFromRequest(req *http.Request, replace bool) string { 199 | val := req.Header.Get("Multikube-Context") 200 | if val != "" { 201 | return val 202 | } 203 | 204 | c, rem := getCtxFromURL(req.URL) 205 | if c != "" { 206 | val = c 207 | } 208 | 209 | if rem != "" && replace { 210 | req.URL.Path = rem 211 | } 212 | return val 213 | } 214 | 215 | // ParseSubjectFromRequest returns a string with the value of ContextKey key from 216 | // the HTTP request Context (context.Context) 217 | func ParseSubjectFromRequest(req *http.Request) string { 218 | if sub, ok := req.Context().Value(subjectKey).(string); ok { 219 | return sub 220 | } 221 | return "" 222 | } 223 | 224 | // getClusterByContextName returns an api.Cluster from the kubeconfig using context name. 225 | // Returns a new empty Cluster object with non-nil maps if no cluster found in the kubeconfig. 226 | func getClusterByContextName(kubeconfig *api.Config, n string) *api.Cluster { 227 | if ctx, ok1 := kubeconfig.Contexts[n]; ok1 { 228 | if clu, ok2 := kubeconfig.Clusters[ctx.Cluster]; ok2 { 229 | return clu 230 | } 231 | } 232 | return nil 233 | } 234 | 235 | // getAuthByContextName returns an api.AuthInfo from the kubeconfig using context name. 236 | // Returns a new empty AuthInfo object with non-nil maps if no cluster found in the kubeconfig. 237 | func getAuthByContextName(kubeconfig *api.Config, n string) *api.AuthInfo { 238 | if ctx, ok1 := kubeconfig.Contexts[n]; ok1 { 239 | if auth, ok2 := kubeconfig.AuthInfos[ctx.AuthInfo]; ok2 { 240 | return auth 241 | } 242 | } 243 | return nil 244 | } 245 | 246 | // getCtxFromURL reads path params from u and returns the kubeconfig context 247 | // as well as the path params used for upstream communication 248 | func getCtxFromURL(u *url.URL) (string, string) { 249 | val := "" 250 | rem := []string{} 251 | if vals := strings.Split(u.Path, "/"); len(vals) > 1 { 252 | val = vals[1] 253 | rem = vals[2:] 254 | } 255 | return val, fmt.Sprintf("/%s", strings.Join(rem, "/")) 256 | } 257 | -------------------------------------------------------------------------------- /pkg/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "k8s.io/client-go/tools/clientcmd/api" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var ( 14 | name = "dev-cluster-1" 15 | defServer = "https://real-k8s-server:8443" 16 | defToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbWlyQG1pZGRsZXdhcmUuc2UiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.nSyFTR7SZ95-pkt_PcjbmVX7rZDizLxONOnF9HWhBIe1R6ir-rrzmOaXjVxfdcVlBKEFE9bz6PJMwD8-tqsZUqlOeXSLNXXeCGhdmhluBJrJMi-Ewyzmvm7yJ2L8bVfhhBJ3z_PivSbxMKLpWz7VkbwaJrk8950QkQ5oB_CV0ysoppTybGzvU1e8tc5h5wRKimju3BA3mA5HxN8K7-2lM_JZ8cbxBToGMBMsHKSy4VXAxm-lmvSwletLXqdSlqDQZejjJYYGaPpvDih1voTJ_FJnYFzx_NWq5qN416IGJrr1RAe92B2gfRUmzftFMMw8NEYBLDNXgKx3d9OOO9xKi9DxZ9wkFrZlwNZBj-VPTgNt5zeNgME8CJqgxvCaESuDAMWkjnfdyhBYAu9uUvbRSjFowFdQFumnVlKNfAlhKOQFOZpifFIwRFYda8lzvlJv1CzHEt500HgL2qofoIOTzFQNeJ_XkOQvRBy4eBkwxKvbHlwUAObxzZrCBjaAeQRGrMU926zpujSFQ_9KzUqNsNrxJWkBybOFViQp5mMZGFIWJbdt_oiROwZLG-NDK2i932hepUfr0i52mrTX-M9vTwy4uQsiMh2eSI7Ntghw0_xgrqqp6HZON7RPdKo2ldC5_rt9TFKKmyXvhZFLgxwsm8bzvqlIbV4KwNbEZIhh-n0" 17 | ) 18 | 19 | var backendServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | fmt.Fprintln(w, `{"apiVersion":"v1","items":[],"kind":"List","metadata":{"resourceVersion":"","selfLink":""}}`) 21 | })) 22 | 23 | var kubeConf *api.Config = &api.Config{ 24 | APIVersion: "v1", 25 | Kind: "Config", 26 | Clusters: map[string]*api.Cluster{ 27 | name: { 28 | Server: backendServer.URL, 29 | }, 30 | }, 31 | AuthInfos: map[string]*api.AuthInfo{ 32 | name: { 33 | Token: defToken, 34 | }, 35 | }, 36 | Contexts: map[string]*api.Context{ 37 | name: { 38 | Cluster: name, 39 | AuthInfo: name, 40 | }, 41 | }, 42 | CurrentContext: name, 43 | } 44 | 45 | // Tests and empty proxy without config. Should return 502 bad gateway 46 | func TestProxy(t *testing.T) { 47 | p, err := New(kubeConf) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | req, err := http.NewRequest("GET", "/dev-cluster-1/api/v1/pods/default", nil) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | rr := httptest.NewRecorder() 57 | p.ServeHTTP(rr, req) 58 | 59 | if status := rr.Code; status != http.StatusOK { 60 | t.Fatalf("Got status code %d. Expected: %d", status, http.StatusOK) 61 | } 62 | 63 | } 64 | 65 | func TestProxyParseURL(t *testing.T) { 66 | urlString := "https://127.0.0.1:8443/api/v1/namespaces?limit=500" 67 | u := parseURL(urlString) 68 | assert.Equal(t, "127.0.0.1:8443", u.Host, "Got unexpected host in URL") 69 | assert.Equal(t, "/api/v1/namespaces", u.Path, "Got unexpected path in URL") 70 | assert.Equal(t, "limit=500", u.RawQuery, "Got unexpected query in URL") 71 | assert.Equal(t, "https", u.Scheme, "Got unexpected scheme in URL") 72 | } 73 | 74 | func TestProxy_getClusterByContextName(t *testing.T) { 75 | cluster := getClusterByContextName(kubeConf, "dev-cluster-1") 76 | assert.NotNil(t, cluster, nil) 77 | } 78 | 79 | func TestProxy_getAuthByContextName(t *testing.T) { 80 | cluster := getAuthByContextName(kubeConf, "dev-cluster-1") 81 | assert.NotNil(t, cluster, nil) 82 | } 83 | 84 | func TestProxy_SetCacheTTL(t *testing.T) { 85 | p, err := New(kubeConf) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | expected := time.Second * 12 90 | p.CacheTTL(expected) 91 | for key := range p.transports { 92 | assert.Equal(t, expected, p.transports[key].(*Transport).Cache.TTL, "Got unexpected cache ttl on at least 1 transport") 93 | } 94 | } 95 | 96 | func TestProxy_Use(t *testing.T) { 97 | p, err := New(kubeConf) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | p.Use(WithEmpty(), WithLogging(), WithJWT()) 102 | assert.Equal(t, 3, len(p.middleware), "Got unexpected number of middlewares") 103 | } 104 | 105 | func TestProxy_Apply(t *testing.T) { 106 | 107 | assert := assert.New(t) 108 | req, err := http.NewRequest("GET", "/dev-cluster-1/api/v1/pods/default", nil) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbWlyQG1pZGRsZXdhcmUuc2UiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.nSyFTR7SZ95-pkt_PcjbmVX7rZDizLxONOnF9HWhBIe1R6ir-rrzmOaXjVxfdcVlBKEFE9bz6PJMwD8-tqsZUqlOeXSLNXXeCGhdmhluBJrJMi-Ewyzmvm7yJ2L8bVfhhBJ3z_PivSbxMKLpWz7VkbwaJrk8950QkQ5oB_CV0ysoppTybGzvU1e8tc5h5wRKimju3BA3mA5HxN8K7-2lM_JZ8cbxBToGMBMsHKSy4VXAxm-lmvSwletLXqdSlqDQZejjJYYGaPpvDih1voTJ_FJnYFzx_NWq5qN416IGJrr1RAe92B2gfRUmzftFMMw8NEYBLDNXgKx3d9OOO9xKi9DxZ9wkFrZlwNZBj-VPTgNt5zeNgME8CJqgxvCaESuDAMWkjnfdyhBYAu9uUvbRSjFowFdQFumnVlKNfAlhKOQFOZpifFIwRFYda8lzvlJv1CzHEt500HgL2qofoIOTzFQNeJ_XkOQvRBy4eBkwxKvbHlwUAObxzZrCBjaAeQRGrMU926zpujSFQ_9KzUqNsNrxJWkBybOFViQp5mMZGFIWJbdt_oiROwZLG-NDK2i932hepUfr0i52mrTX-M9vTwy4uQsiMh2eSI7Ntghw0_xgrqqp6HZON7RPdKo2ldC5_rt9TFKKmyXvhZFLgxwsm8bzvqlIbV4KwNbEZIhh-n0") 114 | rr := httptest.NewRecorder() 115 | 116 | p, err := New(kubeConf) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | p.Use(WithEmpty(), WithLogging(), WithJWT()) 121 | middleware := p.Apply(p.middleware...) 122 | 123 | middleware(p).ServeHTTP(rr, req) 124 | 125 | expected := string(`{"apiVersion":"v1","items":[],"kind":"List","metadata":{"resourceVersion":"","selfLink":""}}`) 126 | assert.JSONEq(expected, rr.Body.String(), "Got unexpected response body") 127 | } 128 | -------------------------------------------------------------------------------- /pkg/proxy/rs256.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "github.com/SermoDigital/jose/crypto" 7 | "github.com/SermoDigital/jose/jws" 8 | "math/rand" 9 | "net/http" 10 | ) 11 | 12 | // RS256Config is configuration for RS256 middleware 13 | type RS256Config struct { 14 | PublicKey *rsa.PublicKey 15 | } 16 | 17 | // WithRS256 is a middleware that validates a JWT token in the http request using RS256 signing method. 18 | // It will do so using a rsa public key provided in Config 19 | func WithRS256(c RS256Config) MiddlewareFunc { 20 | 21 | return func(next http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | 24 | ctxName := ParseContextFromRequest(r, false) 25 | rs256ReqsTotal.WithLabelValues(ctxName).Inc() 26 | 27 | t, err := jws.ParseJWTFromRequest(r) 28 | if err != nil { 29 | rs256ReqsUnauthorized.WithLabelValues(ctxName).Inc() 30 | http.Error(w, err.Error(), http.StatusUnauthorized) 31 | return 32 | } 33 | 34 | if t == nil { 35 | rs256ReqsUnauthorized.WithLabelValues(ctxName).Inc() 36 | http.Error(w, "No token in request", http.StatusUnauthorized) 37 | return 38 | } 39 | 40 | err = t.Validate(c.PublicKey, crypto.SigningMethodRS256) 41 | if err != nil { 42 | rs256ReqsUnauthorized.WithLabelValues(ctxName).Inc() 43 | http.Error(w, err.Error(), http.StatusUnauthorized) 44 | return 45 | } 46 | 47 | // For security purposes a random value is set to username if no username claim is found or if the claim returns an empty string. 48 | // Kubernetes API will ignore user impersonation if this value is empty or nil. 49 | username, ok := t.Claims().Get("sub").(string) 50 | if !ok || username == "" { 51 | username = randomStr(10) 52 | } 53 | 54 | rs256ReqsAuthorized.WithLabelValues(ctxName).Inc() 55 | 56 | ctx := context.WithValue(r.Context(), subjectKey, username) 57 | next.ServeHTTP(w, r.WithContext(ctx)) 58 | 59 | }) 60 | } 61 | } 62 | 63 | // Returns a random fixed length string 64 | func randomStr(n int) string { 65 | letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 66 | b := make([]rune, n) 67 | for i := range b { 68 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 69 | } 70 | return string(b) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/proxy/rs256_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/SermoDigital/jose/crypto" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | var validRS256pubkey = []byte(`-----BEGIN PUBLIC KEY----- 12 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwTlp6YkFJlrhSJ7ukHJv 13 | wNe4+uUdTsizGK1u8Dh58EKpJkR9GrGLaB3LABC+CJpheBc5JU+Hd4UglEWIEFHK 14 | LSAEnsGXhl7bnuNeXxzBBM3LHTRfxQe/2rj69xuW7vbZ8pbhZ1+FVsxkznm28u6F 15 | zddAq2gLHCa0+Tc/IqqVTHx102fqzmOFMwLRHzTxoXaAx1uoRkngRK+8N3btWpQd 16 | hz1vHNPa1+6shuhPILpgGhcyGVsiLO3v4ZUdVZTw71295wTtPCLxoM/9F3o4VaRg 17 | dcrn9jTEUH/2uGgLNMlfpkbZaPk7p1GGaGjgaTZmFs25DurJjOADuNhiT+LXDLgo 18 | O6PIgIBlU6CUwcs7x9TZ1N7bqpWUOOVvIyZ65UNFIbExlAJPNENOer7voG+FJJ8W 19 | LYqmW/xGWG8sDsFZjHSpGaNq1do8eWa6y1X3eZfK7hmYWWxHDG4+0Rfcuf5lIUGq 20 | ChD6j7cVfJf4qRJNxHSccemM8H97MYKuHPTQM0NZruUPDDpKbwelVzglBT5PgNuv 21 | adPggesVbCunCIMggg2Wq47i551A+7Rb3Dki7FzjrHKiuv5CL+oGgxSN8jmCZXfc 22 | jIvzLIFNEY7lxHTyccY/YNgjNEMyUeu+9qI1sy5sAoco9HSncQIrVSD+VXtIhB4n 23 | rL/XzEalgKsZo5Z0rBmo7ycCAwEAAQ== 24 | -----END PUBLIC KEY-----`) 25 | 26 | var invalidRS256pubkey = []byte(`-----BEGIN PUBLIC KEY----- 27 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA21sntvLQsX8p+E7uwJLM 28 | MCyJaMn21rR8Bb2LttyZMt94xV7YKBuWKO1Y+9Qzy316qKdGYrMHSTKreear+g4B 29 | QBYvkom4vPReMZH+BW6sYavTyNqt0Akm+PmH/E8qRDIpvkXbA4goy/tM9Bychaxm 30 | JAKtPIVoXvdpbfmYML5XX5pC8zJuZTCMfu36ncgV+Bxyzf859uIe4oqxVxXMsbKk 31 | wK81kodtG5WeMYcO8xtHMfwtI97IMOlvN/3VUZMWc/wpiOE3CutkaQc/wdRQtQks 32 | fFGXHj1zUev2eB4zO+m7ks4zMCL58jIE1s1LlpE/lcEscIc8jPV6WGHuuUCnL7lB 33 | 8wIDAQAB 34 | -----END PUBLIC KEY-----`) 35 | 36 | var badRS256pubkey = []byte(`not_a_rsa_public_key`) 37 | 38 | func TestMiddlewareWithRS256Validation(t *testing.T) { 39 | assert := assert.New(t) 40 | req, err := http.NewRequest("GET", "/dev-cluster-1/api/v1/pods/default", nil) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | rr := httptest.NewRecorder() 45 | 46 | //req.Header.Set("Multikube-Context", "dev-cluster-1") 47 | req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbWlyQG1pZGRsZXdhcmUuc2UiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.nSyFTR7SZ95-pkt_PcjbmVX7rZDizLxONOnF9HWhBIe1R6ir-rrzmOaXjVxfdcVlBKEFE9bz6PJMwD8-tqsZUqlOeXSLNXXeCGhdmhluBJrJMi-Ewyzmvm7yJ2L8bVfhhBJ3z_PivSbxMKLpWz7VkbwaJrk8950QkQ5oB_CV0ysoppTybGzvU1e8tc5h5wRKimju3BA3mA5HxN8K7-2lM_JZ8cbxBToGMBMsHKSy4VXAxm-lmvSwletLXqdSlqDQZejjJYYGaPpvDih1voTJ_FJnYFzx_NWq5qN416IGJrr1RAe92B2gfRUmzftFMMw8NEYBLDNXgKx3d9OOO9xKi9DxZ9wkFrZlwNZBj-VPTgNt5zeNgME8CJqgxvCaESuDAMWkjnfdyhBYAu9uUvbRSjFowFdQFumnVlKNfAlhKOQFOZpifFIwRFYda8lzvlJv1CzHEt500HgL2qofoIOTzFQNeJ_XkOQvRBy4eBkwxKvbHlwUAObxzZrCBjaAeQRGrMU926zpujSFQ_9KzUqNsNrxJWkBybOFViQp5mMZGFIWJbdt_oiROwZLG-NDK2i932hepUfr0i52mrTX-M9vTwy4uQsiMh2eSI7Ntghw0_xgrqqp6HZON7RPdKo2ldC5_rt9TFKKmyXvhZFLgxwsm8bzvqlIbV4KwNbEZIhh-n0") 48 | 49 | /* 50 | / Test with a valid pub key 51 | */ 52 | pubkey, err := crypto.ParseRSAPublicKeyFromPEM(validRS256pubkey) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | p, err := New(kubeConf) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | p.Use( 63 | WithJWT(), 64 | WithRS256(RS256Config{ 65 | PublicKey: pubkey, 66 | }), 67 | ).Chain().ServeHTTP(rr, req) 68 | 69 | if status := rr.Code; status != http.StatusOK { 70 | t.Fatalf("Got status code %d. Expected: %d", status, http.StatusOK) 71 | } 72 | 73 | expected := string(`{"apiVersion":"v1","items":[],"kind":"List","metadata":{"resourceVersion":"","selfLink":""}}`) 74 | assert.JSONEq(expected, rr.Body.String(), "Got unexpected response body") 75 | 76 | /* 77 | / Test with an invalid pub key 78 | */ 79 | rr = httptest.NewRecorder() 80 | pubkey, err = crypto.ParseRSAPublicKeyFromPEM(invalidRS256pubkey) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | p, err = New(kubeConf) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | p.Use( 91 | WithJWT(), 92 | WithRS256(RS256Config{ 93 | PublicKey: pubkey, 94 | }), 95 | ).Chain().ServeHTTP(rr, req) 96 | 97 | assert.Equal(http.StatusUnauthorized, rr.Code, "Got unexpected status code") 98 | 99 | expected = "crypto/rsa: verification error\n" 100 | assert.Equal(expected, rr.Body.String(), "Got unexpected response body") 101 | 102 | /* 103 | / Test with a bad rsa pub key 104 | */ 105 | rr = httptest.NewRecorder() 106 | _, err = crypto.ParseRSAPublicKeyFromPEM(badRS256pubkey) 107 | assert.EqualError(err, "invalid key: Key must be PEM encoded PKCS1 or PKCS8 private key", "Got unexpected error") 108 | 109 | } 110 | -------------------------------------------------------------------------------- /pkg/proxy/transport.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "github.com/amimof/multikube/pkg/cache" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | "net" 10 | "net/http" 11 | "net/http/httputil" 12 | "time" 13 | ) 14 | 15 | // Transport is an implementation of RoundTripper and extension of http.Transport with the 16 | // addition of a Cache. 17 | type Transport struct { 18 | Cache *cache.Cache 19 | TLSClientConfig *tls.Config 20 | transport *http.Transport 21 | } 22 | 23 | // RoundTrip implements http.Transport 24 | func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) { 25 | 26 | // Use default transport with http2 if not told otherwise 27 | if t.transport == nil { 28 | t.transport = &http.Transport{ 29 | Proxy: http.ProxyFromEnvironment, 30 | DialContext: (&net.Dialer{ 31 | Timeout: 30 * time.Second, 32 | KeepAlive: 30 * time.Second, 33 | DualStack: true, 34 | }).DialContext, 35 | MaxIdleConns: 100, 36 | IdleConnTimeout: 90 * time.Second, 37 | TLSHandshakeTimeout: 10 * time.Second, 38 | ExpectContinueTimeout: 1 * time.Second, 39 | TLSClientConfig: t.TLSClientConfig, 40 | } 41 | } 42 | 43 | // Wrap our RoundTripper with Prometheus middleware. 44 | roundTripper := promhttp.InstrumentRoundTripperCounter(backendCounter, 45 | promhttp.InstrumentRoundTripperInFlight(backendGauge, 46 | promhttp.InstrumentRoundTripperDuration(backendHistogram, t.transport), 47 | ), 48 | ) 49 | 50 | // If no cache exists then carry out the request as usual 51 | if t.Cache == nil { 52 | res, err = roundTripper.RoundTrip(req) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return res, nil 57 | } 58 | 59 | // Cache the response 60 | item := t.Cache.Get(req.URL.String()) 61 | if item != nil { 62 | res, err = t.readResponse(req) 63 | if err != nil { 64 | return nil, err 65 | } 66 | res.Header.Set("Multikube-Cache-Age", item.Age().String()) 67 | } else { 68 | res, err = roundTripper.RoundTrip(req) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | // Cache any response 75 | _, err = t.cacheResponse(res) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return res, nil 81 | 82 | } 83 | 84 | // cacheResponse tries to commit a http.Response to the transport cache. 85 | // Careful! cacheResponse makes use of http.DumpResponse which will drain the original response and replace it with a new one 86 | func (t *Transport) cacheResponse(res *http.Response) (bool, error) { 87 | if t.Cache == nil { 88 | return false, nil 89 | } 90 | // Don't cache if method is not GET 91 | if res.Request.Method != http.MethodGet { 92 | return false, nil 93 | } 94 | // Don't cache if response code isn't 200 (OK) or 304 (NotModified) 95 | if !(res.StatusCode == http.StatusOK || res.StatusCode == http.StatusNotModified) { 96 | return false, nil 97 | } 98 | // Don't cache if certain url params are present (kubernetes streams) 99 | q := res.Request.URL.Query() 100 | if q.Get("watch") == "true" || q.Get("follow") == "true" { 101 | return false, nil 102 | } 103 | b, err := httputil.DumpResponse(res, true) 104 | if err != nil { 105 | return false, err 106 | } 107 | t.Cache.Set(res.Request.URL.String(), b) 108 | return true, nil 109 | } 110 | 111 | func (t *Transport) readResponse(req *http.Request) (*http.Response, error) { 112 | item := t.Cache.Get(req.URL.String()) 113 | if item.Value == nil { 114 | return nil, nil 115 | } 116 | b := bytes.NewBuffer(item.Value) 117 | return http.ReadResponse(bufio.NewReader(b), req) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/proxy/transport_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTransport(t *testing.T) { 8 | t.Logf("Tests not implemented") 9 | } 10 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "github.com/go-openapi/swag" 7 | "github.com/tylerb/graceful" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "strconv" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | ) 17 | 18 | const ( 19 | schemeHTTP = "http" 20 | schemeHTTPS = "https" 21 | schemeUnix = "unix" 22 | ) 23 | 24 | // Server for the multikube API 25 | type Server struct { 26 | EnabledListeners []string 27 | Host string 28 | Port int 29 | ListenLimit int 30 | TLSHost string 31 | TLSPort int 32 | TLSListenLimit int 33 | TLSCertificate string 34 | TLSCertificateKey string 35 | TLSCACertificate string 36 | SocketPath string 37 | Name string 38 | KeepAlive time.Duration 39 | ReadTimeout time.Duration 40 | WriteTimeout time.Duration 41 | TLSKeepAlive time.Duration 42 | TLSReadTimeout time.Duration 43 | TLSWriteTimeout time.Duration 44 | CleanupTimeout time.Duration 45 | MaxHeaderSize uint64 46 | Handler http.Handler 47 | 48 | shutdown chan struct{} 49 | httpServerL net.Listener 50 | httpsServerL net.Listener 51 | domainSocketL net.Listener 52 | hasListeners bool 53 | shuttingDown int32 54 | } 55 | 56 | // NewServer returns a default non-tls server 57 | func NewServer() *Server { 58 | return &Server{ 59 | EnabledListeners: []string{"http"}, 60 | CleanupTimeout: 10 * time.Second, 61 | MaxHeaderSize: 1000000, 62 | Host: "127.0.0.1", 63 | Port: 8080, 64 | ListenLimit: 0, 65 | KeepAlive: 3 * time.Minute, 66 | ReadTimeout: 30 * time.Second, 67 | WriteTimeout: 30 * time.Second, 68 | } 69 | } 70 | 71 | // NewServerTLS returns a default TLS enabled server 72 | func NewServerTLS() *Server { 73 | return &Server{ 74 | EnabledListeners: []string{"https"}, 75 | CleanupTimeout: 10 * time.Second, 76 | MaxHeaderSize: 1000000, 77 | TLSHost: "127.0.0.1", 78 | TLSPort: 8443, 79 | TLSCertificate: "", 80 | TLSCertificateKey: "", 81 | TLSCACertificate: "", 82 | TLSListenLimit: 0, 83 | TLSKeepAlive: 3 * time.Minute, 84 | TLSReadTimeout: 30 * time.Second, 85 | TLSWriteTimeout: 30 * time.Second, 86 | } 87 | } 88 | 89 | func (s *Server) hasScheme(scheme string) bool { 90 | for _, v := range s.EnabledListeners { 91 | if v == scheme { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | 98 | // Listen configures server listeners 99 | func (s *Server) Listen() error { 100 | 101 | if s.shutdown == nil { 102 | s.shutdown = make(chan struct{}) 103 | } 104 | 105 | if s.hasListeners { // already done this 106 | return nil 107 | } 108 | 109 | if s.hasScheme(schemeHTTPS) { 110 | // Use http host if https host wasn't defined 111 | if s.TLSHost == "" { 112 | s.TLSHost = s.Host 113 | } 114 | // Use http listen limit if https listen limit wasn't defined 115 | if s.TLSListenLimit == 0 { 116 | s.TLSListenLimit = s.ListenLimit 117 | } 118 | // Use http tcp keep alive if https tcp keep alive wasn't defined 119 | if int64(s.TLSKeepAlive) == 0 { 120 | s.TLSKeepAlive = s.KeepAlive 121 | } 122 | // Use http read timeout if https read timeout wasn't defined 123 | if int64(s.TLSReadTimeout) == 0 { 124 | s.TLSReadTimeout = s.ReadTimeout 125 | } 126 | // Use http write timeout if https write timeout wasn't defined 127 | if int64(s.TLSWriteTimeout) == 0 { 128 | s.TLSWriteTimeout = s.WriteTimeout 129 | } 130 | } 131 | 132 | if s.hasScheme(schemeUnix) { 133 | domSockListener, err := net.Listen("unix", string(s.SocketPath)) 134 | if err != nil { 135 | return err 136 | } 137 | s.domainSocketL = domSockListener 138 | } 139 | 140 | if s.hasScheme(schemeHTTP) { 141 | listener, err := net.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port))) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | h, p, err := swag.SplitHostPort(listener.Addr().String()) 147 | if err != nil { 148 | return err 149 | } 150 | s.Host = h 151 | s.Port = p 152 | s.httpServerL = listener 153 | } 154 | 155 | if s.hasScheme(schemeHTTPS) { 156 | tlsListener, err := net.Listen("tcp", net.JoinHostPort(s.TLSHost, strconv.Itoa(s.TLSPort))) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | sh, sp, err := swag.SplitHostPort(tlsListener.Addr().String()) 162 | if err != nil { 163 | return err 164 | } 165 | s.TLSHost = sh 166 | s.TLSPort = sp 167 | s.httpsServerL = tlsListener 168 | } 169 | 170 | s.hasListeners = true 171 | return nil 172 | } 173 | 174 | // Serve the api 175 | func (s *Server) Serve() error { 176 | 177 | if !s.hasListeners { 178 | err := s.Listen() 179 | if err != nil { 180 | return err 181 | } 182 | } 183 | 184 | if s.Name == "" { 185 | s.Name = "multikube" 186 | } 187 | 188 | var wg sync.WaitGroup 189 | 190 | if s.hasScheme(schemeUnix) { 191 | domainSocket := &graceful.Server{Server: new(http.Server)} 192 | domainSocket.MaxHeaderBytes = int(s.MaxHeaderSize) 193 | if int64(s.CleanupTimeout) > 0 { 194 | domainSocket.Timeout = s.CleanupTimeout 195 | } 196 | 197 | domainSocket.Handler = s.Handler 198 | 199 | wg.Add(2) 200 | log.Printf("Serving %s at unix://%s", s.Name, s.SocketPath) 201 | go func(l net.Listener) { 202 | defer wg.Done() 203 | if err := domainSocket.Serve(l); err != nil { 204 | log.Fatalf("%v", err) 205 | } 206 | log.Printf("Stopped serving %s at unix://%s", s.Name, s.SocketPath) 207 | }(s.domainSocketL) 208 | go s.handleShutdown(&wg, domainSocket) 209 | } 210 | 211 | if s.hasScheme(schemeHTTP) { 212 | httpServer := &graceful.Server{Server: new(http.Server)} 213 | httpServer.MaxHeaderBytes = int(s.MaxHeaderSize) 214 | httpServer.ReadTimeout = s.ReadTimeout 215 | httpServer.WriteTimeout = s.WriteTimeout 216 | httpServer.SetKeepAlivesEnabled(int64(s.KeepAlive) > 0) 217 | httpServer.TCPKeepAlive = s.KeepAlive 218 | if s.ListenLimit > 0 { 219 | httpServer.ListenLimit = s.ListenLimit 220 | } 221 | 222 | if int64(s.CleanupTimeout) > 0 { 223 | httpServer.Timeout = s.CleanupTimeout 224 | } 225 | 226 | httpServer.Handler = s.Handler 227 | 228 | wg.Add(2) 229 | log.Printf("Serving %s at http://%s", s.Name, s.httpServerL.Addr()) 230 | go func(l net.Listener) { 231 | defer wg.Done() 232 | if err := httpServer.Serve(l); err != nil { 233 | log.Printf("%v", err) 234 | } 235 | log.Printf("Stopped serving %s at http://%s", s.Name, l.Addr()) 236 | }(s.httpServerL) 237 | go s.handleShutdown(&wg, httpServer) 238 | } 239 | 240 | if s.hasScheme(schemeHTTPS) { 241 | 242 | srv := http.Server{} 243 | httpsServer := &graceful.Server{Server: &srv} 244 | httpsServer.MaxHeaderBytes = int(s.MaxHeaderSize) 245 | //httpsServer.ReadTimeout = s.TLSReadTimeout 246 | //httpsServer.WriteTimeout = s.TLSWriteTimeout 247 | httpsServer.SetKeepAlivesEnabled(int64(s.TLSKeepAlive) > 0) 248 | httpsServer.TCPKeepAlive = s.TLSKeepAlive 249 | if s.TLSListenLimit > 0 { 250 | httpsServer.ListenLimit = s.TLSListenLimit 251 | } 252 | if int64(s.CleanupTimeout) > 0 { 253 | httpsServer.Timeout = s.CleanupTimeout 254 | } 255 | 256 | httpsServer.Handler = s.Handler 257 | 258 | // Inspired by https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go 259 | httpsServer.TLSConfig = &tls.Config{ 260 | // Causes servers to use Go's default ciphersuite preferences, 261 | // which are tuned to avoid attacks. Does nothing on clients. 262 | PreferServerCipherSuites: true, 263 | // Only use curves which have assembly implementations 264 | // https://github.com/golang/go/tree/master/src/crypto/elliptic 265 | CurvePreferences: []tls.CurveID{tls.CurveP256}, 266 | // Use modern tls mode https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility 267 | NextProtos: []string{"h2", "http/1.1"}, 268 | // https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols 269 | MinVersion: tls.VersionTLS12, 270 | // These ciphersuites support Forward Secrecy: https://en.wikipedia.org/wiki/Forward_secrecy 271 | CipherSuites: []uint16{ 272 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 273 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 274 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 275 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 276 | }, 277 | ClientAuth: tls.RequestClientCert, 278 | } 279 | 280 | if s.TLSCertificate != "" && s.TLSCertificateKey != "" { 281 | httpsServer.TLSConfig.Certificates = make([]tls.Certificate, 1) 282 | cert, err := tls.LoadX509KeyPair(s.TLSCertificate, s.TLSCertificateKey) 283 | if err != nil { 284 | return err 285 | } 286 | httpsServer.TLSConfig.Certificates[0] = cert 287 | } 288 | 289 | if s.TLSCACertificate != "" { 290 | caCert, err := ioutil.ReadFile(s.TLSCACertificate) 291 | if err != nil { 292 | return err 293 | } 294 | caCertPool := x509.NewCertPool() 295 | caCertPool.AppendCertsFromPEM(caCert) 296 | httpsServer.TLSConfig.ClientCAs = caCertPool 297 | httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert 298 | } 299 | 300 | httpsServer.TLSConfig.BuildNameToCertificate() 301 | 302 | if len(httpsServer.TLSConfig.Certificates) == 0 { 303 | if s.TLSCertificate == "" { 304 | if s.TLSCertificateKey == "" { 305 | log.Fatalf("the required flags `--tls-certificate` and `--tls-key` were not specified") 306 | } 307 | log.Printf("the required flag `--tls-certificate` was not specified") 308 | } 309 | if s.TLSCertificateKey == "" { 310 | log.Fatalf("the required flag `--tls-key` was not specified") 311 | } 312 | } 313 | 314 | wg.Add(2) 315 | log.Printf("Serving %s at https://%s", s.Name, s.httpsServerL.Addr()) 316 | go func(l net.Listener) { 317 | defer wg.Done() 318 | if err := httpsServer.Serve(l); err != nil { 319 | log.Fatalf("%v", err) 320 | } 321 | log.Printf("Stopped serving %s at https://%s", s.Name, l.Addr()) 322 | }(tls.NewListener(s.httpsServerL, httpsServer.TLSConfig)) 323 | go s.handleShutdown(&wg, httpsServer) 324 | } 325 | 326 | wg.Wait() 327 | return nil 328 | } 329 | 330 | func (s *Server) handleShutdown(wg *sync.WaitGroup, server *graceful.Server) { 331 | defer wg.Done() 332 | for { 333 | select { 334 | case <-s.shutdown: 335 | atomic.AddInt32(&s.shuttingDown, 1) 336 | server.Stop(s.CleanupTimeout) 337 | <-server.StopChan() 338 | log.Printf("Shutting down") 339 | return 340 | case <-server.StopChan(): 341 | atomic.AddInt32(&s.shuttingDown, 1) 342 | log.Printf("Shutting down") 343 | return 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /pkg/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestServer(t *testing.T) { 8 | t.Logf("Tests not implemented") 9 | } 10 | --------------------------------------------------------------------------------