├── .dockerignore ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile_loadtest ├── LICENSE.txt ├── Makefile ├── README.md ├── _protogen └── kedge │ └── config │ └── grpc │ └── routes │ ├── adhoc.pb.go │ └── adhoc.validator.pb.go ├── cmd ├── kedge │ ├── auth.go │ ├── configs.go │ ├── main.go │ ├── term.go │ ├── tls.go │ └── version.go └── winch │ ├── main.go │ └── version.go ├── docs ├── k8s_resolver.md ├── kedge.md ├── kedge_native_dialer_certs.png ├── kedge_winch_oidc.png ├── winch.jpg └── winch.md ├── go.mod ├── go.sum ├── misc ├── backendpool.json ├── ca.crt ├── ca.key ├── client.crt ├── client.csr ├── client.key ├── client.p12 ├── director.json ├── gen_cert.sh ├── localhost.crt ├── localhost.csr ├── localhost.key ├── pac.http.js ├── pac.https.js ├── winch_auth.json └── winch_mapper.json ├── pkg ├── bearertokenauth │ └── bearertokenauth.go ├── discovery │ ├── client.go │ ├── construct.go │ ├── discovery.go │ ├── streamer.go │ ├── updater.go │ ├── updater_grpc_test.go │ └── updater_http_test.go ├── e2e │ ├── common.go │ ├── grpc_test.go │ ├── http_test.go │ ├── spinup_test.go │ └── wpad_test.go ├── grpcutils │ └── metadata.go ├── http │ ├── ctxtags │ │ └── tags.go │ ├── header │ │ ├── request.go │ │ └── response.go │ └── tripperware │ │ ├── auth.go │ │ ├── debug.go │ │ ├── map.go │ │ ├── route.go │ │ ├── tripperware.go │ │ └── tripperware_test.go ├── k8s │ ├── client.go │ └── flags.go ├── kedge │ ├── common │ │ └── common.go │ ├── grpc │ │ ├── backendpool │ │ │ ├── backend.go │ │ │ ├── dynamic.go │ │ │ ├── dynamic_test.go │ │ │ ├── pool.go │ │ │ └── static.go │ │ ├── client │ │ │ └── client.go │ │ ├── director │ │ │ ├── adhoc │ │ │ │ └── adhoc.go │ │ │ ├── director.go │ │ │ └── router │ │ │ │ ├── router.go │ │ │ │ └── router_test.go │ │ └── integration_test.go │ └── http │ │ ├── backendpool │ │ ├── backend.go │ │ ├── dynamic.go │ │ ├── dynamic_test.go │ │ ├── pool.go │ │ └── static.go │ │ ├── client │ │ └── client.go │ │ ├── director │ │ ├── adhoc │ │ │ ├── adhoc.go │ │ │ └── adhoc_test.go │ │ ├── proxy.go │ │ ├── proxyreq │ │ │ └── request.go │ │ └── router │ │ │ ├── error.go │ │ │ ├── router.go │ │ │ └── router_test.go │ │ ├── integration.go │ │ ├── integration_test.go │ │ └── lbtransport │ │ ├── body.go │ │ ├── body_test.go │ │ ├── policy.go │ │ ├── policy_test.go │ │ ├── transport.go │ │ └── transport_test.go ├── logstash │ ├── conn.go │ ├── conn_test.go │ ├── logstash.go │ ├── logstash_formatter.go │ └── reporter.go ├── map │ ├── iface.go │ ├── route.go │ ├── route_test.go │ ├── simple.go │ ├── single.go │ ├── suffix.go │ └── suffix_test.go ├── metrics │ ├── backend_configuration.go │ └── kedge_error.go ├── reporter │ ├── errtypes │ │ └── errtypes.go │ └── reporter.go ├── resolvers │ ├── host │ │ ├── host.go │ │ └── host_test.go │ ├── k8s │ │ ├── client.go │ │ ├── resolver.go │ │ ├── resolver_test.go │ │ ├── streamer.go │ │ ├── streamer_test.go │ │ ├── watcher.go │ │ └── watcher_test.go │ └── srv │ │ ├── srv.go │ │ └── srv_test.go ├── sharedflags │ └── set.go ├── tls │ └── client.go ├── tokenauth │ ├── http │ │ └── tripper.go │ ├── source.go │ └── sources │ │ ├── direct │ │ └── direct.go │ │ ├── k8s │ │ └── k8s.go │ │ ├── oauth2 │ │ ├── gcp.go │ │ └── oauth2.go │ │ ├── oidc │ │ └── oidc.go │ │ └── test │ │ └── test.go └── winch │ ├── auth.go │ ├── grpc │ ├── integration_test.go │ └── proxy.go │ ├── http │ ├── integration_test.go │ └── proxy.go │ ├── pac.go │ ├── routes.go │ └── routes_test.go ├── proto ├── e2e │ └── hello.proto ├── kedge │ └── config │ │ ├── backendpool.proto │ │ ├── common │ │ ├── adhoc.proto │ │ └── resolvers │ │ │ └── resolvers.proto │ │ ├── director.proto │ │ ├── grpc │ │ ├── backends │ │ │ └── backend.proto │ │ └── routes │ │ │ └── routes.proto │ │ └── http │ │ ├── backends │ │ └── backend.proto │ │ └── routes │ │ └── routes.proto └── winch │ └── config │ ├── auth.proto │ └── mapper.proto ├── protogen ├── e2e │ ├── hello.pb.go │ └── hello.validator.pb.go ├── kedge │ └── config │ │ ├── backendpool.pb.go │ │ ├── backendpool.validator.pb.go │ │ ├── common │ │ ├── adhoc.pb.go │ │ ├── adhoc.validator.pb.go │ │ └── resolvers │ │ │ ├── resolvers.pb.go │ │ │ └── resolvers.validator.pb.go │ │ ├── director.pb.go │ │ ├── director.validator.pb.go │ │ ├── grpc │ │ ├── backends │ │ │ ├── backend.pb.go │ │ │ └── backend.validator.pb.go │ │ └── routes │ │ │ ├── routes.pb.go │ │ │ └── routes.validator.pb.go │ │ └── http │ │ ├── backends │ │ ├── backend.pb.go │ │ └── backend.validator.pb.go │ │ └── routes │ │ ├── routes.pb.go │ │ └── routes.validator.pb.go └── winch │ └── config │ ├── auth.pb.go │ ├── auth.validator.pb.go │ ├── mapper.pb.go │ └── mapper.validator.pb.go ├── scripts └── protogen.sh └── tools ├── discovery └── main.go ├── loadtest ├── README.md ├── main.go └── metrics.go └── resolver └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | Dockerfile 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.swp 3 | 4 | # For codecov file. 5 | coverage.txt 6 | 7 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Folders 13 | _obj 14 | _test 15 | 16 | # Architecture specific extensions/prefixes 17 | *.[568vq] 18 | [568vq].out 19 | 20 | *.cgo1.go 21 | *.cgo2.c 22 | _cgo_defun.c 23 | _cgo_gotypes.go 24 | _cgo_export.* 25 | 26 | _testmain.go 27 | 28 | *.exe 29 | *.test 30 | *.prof 31 | 32 | vendor/* 33 | cmd/kedge/kedge 34 | cmd/winch/winch 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.13.x 5 | 6 | matrix: 7 | include: 8 | - os: windows 9 | dist: 1803-containers 10 | env: VERSION=1.13.2 VARIANT=windows/windowsservercore-1803 11 | - os: linux 12 | env: VERSION=1.13.2 VARIANT=buster 13 | 14 | go_import_path: github.com/improbable-eng/kedge 15 | 16 | before_install: 17 | # disabling windows defender to make the build quicker 18 | - if [ "$TRAVIS_OS_NAME" = "windows" ]; then powershell -command 'Set-MpPreference -DisableRealtimeMonitoring $true'; fi 19 | - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install make; fi 20 | 21 | install: 22 | - export GOBIN="$GOPATH/bin" 23 | - make deps 24 | 25 | script: 26 | # Different line endings on windows trigger a reformatting false positive, see https://github.com/golangci/golangci-lint/issues/580 27 | - if [ "$TRAVIS_OS_NAME" != "windows" ]; then make vet; fi 28 | - make test -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 as build 2 | MAINTAINER Improbable Team 3 | 4 | ADD . ${GOPATH}/src/github.com/improbable-eng/kedge 5 | WORKDIR ${GOPATH}/src/github.com/improbable-eng/kedge 6 | 7 | ARG BUILD_VERSION 8 | RUN echo "Installing Kedge with version ${BUILD_VERSION}" 9 | RUN go install -ldflags "-X main.BuildVersion=${BUILD_VERSION}" github.com/improbable-eng/kedge/cmd/kedge 10 | RUN go install -ldflags "-X main.BuildVersion=${BUILD_VERSION}" github.com/improbable-eng/kedge/cmd/winch 11 | 12 | FROM ubuntu:18.04 13 | 14 | RUN apt-get update && apt-get install -qq -y --no-install-recommends git wget curl ca-certificates openssh-client 15 | 16 | RUN mkdir /etc/corp-auth 17 | RUN echo "StrictHostKeyChecking no" > /etc/ssh/ssh_config 18 | 19 | COPY --from=build /go/bin/winch /go/bin/kedge / 20 | 21 | ENTRYPOINT ["/kedge"] 22 | 23 | -------------------------------------------------------------------------------- /Dockerfile_loadtest: -------------------------------------------------------------------------------- 1 | #TAG:16.04 2 | FROM ubuntu@sha256:71cd81252a3563a03ad8daee81047b62ab5d892ebbfbf71cf53415f29c130950 3 | MAINTAINER Improbable Team 4 | 5 | ENV GOLANG_VERSION 1.8.1 6 | ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz 7 | ENV GITBRANCH master 8 | ENV PATH /usr/local/go/bin:$PATH 9 | ENV GOPATH=/go 10 | ENV GOBIN=/go/bin 11 | 12 | RUN mkdir /etc/corp-auth 13 | 14 | RUN apt-get update && apt-get install -qq -y --no-install-recommends git vim wget curl ca-certificates openssh-client 15 | 16 | RUN curl -fsSL "${GOLANG_DOWNLOAD_URL}" -o golang.tar.gz \ 17 | && tar -C /usr/local -xzf golang.tar.gz \ 18 | && rm golang.tar.gz 19 | 20 | RUN echo "StrictHostKeyChecking no" > /etc/ssh/ssh_config 21 | 22 | ENV PATH ${PATH}:${GOBIN} 23 | RUN mkdir -p /go/bin 24 | RUN wget -O ${GOBIN}/dep "https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64" && chmod +x ${GOBIN}/dep 25 | # Copy local to not clone everything. 26 | ADD . ${GOPATH}/src/github.com/improbable-eng/kedge 27 | RUN cd ${GOPATH}/src/github.com/improbable-eng/kedge && dep ensure 28 | 29 | ARG BUILD_VERSION 30 | RUN echo "Installing LoadTester" 31 | RUN go install github.com/improbable-eng/kedge/tools/loadtest 32 | 33 | # This image is designed to be run e.g as a pod in separate k8s cluster to target some kedge for load test. 34 | # This container does not include winch, so make sure to configure loadtest against kedge directly. 35 | ENTRYPOINT ["bash"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= $(shell pwd) 2 | FILES ?= $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*pb.go') 3 | DOCKER_IMAGE_NAME ?= kedge 4 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 5 | 6 | all: build 7 | 8 | format: 9 | @echo ">> formatting code" 10 | @goimports -w $(FILES) 11 | 12 | deps: install-tools 13 | @echo ">> downloading dependencies" 14 | @go mod download 15 | 16 | build: 17 | @echo ">> building kedge" 18 | @go install github.com/improbable-eng/kedge/cmd/kedge 19 | @echo ">> building winch" 20 | @go install github.com/improbable-eng/kedge/cmd/winch 21 | 22 | vet: 23 | @goimports -d $(FILES) >> diff.txt 24 | @if [ -s diff.txt ]; then echo "Please format your code with 'make format'."; cat diff.txt; rm diff.txt; exit 1; fi 25 | @rm diff.txt 26 | @echo ">> vetting code" 27 | @go vet ./... 28 | 29 | install-tools: 30 | @echo ">> fetching goimports" 31 | @go get -u golang.org/x/tools/cmd/goimports 32 | 33 | proto: 34 | @echo ">> generating protobufs" 35 | @./scripts/protogen.sh 36 | 37 | test: build 38 | @echo ">> running all tests" 39 | @go test $(shell go list ./... | grep -v /vendor/) 40 | 41 | docker: 42 | @echo ">> building docker image" 43 | @docker build --build-arg BUILD_VERSION=$(date +%Y%m%d-%H%M%S)-001 -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . 44 | 45 | .PHONY: all format deps build vet install-tools proto test docker 46 | -------------------------------------------------------------------------------- /_protogen/kedge/config/grpc/routes/adhoc.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. 2 | // source: kedge/config/grpc/routes/adhoc.proto 3 | // DO NOT EDIT! 4 | 5 | /* 6 | Package kedge_config_grpc_routes is a generated protocol buffer package. 7 | 8 | It is generated from these files: 9 | kedge/config/grpc/routes/adhoc.proto 10 | kedge/config/grpc/routes/routes.proto 11 | 12 | It has these top-level messages: 13 | Adhoc 14 | Route 15 | */ 16 | package kedge_config_grpc_routes 17 | 18 | import github_com_mwitkow_go_proto_validators "github.com/mwitkow/go-proto-validators" 19 | import proto "github.com/golang/protobuf/proto" 20 | import fmt "fmt" 21 | import math "math" 22 | 23 | // Reference imports to suppress errors if they are not otherwise used. 24 | var _ = proto.Marshal 25 | var _ = fmt.Errorf 26 | var _ = math.Inf 27 | 28 | func (this *Adhoc) Validate() error { 29 | if this.Port != nil { 30 | if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(this.Port); err != nil { 31 | return github_com_mwitkow_go_proto_validators.FieldError("Port", err) 32 | } 33 | } 34 | return nil 35 | } 36 | func (this *Adhoc_Port) Validate() error { 37 | for _, item := range this.AllowedRanges { 38 | if item != nil { 39 | if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(item); err != nil { 40 | return github_com_mwitkow_go_proto_validators.FieldError("AllowedRanges", err) 41 | } 42 | } 43 | } 44 | return nil 45 | } 46 | func (this *Adhoc_Port_Range) Validate() error { 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/kedge/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/Bplotka/oidc/authorize" 8 | "github.com/improbable-eng/kedge/pkg/bearertokenauth" 9 | "github.com/improbable-eng/kedge/pkg/sharedflags" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | flagOIDCProvider = sharedflags.Set.String("server_oidc_provider_url", "", 15 | "Expected OIDC Issuer of the request's IDToken. If empty, no OIDC authorization will be required.") 16 | flagOIDCClientID = sharedflags.Set.String("server_oidc_client_id", "", 17 | "Expected OIDC Client ID of the request`s IDToken.") 18 | flagOIDCPermsClaim = sharedflags.Set.String("server_oidc_perms_claim", "", 19 | "Name of the claim that stores user's permissions.") 20 | flagOIDCWhiteListPerms = sharedflags.Set.StringSlice("server_oidc_whitelist_perms", []string(nil), 21 | "Permissions satisfy Kedge access auth.") 22 | flagEnableOIDCAuthForDebugEnpoints = sharedflags.Set.Bool("server_enable_oidc_for_debug_endpoints", false, 23 | "If true, debug endpoints will be hidden by OIDC Auth with the same configuration as proxy.") 24 | flagUnsafeBearerToken = sharedflags.Set.String("unsafe_bearer_token_proxy_auth", "", 25 | "If set, all requests via Kedge for routes proxy_auth set to token will be checked for the specified "+ 26 | "bearer token in the Proxy-Authorization header.") 27 | ) 28 | 29 | func authorizerFromFlags(entry *logrus.Entry) (authorize.Authorizer, error) { 30 | if *flagUnsafeBearerToken != "" { 31 | if *flagOIDCProvider != "" || *flagOIDCClientID != "" || *flagOIDCPermsClaim != "" || len(*flagOIDCWhiteListPerms) > 0 || *flagEnableOIDCAuthForDebugEnpoints { 32 | return nil, errors.New("cannot enable both Basic and OIDC auth") 33 | } 34 | return basicAuthAuthorizerFromFlags(entry) 35 | } 36 | return oidcAuthorizerFromFlags(entry) 37 | } 38 | 39 | func basicAuthAuthorizerFromFlags(entry *logrus.Entry) (authorize.Authorizer, error) { 40 | return bearertokenauth.NewAuthorizer(*flagUnsafeBearerToken), nil 41 | } 42 | 43 | func oidcAuthorizerFromFlags(entry *logrus.Entry) (authorize.Authorizer, error) { 44 | if *flagOIDCProvider == "" { 45 | entry.Warn("No OIDC authorization is configured.") 46 | return nil, nil 47 | } 48 | if *flagOIDCClientID == "" { 49 | return nil, errors.New("OIDC flag validation failed. server_oidc_client_id is missing.") 50 | } 51 | 52 | if *flagOIDCPermsClaim == "" { 53 | return nil, errors.New("OIDC flag validation failed. server_oidc_perms_claim flag is missing.") 54 | } 55 | 56 | if len(*flagOIDCWhiteListPerms) == 0 { 57 | return nil, errors.New("OIDC flag validation failed. server_oidc_whitelist_perms flag cannot be empty.") 58 | } 59 | 60 | var condition []authorize.Condition 61 | for _, permToWhitelist := range *flagOIDCWhiteListPerms { 62 | condition = append(condition, authorize.Contains(permToWhitelist)) 63 | } 64 | 65 | cond, err := authorize.OR(condition...) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return authorize.New( 70 | context.Background(), 71 | authorize.Config{ 72 | Provider: *flagOIDCProvider, 73 | ClientID: *flagOIDCClientID, 74 | PermsClaim: *flagOIDCPermsClaim, 75 | PermCondition: cond, 76 | }, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/kedge/term.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/improbable-eng/kedge/pkg/sharedflags" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var ( 15 | signalShutdownTimeout = sharedflags.Set.Duration( 16 | "server_sigterm_timeout", 17 | 40*time.Second, 18 | "Timeout for graceful Shutdown after catching a signal. "+ 19 | "If the timeout expires, another SIGTERM is sent that skips the Shutdown handler") 20 | ) 21 | 22 | // waitForAny blocks until any of given signal is received, or context is done. 23 | // The SIGTERM is signaled after some time, delaying it for servers to have time to graceful shutdown. 24 | func waitForAny(ctx context.Context, signals ...os.Signal) { 25 | ch := make(chan os.Signal, len(signals)) 26 | signal.Notify(ch, signals...) 27 | defer signal.Stop(ch) 28 | 29 | var gotSignal os.Signal 30 | select { 31 | case <-ctx.Done(): 32 | return 33 | case gotSignal = <-ch: 34 | logrus.Infof("Got %s signal", gotSignal.String()) 35 | } 36 | 37 | // Make sure we finally do SIGTERM after given time. 38 | go func() { 39 | ch := make(chan os.Signal, len(signals)) 40 | signal.Notify(ch, signals...) 41 | defer signal.Stop(ch) 42 | 43 | select { 44 | case <-ctx.Done(): 45 | return 46 | case <-ch: 47 | return 48 | case <-time.After(*signalShutdownTimeout): 49 | } 50 | 51 | logrus.Infof("Graceful Shutdown after %s timed out, re-sending SIGTERM.", gotSignal.String()) 52 | p, err := os.FindProcess(os.Getpid()) 53 | if err != nil { 54 | logrus.Errorf("Can't find process %v", err) 55 | } 56 | p.Signal(syscall.SIGTERM) 57 | }() 58 | } 59 | -------------------------------------------------------------------------------- /cmd/kedge/tls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | 9 | "github.com/improbable-eng/kedge/pkg/sharedflags" 10 | "github.com/mwitkow/go-conntrack/connhelpers" 11 | ) 12 | 13 | var ( 14 | flagTLSServerCert = sharedflags.Set.String( 15 | "server_tls_cert_file", 16 | "../misc/localhost.crt", 17 | "Path to the PEM certificate for server use.") 18 | flagTLSServerKey = sharedflags.Set.String( 19 | "server_tls_key_file", 20 | "../misc/localhost.key", 21 | "Path to the PEM key for the certificate for the server use.") 22 | flagTLSServerClientCAFiles = sharedflags.Set.StringSlice( 23 | "server_tls_client_ca_files", []string{}, 24 | "Paths (comma separated) to PEM certificate chains used for client-side verification. If empty, client-side verification is disabled.", 25 | ) 26 | flagTLSServerClientCertRequired = sharedflags.Set.Bool( 27 | "server_tls_client_cert_required", true, 28 | "Controls whether a client certificate is required. Only used if server_tls_client_ca_files is not empty. "+ 29 | "If true, connections that are not certified by client CA will be rejected.") 30 | ) 31 | 32 | func buildTLSConfigFromFlags() (*tls.Config, error) { 33 | tlsConfig, err := connhelpers.TlsConfigForServerCerts(*flagTLSServerCert, *flagTLSServerKey) 34 | if err != nil { 35 | return nil, fmt.Errorf("failed reading TLS server keys. Err: %v", err) 36 | } 37 | tlsConfig.MinVersion = tls.VersionTLS12 38 | tlsConfig.ClientAuth = tls.NoClientCert 39 | 40 | err = addClientCertIfNeeded(tlsConfig) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return tlsConfig, nil 45 | } 46 | 47 | func addClientCertIfNeeded(tlsConfig *tls.Config) error { 48 | if len(*flagTLSServerClientCAFiles) > 0 { 49 | if *flagTLSServerClientCertRequired { 50 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 51 | } else { 52 | tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven 53 | } 54 | tlsConfig.ClientCAs = x509.NewCertPool() 55 | for _, path := range *flagTLSServerClientCAFiles { 56 | data, err := ioutil.ReadFile(path) 57 | if err != nil { 58 | return fmt.Errorf("failed reading client CA file %v: %v", path, err) 59 | } 60 | if ok := tlsConfig.ClientCAs.AppendCertsFromPEM(data); !ok { 61 | return fmt.Errorf("failed processing client CA file %v", path) 62 | } 63 | } 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/kedge/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // To initialize build version build or run with: 9 | // go run -ldflags="-X main.BuildVersion= server/main.go 10 | // Main will then pass the variable here. 11 | var BuildVersion = "unknown" 12 | 13 | func versionEndpoint(resp http.ResponseWriter, req *http.Request) { 14 | resp.Header().Set("content-type", "text/plain") 15 | jsonVer := map[string]string{ 16 | "build_version": BuildVersion, 17 | } 18 | body, err := json.Marshal(jsonVer) 19 | if err != nil { 20 | resp.Header().Set("x-kedge-error", err.Error()) 21 | resp.WriteHeader(http.StatusInternalServerError) 22 | return 23 | } 24 | resp.Write(body) 25 | resp.WriteHeader(http.StatusOK) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/winch/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // To initialize build version build or run with: 9 | // go run -ldflags="-X main.BuildVersion= winch/main.go 10 | // Main will then pass the variable here. 11 | var BuildVersion = "unknown" 12 | 13 | func handleVersion(resp http.ResponseWriter, _ *http.Request) { 14 | if BuildVersion == "unknown" { 15 | // Current way of deploying does not allow to inject release version so we need to do it here manually to track version. 16 | BuildVersion = "v1.0-beta.3" 17 | } 18 | 19 | version := map[string]string{ 20 | "build_version": BuildVersion, 21 | } 22 | res, err := json.Marshal(version) 23 | if err != nil { 24 | resp.Header().Set("Content-Type", "application/text") 25 | resp.Write([]byte(err.Error())) 26 | resp.WriteHeader(http.StatusInternalServerError) 27 | return 28 | } 29 | resp.Header().Set("Content-Type", "application/json") 30 | resp.Write(res) 31 | resp.WriteHeader(http.StatusOK) 32 | } 33 | -------------------------------------------------------------------------------- /docs/k8s_resolver.md: -------------------------------------------------------------------------------- 1 | # k8sresolver 2 | 3 | Kubernetes resolver is based on [endpoint API](https://kubernetes.io/docs/api-reference/v1.7/#endpoints-v1-core) 4 | 5 | Inspired by https://github.com/sercand/kuberesolver but more suitable for our needs. 6 | 7 | Features: 8 | * [x] K8s resolver that watches [endpoint API](https://kubernetes.io/docs/api-reference/v1.7/#endpoints-v1-core) 9 | * [x] Different types of auth for kube-apiserver access. (You can run it easily from your local machine as well!) 10 | * [x] URL in common kube-DNS format: `.(|.):` 11 | 12 | Still todo: 13 | * [ ] Metrics 14 | * [ ] Fallback to SRV (?) 15 | 16 | ## Usage 17 | 18 | ```go 19 | resolver, err := k8sresolver.NewFromFlags(nil) 20 | if err != nil { 21 | // handle err. 22 | } 23 | 24 | watcher, err := resolver.Resolve(target) 25 | if err != nil { 26 | // handle err. 27 | } 28 | 29 | // Wait for next updates. 30 | updates, err := watcher.Next() 31 | if err != nil { 32 | // handle err. 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/kedge_native_dialer_certs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/improbable-eng/kedge/77a38cc343a2ae84a0a17e8e1a918c9cbce6c735/docs/kedge_native_dialer_certs.png -------------------------------------------------------------------------------- /docs/kedge_winch_oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/improbable-eng/kedge/77a38cc343a2ae84a0a17e8e1a918c9cbce6c735/docs/kedge_winch_oidc.png -------------------------------------------------------------------------------- /docs/winch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/improbable-eng/kedge/77a38cc343a2ae84a0a17e8e1a918c9cbce6c735/docs/winch.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/improbable-eng/kedge 2 | 3 | go 1.13 4 | 5 | require ( 6 | cloud.google.com/go v0.11.1-0.20170804102842-fb68edbdf1b1 // indirect 7 | github.com/Bplotka/go-httpt v0.0.0-20170916130655-531231517216 // indirect 8 | github.com/Bplotka/go-jwt v0.0.0-20170522131130-86d629112187 // indirect 9 | github.com/Bplotka/oidc v0.4.0 10 | github.com/PuerkitoBio/purell v1.0.0 // indirect 11 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2 // indirect 12 | github.com/emicklei/go-restful v1.1.4-0.20170410110728-ff4f55a20633 // indirect 13 | github.com/fortytw2/leaktest v1.1.1-0.20170715211739-3b724c3d7b87 14 | github.com/ghodss/yaml v1.0.0 // indirect 15 | github.com/go-chi/chi v3.1.5+incompatible 16 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1 // indirect 17 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9 // indirect 18 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501 // indirect 19 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87 // indirect 20 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 21 | github.com/golang/protobuf v1.3.2 22 | github.com/google/uuid v0.0.0-20170814143639-7e072fc3a7be 23 | github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20170611114647-f63a7dfb64c1 24 | github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20180418170936-39de4380c2e0 25 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c // indirect 26 | github.com/imdario/mergo v0.0.0-20141206190957-6633656539c1 // indirect 27 | github.com/improbable-eng/go-flagz v0.0.0-20191016172224-7a0dad98abc5 28 | github.com/improbable-eng/go-httpwares v0.0.0-20171110121638-d1341ef6621b 29 | github.com/improbable-eng/go-srvlb v0.0.0-20170728111542-3c3372fdcde8 30 | github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c // indirect 31 | github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d 32 | github.com/juju/ratelimit v0.0.0-20170523012141-5b9ff8664717 // indirect 33 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a // indirect 34 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 35 | github.com/mwitkow/go-proto-validators v0.0.0-20170220212302-a55ca57f374a 36 | github.com/mwitkow/grpc-proxy v0.0.0-20171120185045-67591eb23c48 37 | github.com/oklog/run v1.0.0 38 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 39 | github.com/pkg/errors v0.8.1 40 | github.com/pressly/chi v3.1.5+incompatible 41 | github.com/prometheus/client_golang v1.2.0 42 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 43 | github.com/prometheus/common v0.7.0 44 | github.com/rs/cors v1.5.0 45 | github.com/sirupsen/logrus v1.4.2 46 | github.com/spf13/pflag v1.0.5 47 | github.com/stretchr/testify v1.3.0 48 | github.com/ugorji/go/codec/codecgen v1.1.7 // indirect 49 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 50 | golang.org/x/oauth2 v0.0.0-20170802155448-96fca6c793ec 51 | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 // indirect 52 | golang.org/x/tools v0.0.0-20191030232956-1e24073be82c // indirect 53 | google.golang.org/appengine v1.0.1-0.20170801183137-c5a90ac045b7 // indirect 54 | google.golang.org/genproto v0.0.0-20170711235230-b0a3dcfcd1a9 // indirect 55 | google.golang.org/grpc v1.2.1-0.20170804204618-963eb485d856 56 | gopkg.in/inf.v0 v0.9.0 // indirect 57 | gopkg.in/square/go-jose.v2 v2.1.2 // indirect 58 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect 59 | gopkg.in/yaml.v2 v2.2.2 60 | k8s.io/api v0.0.0-20180828232432-12444147eb11 61 | k8s.io/apimachinery v0.0.0-20190116203031-d49e237a2683 62 | k8s.io/client-go v7.0.0+incompatible 63 | k8s.io/klog v1.0.0 // indirect 64 | k8s.io/utils v0.0.0-20191010214722-8d271d903fe4 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /misc/backendpool.json: -------------------------------------------------------------------------------- 1 | { 2 | "grpc": { 3 | "backends": [ 4 | { 5 | "name": "test_endpoint", 6 | "balancer": "ROUND_ROBIN", 7 | "host": { 8 | "dns_name": "localhost", 9 | "port": 18271 10 | } 11 | } 12 | ] 13 | }, 14 | "http": { 15 | "backends": [ 16 | { 17 | "name": "test_endpoint", 18 | "balancer": "ROUND_ROBIN", 19 | "host": { 20 | "dns_name": "localhost", 21 | "port": 18270 22 | } 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /misc/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE+jCCAuICCQCaCXsVPyCpqjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJV 3 | SzELMAkGA1UECgwCQ0ExIzAhBgNVBAMMGmV4YW1wbGUuY29tIFNlbGYtU2lnbmVk 4 | IENBMB4XDTIwMDIyNzA5NTI0NVoXDTI1MTAwNjA5NTI0NVowPzELMAkGA1UEBhMC 5 | VUsxCzAJBgNVBAoMAkNBMSMwIQYDVQQDDBpleGFtcGxlLmNvbSBTZWxmLVNpZ25l 6 | ZCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALH7lUyUqu6Z8yok 7 | SSAdyvx9VBlakxrVKS65301Q2Jz0w4m84cibXh9nxQcuzsMmy/Dkc4E2zihmx3QM 8 | bjeaexqHgFzna07ahNFrqR8Zjfxv0EzXn9frY4k+EhDmgmkOLB//f4NJyICmzUKM 9 | 0SCMOZZbDBC43SAhdGjwmFv2nR9aJekkHygijV0xEOgl/SoZMLJ7Abb+/bYlsohI 10 | ZEpfTcmq5kwQ0e8sJi2ScfxEAs2idWAStkTGquvE0kq3l+DXTX6IM9owK7Tb5TZg 11 | CImgq6cJg91rCroNsTnHxKDdZALU9hteec+npqftqykHITwtCugPgrmaife6wg8S 12 | oVtpxSvyAbODnTxrDfyFg2eZOuaccgzrZNVxoaBac26xf06cQUHt7rrMhRBgSoo6 13 | GwWNYF60Pou0sS2V2oT9oQJdIucpIw3OFOdj3bbEoG2c452soo+nrOLG9hQK0J5Q 14 | x3ZYzpMqXei7N9x6ePk/wy5DjgHxwLXFIGwfEmSBwYoeCDhrG7reJg2VYMU5nxY0 15 | d/OWrnWN4zlnkpmGeSuCASHCH/qjh1J3hfF6t+aoboZ6odJtoaB88N4H3UoiA2br 16 | R3Ek0+yifrXYaSTJXR+F34SCb1pKNOjrTSdbCTPuPG9DUaSR4oa4UKHnb5A9TJ2p 17 | z4JYkgV2zE9U9MQfINz6vTtdeu8lAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAK5B 18 | b/0olylOBYmzCNs36S6L3MNoA+FI/SsidhHFO4mICXoK41zz5HD4ClJ3xo1fSV3z 19 | Tvx3lxFPgfrhnSrWOXQBIOkLKpc6DCUZNAeWR/N5D97ctyrasdmo+BkxyWROOXgS 20 | JaoWJj1bapqA3Eckr6C5in9LBs1iu5M9BkqLW5+ozaVc6Ha6CbEZMdamzo2sbeFs 21 | EugJz3dt8EJA9VXvbnGMQCSxjCrMdOXdb/QhVSlyAMvSJkrt7zwSMZjkgZQ37sza 22 | pKN3NosUiQnYSdduBDplOTVoOgByiovzGHBtHwfZdn/guuuqmokj8p2ragCdMccG 23 | NOJExGSBb110EO85cE0LystPXdQ7epFgsVaSioZ9/oOZdF/Rb93+m/NAZdBypV8U 24 | kAccswh23E3IJTnTOVNrwYkCQM/CL1/2au2Fbbc2X2ysrnljpoX0e9e6Vix56aet 25 | NFo/FiRWJQ2aoY7rcrTIIEG+WTLpI3p4WN3YVe+gGECH073OkVBy8VQbgmDqWw85 26 | 4YpH6zYpZrcVPm0gMDV9bXER51+1BP51zU3pQcwJoX9PJeU7iw/BwN2AOkM1pGIR 27 | vzf6mFLs2fWyiracN1ZrSyZIUdw/Pmv6o+uaXgDUMWDHXJ5d2xssxUev9lJfxn3i 28 | RHbDYPGDL5amsmUoGleXdm2o5mFE0QKYZEpSifEn 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /misc/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEAsfuVTJSq7pnzKiRJIB3K/H1UGVqTGtUpLrnfTVDYnPTDibzh 3 | yJteH2fFBy7OwybL8ORzgTbOKGbHdAxuN5p7GoeAXOdrTtqE0WupHxmN/G/QTNef 4 | 1+tjiT4SEOaCaQ4sH/9/g0nIgKbNQozRIIw5llsMELjdICF0aPCYW/adH1ol6SQf 5 | KCKNXTEQ6CX9KhkwsnsBtv79tiWyiEhkSl9NyarmTBDR7ywmLZJx/EQCzaJ1YBK2 6 | RMaq68TSSreX4NdNfogz2jArtNvlNmAIiaCrpwmD3WsKug2xOcfEoN1kAtT2G155 7 | z6emp+2rKQchPC0K6A+CuZqJ97rCDxKhW2nFK/IBs4OdPGsN/IWDZ5k65pxyDOtk 8 | 1XGhoFpzbrF/TpxBQe3uusyFEGBKijobBY1gXrQ+i7SxLZXahP2hAl0i5ykjDc4U 9 | 52PdtsSgbZzjnayij6es4sb2FArQnlDHdljOkypd6Ls33Hp4+T/DLkOOAfHAtcUg 10 | bB8SZIHBih4IOGsbut4mDZVgxTmfFjR385audY3jOWeSmYZ5K4IBIcIf+qOHUneF 11 | 8Xq35qhuhnqh0m2hoHzw3gfdSiIDZutHcSTT7KJ+tdhpJMldH4XfhIJvWko06OtN 12 | J1sJM+48b0NRpJHihrhQoedvkD1MnanPgliSBXbMT1T0xB8g3Pq9O1167yUCAwEA 13 | AQKCAgA4zjhPkd+gechPefdQ5dFklsehs/Phi4kyXaa0sYoBRmmma3+QnG4FDgSn 14 | jzv0s0xCHVf0NL7FzE/6bQE8g/SoefjxLfdk2n+rq3X19B0KJdHQxL1Cl+FT61iu 15 | xjN3Pku9Brn2+DSjQxmeFP2mKrsyjuqh567D04mo+KlYKLTrTcVtzNaY47ZEuSVR 16 | Qtazegi93l6kvmvRl+SMdLZ1ukdEh2QrgO7QLEIfJ29z+Wz/nsthl3dPKi1hRJdt 17 | u30hCPa13NjX2aoJdmmI2ku/SWWf0XyhzclFqpsW+vh58085TCkkgRnVugeJ72RC 18 | mwDziNjDSjgJ7xX72EKZtvODDpYcMOo4ssPoTRmYByauEHqq1g2zw3gGbNYcS2Zb 19 | 8mJHI7kn4o9Brx7VILsWeePHY1a+bZiIkwuNXqTUtB38OzD8Sirb4vKRN0hZlCf5 20 | dyD5/6TFne+FR4jPt+uVyhbbxG3wIjhYBaNhQM7qzQYF+Ml0t3Y8QfkINU4D6FgJ 21 | +879uMamV4UqD9D1mUpeEgNtEloS6wLNdAxz4h2ZvtfJA2TPLsnDfl7522Kv5KJf 22 | EQTKvsT8VtInWJCDaxQ7lIwU93eTst5b2p+b5prd//8qkvziiF8QsxueQFm8p351 23 | lKbrhw3k/H3Rohmn2I0bPik7ObJL38IyNXDf3LToOdT44U5FgQKCAQEA5ZNSSKQY 24 | GkpwzjFnOrKwCI1RyFluPbCRXGpFX+QMSwiPNkiajcWevHRWWeTyipLenKcWKVMx 25 | /uoule6XqWw70OTfdHMG+eUbiK0m4zla6CpkXZ04X6FSb9TobdimAsMaYyBEOJ/G 26 | V3LbkbDU0SePHBWOIjLu48tlVXP9xiChLGDtovHpeG9B1vtoQ7+ymxeGFWH0XPC0 27 | /9RZur0iQMCk4X0Sr/K4BtR6b6YV8iX3w0bvJy10aZVV3FKnZI437CYvfb4X1eky 28 | IQOmuqixy0TT1Tddpo2hmc9Ua2luCDR0hJ9fPEZrX1oSLJ6ZnjIEg+yiH5IakTBf 29 | dFsklb0vqt8V4QKCAQEAxngHXFQytVHL3O8SexauiLmdo6DjxYdGzrZfo09u15m1 30 | QRoJgC7A0YWO7uHqojAJxmltQGo0TVHXFb/ZzdpjfRFZeE9ACbSlUBORJf96dXhv 31 | Zu5TCsR+bf1RPPdVdF0yKH9Aix6hJLLSt74VwZFs3POGzZz998cAEwfFQYIDnFPu 32 | xp9YpHbDxVV4AuNrUdXBD1opMA9YdSYJ9iVqOdaoHjxmhrDzJfgdf2K55ne4Zuvn 33 | w3dh/zbn2tM3h+cQX6cadZbcllPCXEmzaEX0oKD3Z/xFMvLu+Pli9BTGhRE3ReYH 34 | QUJTnLDN2QF8vdsiRc/yoeuSaJlVT/zFfkiA0fA5xQKCAQAZQMhXVz2TfsbD16lc 35 | SpWUiz8Iw9WdkZObz9DLyIEVq95dAUWG/MsGPeHVx5pqZdd48eQ9LXqdaNMxCOrD 36 | 5zT5OIPgAlstF+ecdDeH3SJD0Y3ywj2WP1+bD5d1pdQ1D5MSflUiyWh+7BJ9By+R 37 | xkE2vKRC3MlsY05FRRoQNjykbhEV0Hh3F/3tEQCaGzchWkgUiA7iPlQapBgus2lW 38 | KBq69xAcLJ3TmoHRUDqvxT9oyp+59oUMrDGip7DkHcTHBDhLI6Lpk9pAyW8Ir0/H 39 | ZU3L8Wgqzv+MtMK7ggBphKOghwtpzPE4rua648NQJH5cqKX4xRCJEgIeAXyXwBTD 40 | PgDhAoIBABn67mO8teKccYZbWVz5jCFjwun/jrF10uQOZZiYNldMzLhX8zRvalhD 41 | FoHY2wEeLrllZaLQBEa21uQG+DkFRI7DraWdIjHjDtzbot2JgvtLp7GeswouzKoT 42 | RgYoDmysInvApK9RdCC8s+7PmEN9iPWnf1b9HRXJXA4hr0WzAkv9hL1FcLIexePY 43 | G1ytbogI5jvfPpMG/zpen2E0ZqJpohpLJ9Sr5zhqMVpSjylHNMFsUit5Pj/NbS5t 44 | BdDpg3AyDLml5PxcvI2eLDkTJ0G7sIdRM7HGsVewNa7j5VTX+xdQVFTLnOxfDKNC 45 | mZS00di3rvhZQRImOE+/NA8i6JIS000CggEAP/23Xjv6xPiTTJ8P+nkBeu+pkG9Q 46 | tICFxR7hvxAqpJuFAUbMmYHkxtWESlbUd4A2gVcJ8nIPsWqAclfxRnhDLrXF1Bbm 47 | PxVr0ZZO3s8o5kvZYf358Ecv82DUfTW69ZN+9InvtN8oQ0qBVEQ58qcHss1dttGF 48 | Ox45twDgtMvw5h/5g+U6HSy/Ll6KNqAh5dWlh4FyVLDW/ewdV0mzvLfdJvv+VWrg 49 | A4ZaLRI9LobF1d+dp4UX8RvYh0lbvDR7IuJzs5/v3pbBe2RAK+n8KVq0tg5YNzjv 50 | /AexMHCvrPaWve+Pm+HNqINBmbjkZRzYOQDSI9hoN/lNUy8J5x9ImzOw1Q== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /misc/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE/TCCAuUCAQEwDQYJKoZIhvcNAQEFBQAwPzELMAkGA1UEBhMCVUsxCzAJBgNV 3 | BAoMAkNBMSMwIQYDVQQDDBpleGFtcGxlLmNvbSBTZWxmLVNpZ25lZCBDQTAeFw0y 4 | MDAyMjcwOTUzMDBaFw0yNTEwMDYwOTUzMDBaMEoxCzAJBgNVBAYTAlVLMQ8wDQYD 5 | VQQKDAZhZG1pbnMxDDAKBgNVBAoMA2VuZzEcMBoGA1UEAwwTc29tZW9uZUBleGFt 6 | cGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANKBHWz8Hv1b 7 | OButXTjS9X8vmECM7v+PjNB3ooYADj4bp7byG6AJ2u5I5n8Mg6c0eMI1ZPOc7IFk 8 | +bmlJT0+iRIaAtvigwQXwfA2pqtRqIQom9GxF/BnwcxyT46g6Mgvd+yCTgs/Q1pK 9 | Dgfzl5N/TmlfbMP+PyhtZIJMY71+SXqADPekQCjzCIhw7XPy4vDcrbfu/Tht4vl4 10 | NRCA0h5avvJm51t9/wM36SnW4jhlvArmNSJpUbxphR70Knw79MaR+YFGDil2fhv2 11 | 869WB+LsHY8Rbk5ouKgKyYamWBwb05ObpKyxNpYSNNnqeO2BeowgROB8uilmMajc 12 | tPmWOxMNyPsV79PgrsVEF6a5xmiAm97qfoGW3H3H1Fcfx0urb5krIcsuCcwmyQXN 13 | aQhGwMBlsc6QXLSdr6o7s4ORLKeM/Sh3zyuAOV3ntGugQmYL3wYzrX4/EYQAlmD1 14 | v2xRXyom6u0VQvaH2m4hbiiB5pIpWY6U+uwMjoftwOQLP5Dz0Fx314Q7Prvn42aN 15 | UOAPrex1JnuMraKWHXGyNRR+i2JbsJ6aEPilBBuF6615lvAjF7ZMhHlGPtmpHaAQ 16 | BTvj9iROL0qUm0AyxWij/OfbzYVY64YogI23PuJZ/REQzA9ojFl03U5WKwT2dDqr 17 | vJGWaNr7WlKrO3NO2K+O0yE7mpo2XQhrAgMBAAEwDQYJKoZIhvcNAQEFBQADggIB 18 | ABcRped0AIZrMW7f5xMW4f3qmVg96mFpn82JOUgTRe2/P7F0L8Q45Tnf+R55b2qR 19 | vbmLXdhYKgoa8T/cczuRlwFr1MPi6fKdEtgrowAxl1bM7uNkX8PWkYDpmcpTi0AB 20 | DVNSNzrZ5So2aFUGu76l0FC6/GsB+vOcyV8knBq2x/9muPcZXIgtNeYWin2DAIgt 21 | JPia2k9rgDDvW+t9v6Dq6Fimdi5e5gDZwWLN6zBG51cA9+951SVHn8mxkMMEhs2t 22 | WE0FTWPrjFI6U5H+VQzP/mT5vYmxuMPJC1GIK7kXbcBiDsTqFuIK9zWDj4yCTe2F 23 | PpLwuFTikarhOiAeJGgTz2Kdr+En3zbf21rL5R1zOIdTw36mQw+gBG/lJI+kF+UV 24 | Nt5tys4VX3tIPb3uXv0TSxNIpxND4Y5jW2D74g8PURLWKOFm4gEnaWS3RE/2ODc+ 25 | u90qSxu9wOo/9XqEXNJtPNXTy9Dfz+vHkg6u7Xqhk9KRixvaEjrLIhF/YkByR1F0 26 | ApRPC4/Ta3m+5rdfuEeIwwiR8cIgCvNQj+2Cvioy8YN22S+2TcABC+qeJ/u1Y2wV 27 | 1xr1ooknb3dGA6PI8rCVsAF7r2toBaP9v/7ljS0WDNUCPCHqK3FOC0C3E+cVHh6i 28 | MeHOS2qzLanEJTE7hj3yib4IxuHv22fNRz/VGtofChVN 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /misc/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEjzCCAncCAQAwSjELMAkGA1UEBhMCVUsxDzANBgNVBAoMBmFkbWluczEMMAoG 3 | A1UECgwDZW5nMRwwGgYDVQQDDBNzb21lb25lQGV4YW1wbGUuY29tMIICIjANBgkq 4 | hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oEdbPwe/Vs4G61dONL1fy+YQIzu/4+M 5 | 0HeihgAOPhuntvIboAna7kjmfwyDpzR4wjVk85zsgWT5uaUlPT6JEhoC2+KDBBfB 6 | 8Damq1GohCib0bEX8GfBzHJPjqDoyC937IJOCz9DWkoOB/OXk39OaV9sw/4/KG1k 7 | gkxjvX5JeoAM96RAKPMIiHDtc/Li8Nytt+79OG3i+Xg1EIDSHlq+8mbnW33/Azfp 8 | KdbiOGW8CuY1ImlRvGmFHvQqfDv0xpH5gUYOKXZ+G/bzr1YH4uwdjxFuTmi4qArJ 9 | hqZYHBvTk5ukrLE2lhI02ep47YF6jCBE4Hy6KWYxqNy0+ZY7Ew3I+xXv0+CuxUQX 10 | prnGaICb3up+gZbcfcfUVx/HS6tvmSshyy4JzCbJBc1pCEbAwGWxzpBctJ2vqjuz 11 | g5Esp4z9KHfPK4A5Xee0a6BCZgvfBjOtfj8RhACWYPW/bFFfKibq7RVC9ofabiFu 12 | KIHmkilZjpT67AyOh+3A5As/kPPQXHfXhDs+u+fjZo1Q4A+t7HUme4ytopYdcbI1 13 | FH6LYluwnpoQ+KUEG4XrrXmW8CMXtkyEeUY+2akdoBAFO+P2JE4vSpSbQDLFaKP8 14 | 59vNhVjrhiiAjbc+4ln9ERDMD2iMWXTdTlYrBPZ0Oqu8kZZo2vtaUqs7c07Yr47T 15 | ITuamjZdCGsCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQC3wjqr/icxHH13WDi9 16 | Lx5P5NTp+s+jHqPxwOigbJoNjDZ+6ebMLqycQScgBLiOZlb7p5DfhdFzgUOjBz/s 17 | 7Nz7sXBUnc58eWgoi+uukv6tJoCgg6P94athv/LuewNjNtphAM8gjSn3iS/wWTol 18 | uXGbA87wLEUtldGh91gtzh8BtNb2aBBglst0O678rQLoR+V3WK1ErG1yIDEU3Qod 19 | J3omV05Q6nsjBZ7SIiUwj1qDNtJ+u2kL3a5l8EfVLOGWLnyqQM52LRKRQm/0tBTj 20 | vE0V07etduva4U3y6Ndh3LbxG5OlQ4NfB1WHJS8nP9sWyAnG0BjaKjbfoyL0g3AP 21 | Xza0B1e6fwLqJfxKStB0Nd7ojL0loCF253utm2EQNe62ZY+3Ej767il97U5XBxQD 22 | 03e+CsdtWMeLf+vz2l/9U5+7uo60It3EmWLZP84emk8aG5o9YfSDKdkdHFU8lCeD 23 | gsZuil6NQmmMYpjvP4U8r59eZc5UdDuH5ZD83FSX/cMEQTaJM/ePKSrm+INVRSoV 24 | G/iOEkwEtKKI9td2sCz/KfKRh/uK+U5l5E6dWpxnYxTU2EZH0dqzD0zpNPgA6CUW 25 | y48NXH6CNeurgBXThfQPidGtFpfIhLNpfRaIHNbCxpoIaLqydC3n+yfZlWjwhZYs 26 | yCRw3tNJqe1A8XB1G5QhZM6N9A== 27 | -----END CERTIFICATE REQUEST----- 28 | -------------------------------------------------------------------------------- /misc/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEA0oEdbPwe/Vs4G61dONL1fy+YQIzu/4+M0HeihgAOPhuntvIb 3 | oAna7kjmfwyDpzR4wjVk85zsgWT5uaUlPT6JEhoC2+KDBBfB8Damq1GohCib0bEX 4 | 8GfBzHJPjqDoyC937IJOCz9DWkoOB/OXk39OaV9sw/4/KG1kgkxjvX5JeoAM96RA 5 | KPMIiHDtc/Li8Nytt+79OG3i+Xg1EIDSHlq+8mbnW33/AzfpKdbiOGW8CuY1ImlR 6 | vGmFHvQqfDv0xpH5gUYOKXZ+G/bzr1YH4uwdjxFuTmi4qArJhqZYHBvTk5ukrLE2 7 | lhI02ep47YF6jCBE4Hy6KWYxqNy0+ZY7Ew3I+xXv0+CuxUQXprnGaICb3up+gZbc 8 | fcfUVx/HS6tvmSshyy4JzCbJBc1pCEbAwGWxzpBctJ2vqjuzg5Esp4z9KHfPK4A5 9 | Xee0a6BCZgvfBjOtfj8RhACWYPW/bFFfKibq7RVC9ofabiFuKIHmkilZjpT67AyO 10 | h+3A5As/kPPQXHfXhDs+u+fjZo1Q4A+t7HUme4ytopYdcbI1FH6LYluwnpoQ+KUE 11 | G4XrrXmW8CMXtkyEeUY+2akdoBAFO+P2JE4vSpSbQDLFaKP859vNhVjrhiiAjbc+ 12 | 4ln9ERDMD2iMWXTdTlYrBPZ0Oqu8kZZo2vtaUqs7c07Yr47TITuamjZdCGsCAwEA 13 | AQKCAgBhxEHMalRiS0mF4UDYsXH/vjUyVcJyw1MdR1MedUwoIGQGne+iprEd1pHn 14 | FALZV492c6INWyak+ibZyA/BoBe9ZoNJaKx87CPQ0zEZhHWF2+5yt5NsvmPN9pFo 15 | puc2goVqPSLOKnW3q4lUvBf8EzZxzjYP2TewKQ5zTdNNISgzUuGy6oA6BJZD+F7w 16 | aTBpubSdJsxItxCh2OicX5g3LOJeZtixL5WPAxqxqfGrS+TIEx8+ejfSEBgZxkEt 17 | LhODXizJk6XkL/pmNo37MzO6evtHhUr787s/oLL0bLGjbHEZr0IOSsgVbyNIypOb 18 | i3VFO3+B26wCqkoKKWeMO+/364EZ9MiGLwocO+annAQ4p3yxzTx8p7TkJOetGjYF 19 | XwsNvSz0O3EZZi4KEUwgureheS7rckDOcwLqzr2OSLG4ZoMgnWb5oaGhY29PDbBG 20 | 8evdk0xRAdyxRJpCZ8TJfXWmtOV+tX+HY+HN1wUN/ldyJDSPHsIXF1PV6HpgQoeT 21 | 8VT+m1n6OMo1rwviuScYdxst7MPMwxbgy+XrbZh5vFCc6lh26namFEmu8mZO/rEt 22 | OkC8Qxh6gI697ov2W0v5lnmeQw1aCzx/kG5VXw6uvHE4BLp0vvp5wAhmzjiHbou/ 23 | IgvnxgRZ/+NA9gQ+Ie72FuYqxWGhSkhbD48w7lYqhmrEzLJywQKCAQEA8cNQLzY0 24 | lDL0zsUNwqy4AnBCfuEuTv5KaEGpvofv+oEkfH5aTL2NioySmWin8XE9nNgVcWjS 25 | rl6LPQcZejtPbQHMwOpP2oUgbNXO4k9jweYf13+boSfCASR8zeniKkk8DCM/7xp1 26 | waQ4ZPYV5r4nTqOm/ZB4m21rP2OOPbIQXkSlI1Zb2lO6vbtpTk7AKI3/NiMNG0j7 27 | i7WIC4P4M+NcMgp6TZB6rLTJ9xBClEqCiNo+8rb9hDa80NTdmRIfAxsMK5sfG+wY 28 | HeQEAc3Ys7SkmlH7nXX+YOvjCTXd3rVfOu5tAyyMuzCca/ccJTpg1eDOJkzwgpfU 29 | 5hxDnhVCxO530QKCAQEA3uaQcoHW9ygigzzLZShzV49mTU7t5Qfj9dn6F7G1iOHF 30 | QQ0LSGmh0JgMUdFOfk7TQASobROsfjTq0bubSu1A6GxjTPttwSXJJhLpd1oL/KeO 31 | uuMk3ggSNpnh0a7xRQLR3K47FZ6ZesMmiMi3X1Tg2LDgfX/GC9w0RsW8SmwC26aB 32 | V9L925bjqjnKzv/1Aj+w8miSmMyPsFkIszLNfiH+TouLL155TSsaK4JbCdTNDmUQ 33 | HQ23zL5XXnTfhm1bzGccrc+cS7w3OuPFVCMP4+K/GADpg/3i/fe3Mk1lAz7drnw+ 34 | j7lyFMRzFbFOkdzdMVSFv3jWEBuB2QPmI4G2/cbHewKCAQEA7B16xIFGHK8flImz 35 | hLu3AvqslspJtfB4rxXiOCj/YUKbZdLpUiWWhQgBbT5fN1kHeZU4bAiaKp4/kpzK 36 | byxZx/aICKlOz/ZQ0rqGUVSD8y1TT95bGqt/uCnwkhKoRfA8awZKPLU06KgAQ+pr 37 | PW3dado/D0n34KSep8wNcYfkoIyeU8LV9obrzL2qfUZAOdtks3TMKie/NJVYhxae 38 | Y97bfivpgrNmfCIdVeRVggq7LfkonVfGhUgIZFRhEEw4aYoS+suHOHf+ncLBfxmR 39 | WQFF+Um0WfaROAtpMuefBxFQLngJk54A3Bj/zBoNLPa0+G2UKKfgBUtQLGTSTSQq 40 | j7bu4QKCAQAloVcRfcoEv8nTzheoZEPUrGjg9EpFOCfMAZivF1lGWpcqfEFDYFHp 41 | HUI3LdbKjBQt4ptsjr24MMbT0ZBN/e/PTT0WafwFd5OV0euIMrODW+ZEtsQql8I7 42 | ZY8yDw68T9WOI/vlknjmuLtwrDII7hNngbTkewW8StioeglnpwR0gI+lfAFPaRHW 43 | UxtiBuQeFRKSWgUltqMralyFpDX87VA5gmVlAdYIqRYp8j/cUQitPXKS9RXqgtfN 44 | In35+8xbnazByyLfxk6sqndN1P2Bw23vZ3gEyH/h0EglPdunyJ00L9V//ha8Ws3z 45 | A5P4HAlboqFRydyZq4soT/gyVlvGTCCxAoIBADYelR8wbe2ypWNYSGAd7jk/BTd9 46 | AeKent6PD7BJYO1pilkHWjXK+clWaKxWqZ4nCLKk5o/Ias7g1f6kOWBqFfi2Eedz 47 | RhuIiAtCuEtWS14os3w6lLyEjqwn74CcfZHdWzQMQKyq3k+D5FP+P1V71bvEkzSh 48 | uB4DL3he3EYti99iao99xuMgzzsvf1MDV15tS+QuG8DlssCd1kU1dkUCiJ/pyype 49 | fGRAU37iZEw7vYnfLd9W5+XJcJnWL/vrb8OYzqPa9ZCrMtX3bLbCjwBkl6rp5wLe 50 | GRD8+72zXrcXTl8f2OVzh5CVh9LypvPP4fBiTenPIMAd5Tz/j+9UH87Hw9Y= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /misc/client.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/improbable-eng/kedge/77a38cc343a2ae84a0a17e8e1a918c9cbce6c735/misc/client.p12 -------------------------------------------------------------------------------- /misc/director.json: -------------------------------------------------------------------------------- 1 | { 2 | "grpc": { 3 | "routes": [ 4 | { 5 | "backend_name": "test_endpoint", 6 | "service_name_matcher": "*", 7 | "authority_host_matcher": "test_endpoint.localhost.internal.example.com", 8 | "authority_port_matcher": 18271 9 | }, 10 | { 11 | "backend_name": "test_endpoint", 12 | "service_name_matcher": "*", 13 | "authority_host_matcher": "no_auth.localhost.internal.example.com", 14 | "authority_port_matcher": 18271 15 | }, 16 | { 17 | "backend_name": "test_endpoint", 18 | "service_name_matcher": "*", 19 | "authority_host_matcher": "bearer_token_auth.localhost.internal.example.com", 20 | "authority_port_matcher": 18271 21 | }, 22 | { 23 | "backend_name": "test_endpoint", 24 | "service_name_matcher": "*", 25 | "authority_host_matcher": "wrong_bearer_token_auth.localhost.internal.example.com", 26 | "authority_port_matcher": 18271 27 | } 28 | ] 29 | }, 30 | "http": { 31 | "routes": [ 32 | { 33 | "backend_name": "test_endpoint", 34 | "host_matcher": "test_endpoint.localhost.internal.example.com", 35 | "port_matcher": 18270 36 | }, 37 | { 38 | "backend_name": "test_endpoint", 39 | "host_matcher": "no_auth.localhost.internal.example.com", 40 | "port_matcher": 18270 41 | }, 42 | { 43 | "backend_name": "test_endpoint", 44 | "host_matcher": "bearer_token_auth.localhost.internal.example.com", 45 | "port_matcher": 18270 46 | }, 47 | { 48 | "backend_name": "test_endpoint", 49 | "host_matcher": "wrong_bearer_token_auth.localhost.internal.example.com", 50 | "port_matcher": 18270 51 | } 52 | ] 53 | } 54 | } -------------------------------------------------------------------------------- /misc/gen_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Regenerate the self-signed certificate for local host. 3 | 4 | #set -e 5 | set -x 6 | 7 | openssl genrsa -out ca.key 4096 # note, foo is the password to ca.key 8 | openssl req -nodes -new -x509 -days 2048 -key ca.key -out ca.crt \ 9 | -subj "/C=UK/O=CA/CN=example.com Self-Signed CA" 10 | 11 | echo "Generating Client Cert cert (ca.key and ca.crt)" 12 | openssl genrsa -out client.key 4096 13 | openssl req -nodes -new -key client.key -out client.csr \ 14 | -subj "/C=UK/O=admins/O=eng/CN=someone@example.com/EA=someone@example.com" 15 | openssl x509 -req -days 2048 -in client.csr -CA ca.crt -CAkey ca.key -passin pass:foo -set_serial 01 -out client.crt 16 | openssl pkcs12 -export -nodes -out client.p12 -inkey client.key -in client.crt -certfile ca.crt 17 | 18 | echo "Generating a 'localhost' Server Cert" 19 | openssl req -nodes -new -key localhost.key -out localhost.csr \ 20 | -subj "/C=UK/O=admins/O=eng/CN=localhost" 21 | openssl x509 -req -days 2048 -in localhost.csr -CA ca.crt -CAkey ca.key -passin pass:foo -set_serial 10 -out localhost.crt 22 | -------------------------------------------------------------------------------- /misc/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID8zCCAdsCAQowDQYJKoZIhvcNAQEFBQAwPzELMAkGA1UEBhMCVUsxCzAJBgNV 3 | BAoMAkNBMSMwIQYDVQQDDBpleGFtcGxlLmNvbSBTZWxmLVNpZ25lZCBDQTAeFw0y 4 | MDAyMjcwOTUzMTZaFw0yNTEwMDYwOTUzMTZaMEAxCzAJBgNVBAYTAlVLMQ8wDQYD 5 | VQQKDAZhZG1pbnMxDDAKBgNVBAoMA2VuZzESMBAGA1UEAwwJbG9jYWxob3N0MIIB 6 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8YUvlu9h/eEyAiov0+KN92bE 7 | W+jrhWgklMe18fS9X28TcOqIC4EbbZgFFnubc3yBVrezsGVwS7N20YRDwhGq6WPn 8 | DxhWeZUdeIPEiv3/BJ2MwdbJrDPXxP3aVC86KiKTCg9jt9QOAmzLR+U5ABHaRcjD 9 | RMjg1A/xwBlgMGc6HTNYWg7jRNGY16nZAV3J8YyNAzlLaVCdyT8SAKxjrv+bNIut 10 | dKMT+RAtHzae777kwrRFatcR5dImuYJTwC0hY8mqsOzVAOkfolf2Xh/I8xnuZFKJ 11 | MmS1OCtCyV/wK7J+f6JdE42HqNCVrzN38fAbo8DTqiEagHnjbyeKBcldvLo5MwID 12 | AQABMA0GCSqGSIb3DQEBBQUAA4ICAQASDa5psyx+zE10HsFcGSrWsAZsjtpuJ1A5 13 | k4wvfwRznKzKsLjNHTTz9ZFnwIt95shvf17PUCJsSb21xyFMID1TW9koOA1D/VWZ 14 | +uxxC1C5e+6p0Wk9oufEN+dN3CA/pux7FeAQWzp+WtrWHT0MyisB1mydOgROr+cv 15 | IYX1YBxidFvZ1nEa6Q7qPvDcPDnvHlHUmEsKjF/f7WrUq/2NL20+vM2yTlcjoFyn 16 | n3iW1yZTiMI49EPZMCOHur5gy+PdaIQxTVh5B3Q8LamLfdNM0hzh4vSfVhr797YR 17 | +UF44NE0HzP8La3xYMXjBfgOb241izexvGg/v5tv5SGBYPZlUwVKPTqTXJYi2NlQ 18 | MNogMd+BtRUmPowmN7H6x1S7p0QHa5Oj1squyP6iluHhkUmraD11Io/Rz9/j0Pgg 19 | VgPmwMhu0LFO7KAKqERWmuK+ppiA4gUhBkS8yUOcxp/ot8OoGoC0otcY7nTbcLCk 20 | o+fmMfQXCIRqu+Y4CUG9AYAXx5Eegyfs6wUE2amLCnswGx848k7jZEl5C1UG37HO 21 | 4dnyihVJpu9H8L58rtQQed3IzJakduC1n9jVQ6tRQFpP1QMebHKWqrsrS7MmfLoS 22 | oGMx/9n5BK30t/+sK0LQ0KTTXJetiTI8QrcuFRR0LGqqMl4Cyl7wQQfZgNY/wBmy 23 | WT2uaxlX/A== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /misc/localhost.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIChTCCAW0CAQAwQDELMAkGA1UEBhMCVUsxDzANBgNVBAoMBmFkbWluczEMMAoG 3 | A1UECgwDZW5nMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUA 4 | A4IBDwAwggEKAoIBAQDxhS+W72H94TICKi/T4o33ZsRb6OuFaCSUx7Xx9L1fbxNw 5 | 6ogLgRttmAUWe5tzfIFWt7OwZXBLs3bRhEPCEarpY+cPGFZ5lR14g8SK/f8EnYzB 6 | 1smsM9fE/dpULzoqIpMKD2O31A4CbMtH5TkAEdpFyMNEyODUD/HAGWAwZzodM1ha 7 | DuNE0ZjXqdkBXcnxjI0DOUtpUJ3JPxIArGOu/5s0i610oxP5EC0fNp7vvuTCtEVq 8 | 1xHl0ia5glPALSFjyaqw7NUA6R+iV/ZeH8jzGe5kUokyZLU4K0LJX/Arsn5/ol0T 9 | jYeo0JWvM3fx8BujwNOqIRqAeeNvJ4oFyV28ujkzAgMBAAGgADANBgkqhkiG9w0B 10 | AQsFAAOCAQEAYCU7amDiWyPAomsJB1CUUvmStrLecAuLvV97ABF+mfiIrCSmArZJ 11 | NmYaFRr6ZAyi80LuHa+hV1bMG0qDGZxVBsyZuzU69Ml2pIjEIC52nduBBuxJpy25 12 | SiHGntSX6iPKNdJRyTBLNnIx2IT8G6Rkbd7do6fF9p60ajf4qYWuwMdt2WTdyiBq 13 | q4GhWRXAZQYbTflU59FhljcJIAZ12DaA53YcNv8aHm/N95BnsPDWIq5bbLs6+Bnn 14 | 5RYgwqUkaFyB6vkQZ8BdtlRtXgOFtefKmkITKRtBl18+9GbuCd3+Hs4BdSbW1K0h 15 | j24n+mGp2XZqqXGBGTozNqBYSM4vmJg+cA== 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /misc/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDxhS+W72H94TIC 3 | Ki/T4o33ZsRb6OuFaCSUx7Xx9L1fbxNw6ogLgRttmAUWe5tzfIFWt7OwZXBLs3bR 4 | hEPCEarpY+cPGFZ5lR14g8SK/f8EnYzB1smsM9fE/dpULzoqIpMKD2O31A4CbMtH 5 | 5TkAEdpFyMNEyODUD/HAGWAwZzodM1haDuNE0ZjXqdkBXcnxjI0DOUtpUJ3JPxIA 6 | rGOu/5s0i610oxP5EC0fNp7vvuTCtEVq1xHl0ia5glPALSFjyaqw7NUA6R+iV/Ze 7 | H8jzGe5kUokyZLU4K0LJX/Arsn5/ol0TjYeo0JWvM3fx8BujwNOqIRqAeeNvJ4oF 8 | yV28ujkzAgMBAAECggEASxvtQdYuNkL7R1sRRqaVGdRWynJ0FCfgAHjfJ2DCJ9Sm 9 | Sh5VsqYy/nEhW+2S2WZl3q8AbaIOOyyTjfLBE7Bk43ITIEmkUulBogHwdH0q+qd4 10 | Z6vBShFRT8zWQgnx37qi2aURkNCcjrqAuVoa0N+8bqlRuKlz6d8Pgnshw8vGcd+0 11 | N6ctlu6sWjjEa2AtUX2IRdZRF8zKlIvHcNl+xrX3Q3VDbyr0aR1Cs82Cc9tctO3X 12 | c2tSV3BB8IBqDS5dHEvq4BvBA7PDZs8pJWUpvFgUmS9LUHmUs5yTFlBodICFMixR 13 | 39tH5GBiIUXJYi87wuYJoJXbpVpENex3VUKnOKjMQQKBgQD6lG1//2K1YkxJ6gxQ 14 | +4pq44ABMkaiNMEB3mq4AdHkKvjVfr2esVGWEa8os9P0wJ5tQqe3cXG2gtvXkJRT 15 | /jZim4LpsT0c3XtbHsz0DVb76/Wtc6gsDg940LTgWTp3e2FO0QtSLZnGaamOYJPS 16 | qO9msVYdr1NO+OGiXnEy2lYsUwKBgQD2vpdpWWbCFgDDzn8L8CgrjWi9KBsStnCF 17 | 0eGcZmoriGFHxUgNcJ8G+wl/8FrSQr2zdr4YqiEKJeo+RERAohFAOGZmkFZYbQe3 18 | I0JQCVK+M6G/Zf8OHgWMNjJrQ+rmIvIvtO9QgPNtdzRw5bkcBfZCeJrNPzV1EgR5 19 | D1whcoMjoQKBgDdDwRq3wpdqhJTQr8K0l4SXhEW/RuDDbcXxveuzSw1dhN/hQgTb 20 | 6riEUfNSJe8XBFnol4DX3lJ4bfAPDQexS2FYFvlfg7D4EBq6ok05G/QXyGlm1rJQ 21 | r4zfyuSoCUMMzRtK84o+UGn4J9Mk7bVKWPJ3Lh0B7AfA0FK1LZYfnV3NAoGBAPAz 22 | dASLkpxIfTAgus8tWH2laJwUCd76mam8Osxdaue8GS+cHttuknFiOspAermLXU7y 23 | vnYWUJmndVRucp8U5oRFI3Ke+l+UrFkdSvXNTa55ZvGDYnskwLPRIt4HPQoSZQAK 24 | PJp7Hf6nd/abu8tLBoOJEvHRocG463/Kcx7gckdhAoGBAPmlntv6qN1rs7r8NTXo 25 | cn4jGCjKOhf5BjorpOdvR0zanhe+5KehvYaDCv/2pyvTkboqyX2vvn36mG8qkuPB 26 | Fc63ycNa+SSiCbvpOyXD7oytxEblsUWpt8RGi1TRk8n0yJQ0paGbwlehDQDmMAd/ 27 | DdAs80ie0jqFHN21Ia1dbZpI 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /misc/pac.http.js: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | // A request over http, where the domain has the suffix '.internal.improbable.io' and 3 | // an optional port. 4 | var regexp = /^http:\/\/[^\/]+\.local(\:[\d]+)?\// 5 | if (regexp.test(url)) { 6 | return "PROXY localhost:8080" 7 | } 8 | return "DIRECT" 9 | } 10 | -------------------------------------------------------------------------------- /misc/pac.https.js: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | // A request over http, where the domain has the suffix '.local' and 3 | // an optional port. 4 | var regexp = /^http:\/\/[^\/]+\.local(\:[\d]+)?\// 5 | if (regexp.test(url)) { 6 | return "HTTPS localhost:8443" 7 | } 8 | return "DIRECT" 9 | } 10 | -------------------------------------------------------------------------------- /misc/winch_auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth_sources": [ 3 | { 4 | "name": "test-endpoint-grpc", 5 | "token": { 6 | "token": "secret-token" 7 | } 8 | }, 9 | { 10 | "name": "test-endpoint-http", 11 | "token": { 12 | "token": "secret-token2" 13 | } 14 | }, 15 | { 16 | "name": "proxy-bearer-token-auth", 17 | "token": { 18 | "token": "secret-bearer-token" 19 | } 20 | }, 21 | { 22 | "name": "wrong-proxy-bearer-token-auth", 23 | "token": { 24 | "token": "wrong-secret-bearer-token" 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /misc/winch_mapper.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "regexp": { 5 | "exp": "^no_auth[.](?P[a-z0-9-].*)[.]internal[.]example[.]com", 6 | "url": "https://${cluster}:18171" 7 | } 8 | }, 9 | { 10 | "proxy_auth": "proxy-bearer-token-auth", 11 | "regexp": { 12 | "exp": "^bearer_token_auth[.](?P[a-z0-9-].*)[.]internal[.]example[.]com", 13 | "url": "https://${cluster}:18171" 14 | } 15 | }, 16 | { 17 | "proxy_auth": "wrong-proxy-bearer-token-auth", 18 | "regexp": { 19 | "exp": "^wrong_bearer_token_auth[.](?P[a-z0-9-].*)[.]internal[.]example[.]com", 20 | "url": "https://${cluster}:18171" 21 | } 22 | }, 23 | { 24 | "backend_auth": "test-endpoint-http", 25 | "regexp": { 26 | "exp": "^test_endpoint[.](?P[a-z0-9-].*)[.]internal[.]example[.]com", 27 | "url": "https://${cluster}:18171" 28 | }, 29 | "protocol": "HTTP" 30 | }, 31 | { 32 | "backend_auth": "test-endpoint-grpc", 33 | "regexp": { 34 | "exp": "^([a-z0-9-_].*)[.](?P[a-z0-9-].*)[.]internal[.]example[.]com", 35 | "url": "https://${cluster}:18172" 36 | }, 37 | "protocol": "GRPC" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /pkg/bearertokenauth/bearertokenauth.go: -------------------------------------------------------------------------------- 1 | package bearertokenauth 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/codes" 8 | 9 | "github.com/Bplotka/oidc/authorize" 10 | ) 11 | 12 | type bearerTokenAuthorizer struct { 13 | token string 14 | } 15 | 16 | // NewAuthorizer returns a new Bearer Token Authorizer that checks the provided token to the one it is initialized with. 17 | func NewAuthorizer(token string) authorize.Authorizer { 18 | return &bearerTokenAuthorizer{ 19 | token: token, 20 | } 21 | } 22 | 23 | func (b bearerTokenAuthorizer) IsAuthorized(ctx context.Context, token string) error { 24 | if token != b.token { 25 | return grpc.Errorf(codes.Unauthenticated, "provided bearer token is invalid") 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/discovery/client.go: -------------------------------------------------------------------------------- 1 | package discovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/improbable-eng/kedge/pkg/k8s" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type serviceClient interface { 14 | StartChangeStream(ctx context.Context, labelSelector string) (io.ReadCloser, error) 15 | } 16 | 17 | type client struct { 18 | k8sClient *k8s.APIClient 19 | } 20 | 21 | // StartChangeStream starts stream of changes from watch services. 22 | // See https://kubernetes.io/docs/api-reference/v1.7/#watch-132 23 | // NOTE: In the beginning of stream, k8s will give us sufficient info about current state. (No need to GET first) 24 | func (c *client) StartChangeStream(ctx context.Context, labelSelector string) (io.ReadCloser, error) { 25 | servicesToExposeWatch := fmt.Sprintf("%s/api/v1/watch/services?labelSelector=%s", 26 | c.k8sClient.Address, 27 | labelSelector, 28 | ) 29 | 30 | return c.startGET(ctx, servicesToExposeWatch) 31 | } 32 | 33 | // NOTE: It is caller responsibility to read body through and close it. 34 | func (c *client) startGET(ctx context.Context, url string) (io.ReadCloser, error) { 35 | req, err := http.NewRequest("GET", url, nil) 36 | if err != nil { 37 | return nil, errors.Wrapf(err, "Failed to create new GET request %s", url) 38 | } 39 | 40 | resp, err := c.k8sClient.Do(req.WithContext(ctx)) 41 | if err != nil { 42 | return nil, errors.Wrapf(err, "Failed to do GET %s request", url) 43 | } 44 | 45 | if resp.StatusCode != http.StatusOK { 46 | resp.Body.Close() 47 | return nil, errors.Errorf("Invalid response code %d on GET %s request", resp.StatusCode, url) 48 | } 49 | 50 | return resp.Body, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/e2e/common.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import "time" 4 | 5 | // Retry executes f every interval seconds until timeout or no error is returned from f. 6 | // This function is copy-pasted from "github.com/improbable-eng/thanos/pkg/runutil" 7 | func Retry(interval time.Duration, stopc <-chan struct{}, f func() error) error { 8 | tick := time.NewTicker(interval) 9 | defer tick.Stop() 10 | 11 | var err error 12 | for { 13 | if err = f(); err == nil { 14 | return nil 15 | } 16 | select { 17 | case <-stopc: 18 | return err 19 | case <-tick.C: 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/e2e/wpad_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const expectedPACFile = `function FindProxyForURL(url, host) { 17 | var proxy = "PROXY 127.0.0.1:18070; DIRECT"; 18 | var direct = "DIRECT"; 19 | 20 | // no proxy for local hosts without domain: 21 | if(isPlainHostName(host)) return direct; 22 | 23 | // We only proxy http, not even https. 24 | if ( 25 | url.substring(0, 4) == "ftp:" || 26 | url.substring(0, 6) == "rsync:" || 27 | url.substring(0, 6) == "https:" 28 | ) 29 | return direct; 30 | 31 | // Commented for debug purposes. 32 | // Use direct connection whenever we have direct network connectivity. 33 | //if (isResolvable(host)) { 34 | // return direct 35 | //} 36 | if (shExpMatch(host, "*.*.internal.example.com")) { 37 | return proxy; 38 | } 39 | 40 | return direct; 41 | }` 42 | 43 | func TestWinchPAC(t *testing.T) { 44 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 45 | exit, err := spinup(t, ctx, config{winch: true}) 46 | if err != nil { 47 | t.Error(err) 48 | cancel() 49 | return 50 | } 51 | 52 | defer func() { 53 | cancel() 54 | <-exit 55 | }() 56 | 57 | err = Retry(time.Second, ctx.Done(), func() error { 58 | if err = assertRunning(exit); err != nil { 59 | t.Error(err) 60 | return nil 61 | } 62 | 63 | res, err := queryWPAD(ctx) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | assert.Equal(t, expectedPACFile, res) 69 | 70 | return nil 71 | }) 72 | require.NoError(t, err) 73 | } 74 | 75 | func assertRunning(unexpectedExit chan error) error { 76 | select { 77 | case err := <-unexpectedExit: 78 | return errors.Wrap(err, "Some process exited unexpectedly") 79 | default: 80 | return nil 81 | } 82 | } 83 | 84 | func queryWPAD(ctx context.Context) (string, error) { 85 | req, err := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%s/wpad.dat", httpWinchPort), nil) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | resp, err := http.DefaultClient.Do(req.WithContext(ctx)) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | defer resp.Body.Close() 96 | 97 | b, err := ioutil.ReadAll(resp.Body) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | return string(b), nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/grpcutils/metadata.go: -------------------------------------------------------------------------------- 1 | package grpcutils 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc/metadata" 7 | ) 8 | 9 | func CloneIncomingToOutgoingMD(ctx context.Context) context.Context { 10 | md, ok := metadata.FromIncomingContext(ctx) 11 | if !ok { 12 | return ctx 13 | } 14 | // Copy the inbound metadata explicitly. 15 | return metadata.NewOutgoingContext(ctx, md.Copy()) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/http/ctxtags/tags.go: -------------------------------------------------------------------------------- 1 | package ctxtags 2 | 3 | const ( 4 | 5 | // TagForAuth is used in authTripperware to specify auth.Source for backend auth. 6 | TagForAuth = "http.auth" 7 | // TagForProxyAuth is used in authTripperware to specify auth.Source for proxy auth. 8 | TagForProxyAuth = "http.proxy.auth" 9 | 10 | // TagForProxyDestURL is used in routeTripperware to specify proxy URL. 11 | TagForProxyDestURL = "http.proxy.url" 12 | 13 | // TagForProxyAdhoc is used in kedge proxy to specify adhoc rule used in request. 14 | TagForProxyAdhoc = "http.proxy.adhoc" 15 | // TagForProxyBackend is used in kedge proxy to specify backend used in request. 16 | TagForProxyBackend = "http.proxy.backend" 17 | // TagForScheme specifies which scheme request is using. It is specified by each server. 18 | TagForScheme = "http.scheme" 19 | 20 | // TagForProxyAuthTime specifies time that took to put valid proxy auth in Headers. 21 | // It can sometimes take time in case of full OIDC login. 22 | TagForProxyAuthTime = "http.proxy.auth.time" 23 | // TagForBackendAuthTime specifies time that took to put valid backend auth in Headers. 24 | // It can sometimes take time in case of full OIDC login. 25 | TagForBackendAuthTime = "http.auth.time" 26 | 27 | // TagForBackendTarget specifies the target name used to resolve in lbtransport by backend 28 | TagForBackendTarget = "http.backend.target" 29 | 30 | // TagForTargetAddress specifies the resolved address used by request in lbtransport. 31 | TagForTargetAddress = "http.target.address" 32 | 33 | // TagRequestID specified request ID of the request. 34 | TagRequestID = "http.request_id" 35 | ) 36 | -------------------------------------------------------------------------------- /pkg/http/header/request.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | const ( 4 | // RequestKedgeRequestID header is used to specify RequestID. 5 | RequestKedgeRequestID = "X-Kedge-Request-ID" 6 | 7 | // RequestKedgeForceInfoLogs header is used to signal kedge, to log on info proxy error or OK request. 8 | RequestKedgeForceInfoLogs = "X-Kedge-Info-Logs" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/http/header/response.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | const ( 4 | // ResponseKedgeError header is used to expose in HTTP response real error why request was not proxied. 5 | ResponseKedgeError = "X-Kedge-Error" 6 | 7 | // ResponseKedgeError header is used to expose in HTTP respone error Type of the error resulting in request not proxied. 8 | ResponseKedgeErrorType = "X-Kedge-Error-Type" 9 | 10 | // ResponseWinchError header is used to expose in HTTP response real error why request was not proxied on winch side. 11 | ResponseWinchError = "X-Winch-Error" 12 | 13 | // ResponseWinchErrorType header is used to expose in HTTP respone error Type of the error resulting in request not proxied on winch side. 14 | ResponseWinchErrorType = "X-Winch-Error-Type" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/http/tripperware/auth.go: -------------------------------------------------------------------------------- 1 | package tripperware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | http_ctxtags "github.com/improbable-eng/go-httpwares/tags" 9 | "github.com/improbable-eng/kedge/pkg/http/ctxtags" 10 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 11 | "github.com/improbable-eng/kedge/pkg/tokenauth" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | ProxyAuthHeader = "Proxy-Authorization" 17 | authHeader = "Authorization" 18 | ) 19 | 20 | // authTripper is a piece of tripperware that injects auth defined per route to authorize request for. 21 | // NOTE: It requires to have mappingTripper before itself to put the routing inside context. 22 | type authTripper struct { 23 | parent http.RoundTripper 24 | 25 | authHeader string 26 | authTag string 27 | authTimeTag string 28 | authFromRoute func(route *kedge_map.Route) (tokenauth.Source, bool) 29 | } 30 | 31 | func (t *authTripper) RoundTrip(req *http.Request) (*http.Response, error) { 32 | route, ok, err := getRoute(req.Context()) 33 | if err != nil { 34 | closeIfNotNil(req.Body) 35 | return nil, errors.Wrap(err, "authTripper: Failed to get route from context") 36 | } 37 | if !ok { 38 | return t.parent.RoundTrip(req) 39 | } 40 | 41 | authSource, ok := t.authFromRoute(route) 42 | if authSource == nil { 43 | // No auth configured. 44 | return t.parent.RoundTrip(req) 45 | } 46 | 47 | tags := http_ctxtags.ExtractInbound(req) 48 | tags.Set(t.authTag, authSource.Name()) 49 | 50 | now := time.Now() 51 | val, err := authSource.Token(req.Context()) 52 | tags.Set(t.authTimeTag, time.Since(now).String()) 53 | if err != nil { 54 | closeIfNotNil(req.Body) 55 | return nil, errors.Wrapf(err, "authTripper: Failed to get header value from authSource %s", authSource.Name()) 56 | } 57 | 58 | req.Header.Set(t.authHeader, fmt.Sprintf("Bearer %s", val)) 59 | return t.parent.RoundTrip(req) 60 | } 61 | 62 | func WrapForProxyAuth(parentTransport http.RoundTripper) http.RoundTripper { 63 | return &authTripper{ 64 | parent: parentTransport, 65 | authHeader: ProxyAuthHeader, 66 | authTag: ctxtags.TagForProxyAuth, 67 | authTimeTag: ctxtags.TagForProxyAuthTime, 68 | authFromRoute: func(route *kedge_map.Route) (tokenauth.Source, bool) { 69 | return route.ProxyAuth, route.ProxyAuth != nil 70 | }, 71 | } 72 | } 73 | 74 | func WrapForBackendAuth(parentTransport http.RoundTripper) http.RoundTripper { 75 | return &authTripper{ 76 | parent: parentTransport, 77 | authHeader: authHeader, 78 | authTag: ctxtags.TagForAuth, 79 | authTimeTag: ctxtags.TagForBackendAuthTime, 80 | authFromRoute: func(route *kedge_map.Route) (tokenauth.Source, bool) { 81 | return route.BackendAuth, route.BackendAuth != nil 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/http/tripperware/debug.go: -------------------------------------------------------------------------------- 1 | package tripperware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "os" 8 | 9 | "github.com/google/uuid" 10 | http_ctxtags "github.com/improbable-eng/go-httpwares/tags" 11 | "github.com/improbable-eng/kedge/pkg/http/ctxtags" 12 | "github.com/improbable-eng/kedge/pkg/http/header" 13 | ) 14 | 15 | // setHeaderTripperware is a piece of tripperware that sets specified header value into request. 16 | // Optionally you can specify tag name that to put the header value into. 17 | type setHeaderTripperware struct { 18 | headerName string 19 | headerValueFn func() string 20 | tagName string 21 | 22 | parent http.RoundTripper 23 | } 24 | 25 | func (t *setHeaderTripperware) RoundTrip(req *http.Request) (*http.Response, error) { 26 | value := t.headerValueFn() 27 | req.Header.Set(t.headerName, value) 28 | 29 | if t.tagName != "" { 30 | tags := http_ctxtags.ExtractInbound(req) 31 | tags.Set(t.tagName, value) 32 | } 33 | return t.parent.RoundTrip(req) 34 | } 35 | 36 | // WrapForRequestID wraps tripperware with new one that appends unique request ID to "X-Kedge-Request-ID" header allowing 37 | // better debug tracking. 38 | func WrapForRequestID(prefix string, parentTransport http.RoundTripper) http.RoundTripper { 39 | return &setHeaderTripperware{ 40 | headerName: header.RequestKedgeRequestID, 41 | headerValueFn: func() string { 42 | return fmt.Sprintf("%s%s", prefix, uuid.New().String()) 43 | }, 44 | tagName: ctxtags.TagRequestID, 45 | parent: parentTransport, 46 | } 47 | } 48 | 49 | // WrapForDebug wraps tripperware with new one that signals kedge to use INFO logs for all DEBUG logs. 50 | func WrapForDebug(parentTransport http.RoundTripper) http.RoundTripper { 51 | return &setHeaderTripperware{ 52 | headerName: header.RequestKedgeForceInfoLogs, 53 | headerValueFn: func() string { 54 | return os.ExpandEnv("winch-$USER") 55 | }, 56 | parent: parentTransport, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/http/tripperware/map.go: -------------------------------------------------------------------------------- 1 | package tripperware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | http_ctxtags "github.com/improbable-eng/go-httpwares/tags" 8 | "github.com/improbable-eng/kedge/pkg/http/ctxtags" 9 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // routeContextKey specifies the key that route is stored inside request's context. 14 | // if no route is found the nil route is stored. 15 | const routeContextKey = "proxy-route" 16 | 17 | func getRoute(ctx context.Context) (*kedge_map.Route, bool, error) { 18 | r, ok := ctx.Value(routeContextKey).(*kedge_map.Route) 19 | if !ok { 20 | // Unit tests should catch that. 21 | return nil, false, errors.New("InternalError: Tripperware misconfiguration. MappingTripper was not before current tripper.") 22 | } 23 | return r, r != nil, nil 24 | } 25 | 26 | type mappingTripper struct { 27 | mapper kedge_map.Mapper 28 | parent http.RoundTripper 29 | } 30 | 31 | func requestWithRoute(req *http.Request, r *kedge_map.Route) *http.Request { 32 | return req.WithContext(context.WithValue(req.Context(), routeContextKey, r)) 33 | } 34 | 35 | func (t *mappingTripper) RoundTrip(req *http.Request) (*http.Response, error) { 36 | // Get routing based on request Host and optional port. 37 | route, err := t.mapper.Map(req.URL.Hostname(), req.URL.Port()) 38 | if kedge_map.IsNotKedgeDestinationError(err) { 39 | tags := http_ctxtags.ExtractInbound(req) 40 | tags.Set(ctxtags.TagForProxyDestURL, "not-kedge-destination") 41 | return t.parent.RoundTrip( 42 | // We store nil to ensure that we can catch case when mappingTripper is not in the chain before other trippers 43 | // that requires stored route. 44 | requestWithRoute(req, nil), 45 | ) 46 | } 47 | if err != nil { 48 | closeIfNotNil(req.Body) 49 | return nil, errors.Wrapf(err, "mappingTripper: Failed to map host %s and port %s into route", req.URL.Hostname(), req.URL.Port()) 50 | } 51 | 52 | return t.parent.RoundTrip( 53 | requestWithRoute(req, route), 54 | ) 55 | } 56 | 57 | func WrapForMapping(mapper kedge_map.Mapper, parentTransport http.RoundTripper) http.RoundTripper { 58 | return &mappingTripper{mapper: mapper, parent: parentTransport} 59 | } 60 | -------------------------------------------------------------------------------- /pkg/http/tripperware/route.go: -------------------------------------------------------------------------------- 1 | package tripperware 2 | 3 | import ( 4 | "net/http" 5 | 6 | http_ctxtags "github.com/improbable-eng/go-httpwares/tags" 7 | "github.com/improbable-eng/kedge/pkg/http/ctxtags" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // routingTripper is a piece of tripperware that dials certain destinations (indicated by a route stored in request's context) through a remote proxy (kedge). 12 | // The dialing is performed in a "reverse proxy" fashion, where the Host header indicates to the kedge what backend 13 | // to dial. 14 | // NOTE: It requires to have mappingTripper before itself to put the routing inside context. 15 | type routingTripper struct { 16 | parent http.RoundTripper 17 | } 18 | 19 | func (t *routingTripper) RoundTrip(req *http.Request) (*http.Response, error) { 20 | route, ok, err := getRoute(req.Context()) 21 | if err != nil { 22 | closeIfNotNil(req.Body) 23 | return nil, errors.Wrap(err, "routingTripper: Failed to get route from context") 24 | } 25 | if !ok { 26 | return t.parent.RoundTrip(req) 27 | } 28 | // TODO(bplotka): Pack some cache for kedge DNS resolution. It does not change much after winch startup. 29 | destURL := route.URL 30 | 31 | tags := http_ctxtags.ExtractInbound(req) 32 | tags.Set(ctxtags.TagForProxyDestURL, destURL) 33 | tags.Set(http_ctxtags.TagForHandlerName, destURL) 34 | 35 | // Copy the URL and the request to not override the callers info. 36 | copyUrl := *req.URL 37 | copyUrl.Scheme = destURL.Scheme 38 | copyUrl.Host = destURL.Host 39 | copyReq := req.WithContext(req.Context()) // makes a copy 40 | copyReq.URL = &(copyUrl) // shallow copy 41 | copyReq.Host = req.URL.Host // store the original host (+ optional :port) 42 | return t.parent.RoundTrip(copyReq) 43 | } 44 | 45 | func WrapForRouting(parentTransport http.RoundTripper) http.RoundTripper { 46 | return &routingTripper{parent: parentTransport} 47 | } 48 | -------------------------------------------------------------------------------- /pkg/http/tripperware/tripperware.go: -------------------------------------------------------------------------------- 1 | package tripperware 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "net" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type defaultTripper struct { 12 | *http.Transport 13 | } 14 | 15 | func Default(config *tls.Config) http.RoundTripper { 16 | transport := &http.Transport{ 17 | Proxy: http.ProxyFromEnvironment, 18 | // TODO(bplotka): Switch back to DialCtx when it will be safe to use. 19 | // https://groups.google.com/forum/#!topic/golang-nuts/oiBBZfUb2hM 20 | Dial: (&net.Dialer{ 21 | Timeout: 10 * time.Second, 22 | KeepAlive: 30 * time.Second, 23 | DualStack: false, 24 | }).Dial, 25 | MaxIdleConns: 4, 26 | IdleConnTimeout: 90 * time.Second, 27 | TLSHandshakeTimeout: 10 * time.Second, 28 | ExpectContinueTimeout: 1 * time.Second, 29 | TLSClientConfig: config, 30 | } 31 | return &defaultTripper{Transport: transport} 32 | } 33 | 34 | func DefaultWithTransport(transport *http.Transport, config *tls.Config) http.RoundTripper { 35 | transport.TLSClientConfig = config 36 | return &defaultTripper{Transport: transport} 37 | } 38 | 39 | func closeIfNotNil(r io.Closer) { 40 | if r != nil { 41 | _ = r.Close() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/k8s/client.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/improbable-eng/kedge/pkg/tokenauth" 8 | httpauth "github.com/improbable-eng/kedge/pkg/tokenauth/http" 9 | ) 10 | 11 | type APIClient struct { 12 | *http.Client 13 | 14 | Address string 15 | } 16 | 17 | // New returns a new Kubernetes client with HTTP client (based on given tokenauth Source and tlsConfig) to be used against kube-apiserver. 18 | func New(k8sURL string, source tokenauth.Source, tlsConfig *tls.Config) *APIClient { 19 | return &APIClient{ 20 | Client: &http.Client{ 21 | // TLS transport with auth injection. 22 | Transport: httpauth.NewTripper( 23 | &http.Transport{ 24 | TLSClientConfig: tlsConfig, 25 | }, 26 | source, 27 | "Authorization", 28 | ), 29 | }, 30 | Address: k8sURL, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/k8s/flags.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/url" 10 | "os" 11 | 12 | "context" 13 | "time" 14 | 15 | "github.com/improbable-eng/kedge/pkg/sharedflags" 16 | "github.com/improbable-eng/kedge/pkg/tokenauth" 17 | directauth "github.com/improbable-eng/kedge/pkg/tokenauth/sources/direct" 18 | k8sauth "github.com/improbable-eng/kedge/pkg/tokenauth/sources/k8s" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | const ( 23 | defaultSAToken = "/var/run/secrets/kubernetes.io/serviceaccount/token" 24 | defaultSACACert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 25 | ) 26 | 27 | var ( 28 | // NOTE: Default values for all flags are designed for running within k8s pod. 29 | defaultKubeURL = fmt.Sprintf("https://%s", net.JoinHostPort(os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT"))) 30 | fKubeAPIURL = sharedflags.Set.String("k8sclient_kubeapi_url", defaultKubeURL, 31 | "TCP address to Kube API server in a form of 'http(s)://host:value'. If empty it will be fetched from env variables:"+ 32 | "KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT") 33 | fInsecureSkipVerify = sharedflags.Set.Bool("k8sclient_tls_insecure", false, "If enabled, no server verification will be "+ 34 | "performed on client side. Not recommended.") 35 | fKubeAPIRootCAPath = sharedflags.Set.String("k8sclient_ca_file", defaultSACACert, "Path to service account CA file. "+ 36 | "Required if kubeapi_tls_insecure = false.") 37 | 38 | // Different kinds of auth are supported. Currently supported with flags: 39 | // - specifying file with token 40 | // - specifying user (access) for kube config auth section to be reused 41 | fTokenAuthPath = sharedflags.Set.String("k8client_token_file", defaultSAToken, 42 | "Path to service account token to be used. This auth method has priority 2.") 43 | fKubeConfigAuthUser = sharedflags.Set.String("k8sclient_kubeconfig_user", "", 44 | "If user is specified resolver will try to fetch api auth method directly from kubeconfig. "+ 45 | "This auth method has priority 1.") 46 | fKubeConfigAuthPath = sharedflags.Set.String("k8sclient_kubeconfig_path", "", "Kube config path. "+ 47 | "Only used when k8sclient_kubeconfig_user is specified. If empty it will try default path.") 48 | fAuthTimeout = sharedflags.Set.Duration("k8sclient_auth_timeout", 10*time.Second, "Max duration we will wait for k8s client auth to be set up.") 49 | ) 50 | 51 | // NewFromFlags creates APIClient from flags. 52 | func NewFromFlags() (*APIClient, error) { 53 | k8sURL := *fKubeAPIURL 54 | if k8sURL == "" || k8sURL == "https://:" { 55 | return nil, errors.Errorf( 56 | "k8sclient: k8sclient_kubeapi_url flag needs to be specified or " + 57 | "KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined") 58 | } 59 | 60 | _, err := url.Parse(k8sURL) 61 | if err != nil { 62 | return nil, errors.Wrapf(err, "k8sclient: k8sclient_kubeapi_url flag needs to be valid URL. Value %s ", k8sURL) 63 | } 64 | tlsConfig := &tls.Config{ 65 | InsecureSkipVerify: *fInsecureSkipVerify, 66 | } 67 | if !*fInsecureSkipVerify { 68 | ca, err := ioutil.ReadFile(*fKubeAPIRootCAPath) 69 | if err != nil { 70 | return nil, errors.Wrapf(err, "k8sclient: failed to parse RootCA from file %s", *fKubeAPIRootCAPath) 71 | } 72 | certPool := x509.NewCertPool() 73 | certPool.AppendCertsFromPEM(ca) 74 | tlsConfig = &tls.Config{ 75 | MinVersion: tls.VersionTLS10, 76 | RootCAs: certPool, 77 | } 78 | } 79 | 80 | var source tokenauth.Source 81 | 82 | ctx, cancel := context.WithTimeout(context.Background(), *fAuthTimeout) 83 | defer cancel() 84 | // Try kubeconfig auth first. 85 | if user := *fKubeConfigAuthUser; user != "" { 86 | source, err = k8sauth.New(ctx, "kube_api", *fKubeConfigAuthPath, user) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "k8sclient: failed to create k8sauth Source") 89 | } 90 | } 91 | 92 | if source == nil { 93 | // Try token auth as fallback. 94 | token, err := ioutil.ReadFile(*fTokenAuthPath) 95 | if err != nil { 96 | return nil, errors.Wrapf(err, "k8sclient: failed to parse token from %s. No auth method found", *fTokenAuthPath) 97 | } 98 | source = directauth.New("kube_api", string(token)) 99 | } 100 | 101 | return New(k8sURL, source, tlsConfig), nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/kedge/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | 9 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/common" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | var ( 14 | // DefaultALookup is the lookup resolver for DNS A records. 15 | // You can override it for caching or testing. 16 | DefaultALookup = net.LookupHost 17 | ) 18 | 19 | // Addresser implements logic that decides what "ad-hoc" ip:port to dial for a backend, if any. 20 | // 21 | // Adhoc rules are a way of forwarding requests to services that fall outside of pre-defined Routes and Backends. 22 | type Addresser interface { 23 | // Address decides the ip:port to send the request to, if any. Errors may be returned if permission is denied. 24 | // The returned string must contain contain both ip and port separated by colon. 25 | Address(hostString string) (string, error) 26 | } 27 | 28 | type dynamic struct { 29 | mu sync.RWMutex 30 | staticAddresser Addresser 31 | } 32 | 33 | // NewDynamic creates a new dynamic router that can be have its routes updated. 34 | func NewDynamic(add Addresser) *dynamic { 35 | return &dynamic{staticAddresser: add} 36 | } 37 | 38 | func (d *dynamic) Address(hostString string) (string, error) { 39 | d.mu.RLock() 40 | addresser := d.staticAddresser 41 | d.mu.RUnlock() 42 | return addresser.Address(hostString) 43 | } 44 | 45 | // Update sets addresser behaviour to the provided set of adhoc rules. 46 | func (d *dynamic) Update(add Addresser) { 47 | d.mu.Lock() 48 | d.staticAddresser = add 49 | d.mu.Unlock() 50 | } 51 | 52 | func ExtractHostPort(hostStr string) (hostName string, port int, err error) { 53 | // Using SplitHostPort is a pain due to opaque error messages. Let's assume we only do hostname matches, they fall 54 | // through later anyway. 55 | portOffset := strings.LastIndex(hostStr, ":") 56 | if portOffset == -1 { 57 | return hostStr, 0, nil 58 | } 59 | portPart := hostStr[portOffset+1:] 60 | pNum, err := strconv.ParseInt(portPart, 10, 32) 61 | if err != nil { 62 | return "", 0, err 63 | } 64 | return hostStr[:portOffset], int(pNum), nil 65 | } 66 | 67 | func HostMatches(host string, matcher string) bool { 68 | if matcher == "" { 69 | return false 70 | } 71 | if matcher[0] != '*' { 72 | return host == matcher 73 | } 74 | return strings.HasSuffix(host, matcher[1:]) 75 | } 76 | 77 | func PortAllowed(port int, portRule *pb.Adhoc_Port) bool { 78 | uPort := uint32(port) 79 | for _, p := range portRule.Allowed { 80 | if p == uPort { 81 | return true 82 | } 83 | } 84 | for _, r := range portRule.AllowedRanges { 85 | if r.From <= uPort && uPort <= r.To { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | 92 | func AdhocResolveHost(hostStr string, replace *pb.Adhoc_Replace) (string, error) { 93 | if replace != nil { 94 | if !strings.Contains(hostStr, replace.Pattern) { 95 | return "", errors.Errorf("replace pattern %s does match given host %s. Configuration error", replace.Pattern, hostStr) 96 | } 97 | 98 | hostStr = strings.Replace(hostStr, replace.Pattern, replace.Substitution, -1) 99 | } 100 | addrs, err := DefaultALookup(hostStr) 101 | if err != nil { 102 | return "", err 103 | } 104 | if len(addrs) == 0 { 105 | return "", errors.Errorf("did not find any A records for host %v", hostStr) 106 | } 107 | return addrs[0], nil 108 | } 109 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/backendpool/dynamic.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "hash/fnv" 5 | "sync" 6 | 7 | "github.com/improbable-eng/kedge/pkg/metrics" 8 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/grpc/backends" 9 | "github.com/sirupsen/logrus" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // dynamic is a Pool to which you can update or remove routes. 14 | type dynamic struct { 15 | backends map[string]*backend 16 | mu sync.RWMutex 17 | backendFactory func(backend *pb.Backend) (*backend, error) 18 | logger logrus.FieldLogger 19 | } 20 | 21 | func (s *dynamic) Close() error { 22 | s.mu.Lock() 23 | defer s.mu.Unlock() 24 | for _, be := range s.backends { 25 | be.Close() 26 | } 27 | return nil 28 | } 29 | 30 | // NewDynamic creates a pool with a dynamic allocator 31 | func NewDynamic(logger logrus.FieldLogger) *dynamic { 32 | s := &dynamic{backends: make(map[string]*backend), backendFactory: newBackend, logger: logger} 33 | return s 34 | } 35 | 36 | func (s *dynamic) Conn(backendName string) (*grpc.ClientConn, error) { 37 | s.mu.RLock() 38 | defer s.mu.RUnlock() 39 | be, ok := s.backends[backendName] 40 | if !ok { 41 | return nil, ErrUnknownBackend 42 | } 43 | return be.Conn() 44 | } 45 | 46 | // AddOrUpdate checks tries to perform the least destructive operation of adding a new backend. 47 | // 48 | // If a backend of a given name already exists, and the configuration hasn't changed, no new work will be done. 49 | // If a backend requires changes, the previous one will be removed and closed. 50 | func (s *dynamic) AddOrUpdate(config *pb.Backend, logTestResolution bool) (changed bool, err error) { 51 | s.mu.RLock() 52 | existing, ok := s.backends[config.Name] 53 | s.mu.RUnlock() 54 | 55 | defer func() { 56 | if changed && logTestResolution { 57 | go s.backends[config.Name].LogTestResolution( 58 | s.logger.WithField("protocol", "grpc").WithField("backend", config.Name), 59 | ) 60 | } 61 | }() 62 | 63 | if !ok { 64 | err = s.addNewBackend(config) 65 | if err != nil { 66 | return changed, err 67 | } 68 | changed = true 69 | s.logger.Infof("Adding new grpc backend: %v", config.Name) 70 | metrics.BackendGRPCConfigurationCounter.WithLabelValues(config.Name, metrics.ConfiguationActionCreate).Inc() 71 | return changed, nil 72 | } 73 | 74 | var updated bool 75 | updated, err = s.updateBackendWithDiffing(existing, config) 76 | if err != nil { 77 | return changed, err 78 | } 79 | if updated { 80 | changed = true 81 | s.logger.Infof("Updated grpc backend: %v", config.Name) 82 | metrics.BackendGRPCConfigurationCounter.WithLabelValues(config.Name, metrics.ConfiguationActionChange).Inc() 83 | } 84 | return changed, nil 85 | } 86 | 87 | func (s *dynamic) addNewBackend(config *pb.Backend) error { 88 | be, err := s.backendFactory(config) 89 | if err != nil { 90 | return err 91 | } 92 | s.mu.Lock() 93 | s.backends[config.Name] = be 94 | s.mu.Unlock() 95 | return nil 96 | } 97 | 98 | func (s *dynamic) updateBackendWithDiffing(existing *backend, config *pb.Backend) (changed bool, err error) { 99 | if configsAreTheSame(existing.config, config) { 100 | return false, nil 101 | } 102 | if err := s.addNewBackend(config); err != nil { 103 | return true, err 104 | } 105 | // Make sure we clear up resources. 106 | existing.Close() 107 | return true, nil 108 | } 109 | 110 | // Remove removes and shuts down a previously active backend. 111 | func (s *dynamic) Remove(backendName string) error { 112 | s.mu.RLock() 113 | existing, ok := s.backends[backendName] 114 | s.mu.RUnlock() 115 | if !ok { 116 | return ErrUnknownBackend 117 | } 118 | s.mu.Lock() 119 | delete(s.backends, backendName) 120 | s.mu.Unlock() 121 | existing.Close() 122 | 123 | s.logger.Infof("Removed grpc backend: %v", backendName) 124 | metrics.BackendGRPCConfigurationCounter.WithLabelValues(backendName, metrics.ConfiguationActionDelete).Inc() 125 | return nil 126 | } 127 | 128 | // Configs returns a map of all active backends and their configuration. 129 | func (s *dynamic) Configs() map[string]*pb.Backend { 130 | ret := make(map[string]*pb.Backend) 131 | s.mu.RLock() 132 | for k, v := range s.backends { 133 | ret[k] = v.config 134 | } 135 | s.mu.RUnlock() 136 | return ret 137 | } 138 | 139 | func configsAreTheSame(c1 *pb.Backend, c2 *pb.Backend) bool { 140 | h1 := fnv.New64a() 141 | h2 := fnv.New64a() 142 | h1.Write([]byte(c1.String())) 143 | h2.Write([]byte(c2.String())) 144 | return h1.Sum64() == h2.Sum64() 145 | } 146 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/backendpool/dynamic_test.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "testing" 5 | 6 | pb_resolvers "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 7 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/grpc/backends" 8 | "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDynamic_Operations(t *testing.T) { 14 | d := NewDynamic(logrus.New()) 15 | d.backendFactory = func(config *pb.Backend) (*backend, error) { 16 | return &backend{config: config}, nil 17 | } 18 | assert.Len(t, d.Configs(), 0, "at first there needs to be nothing") 19 | changed, err := d.AddOrUpdate( 20 | &pb.Backend{ 21 | Name: "foobar", DisableConntracking: true, Resolver: &pb.Backend_K8S{ 22 | K8S: &pb_resolvers.K8SResolver{ 23 | DnsPortName: "some:port", 24 | }, 25 | }, 26 | }, 27 | false, 28 | ) 29 | require.NoError(t, err) 30 | assert.True(t, changed) 31 | 32 | changed, err = d.AddOrUpdate(&pb.Backend{Name: "carbar", DisableConntracking: true}, false) 33 | require.NoError(t, err) 34 | assert.True(t, changed) 35 | 36 | assert.Len(t, d.Configs(), 2, "we should have two") 37 | oldCarBar := d.backends["carbar"] 38 | changed, err = d.AddOrUpdate(&pb.Backend{Name: "carbar"}, false) 39 | require.NoError(t, err) 40 | assert.True(t, changed) 41 | assert.True(t, oldCarBar.closed, "oldCarBar should enter closed state") 42 | assert.Len(t, d.Configs(), 2, "we should still two") 43 | 44 | // Add totally the same one. 45 | changed, err = d.AddOrUpdate( 46 | &pb.Backend{ 47 | Name: "foobar", DisableConntracking: true, Resolver: &pb.Backend_K8S{ 48 | K8S: &pb_resolvers.K8SResolver{ 49 | DnsPortName: "some:port", 50 | }, 51 | }, 52 | }, 53 | false, 54 | ) 55 | require.NoError(t, err) 56 | assert.False(t, changed, "We tried to update totally the same backend, it should not change.") 57 | 58 | assert.Error(t, d.Remove("nonexisting"), "removing a non existing backend should return error") 59 | assert.NoError(t, d.Remove("foobar"), "removing a non existing backend should return error") 60 | assert.Len(t, d.Configs(), 1, "we now should have two") 61 | } 62 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/backendpool/pool.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "google.golang.org/grpc/codes" 6 | ) 7 | 8 | var ( 9 | ErrUnknownBackend = grpc.Errorf(codes.Unimplemented, "unknown backend") 10 | ) 11 | 12 | type Pool interface { 13 | // Conn returns a dialled grpc.ClientConn for a given backend name. 14 | Conn(backendName string) (*grpc.ClientConn, error) 15 | 16 | // Close closes all the connections of the pool. 17 | Close() error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/backendpool/static.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "fmt" 5 | 6 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/grpc/backends" 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | // static is a Pool with a static configuration. 11 | type static struct { 12 | backends map[string]*backend 13 | } 14 | 15 | func (s *static) Close() error { 16 | for _, be := range s.backends { 17 | be.Close() 18 | } 19 | return nil 20 | } 21 | 22 | // NewStatic creates a backend pool that has static configuration. 23 | func NewStatic(backends []*pb.Backend) (Pool, error) { 24 | s := &static{backends: make(map[string]*backend)} 25 | for _, beCnf := range backends { 26 | be, err := newBackend(beCnf) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed creating backend '%v': %v", beCnf.Name, err) 29 | } 30 | s.backends[beCnf.Name] = be 31 | } 32 | return s, nil 33 | } 34 | 35 | func (s *static) Conn(backendName string) (*grpc.ClientConn, error) { 36 | be, ok := s.backends[backendName] 37 | if !ok { 38 | return nil, ErrUnknownBackend 39 | } 40 | return be.Conn() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/client/client.go: -------------------------------------------------------------------------------- 1 | package kedge_grpc 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | 7 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 8 | "golang.org/x/net/context" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/credentials" 11 | ) 12 | 13 | // DialThroughKedge provides a grpc.ClientConn that forwards all traffic to the specific kedge. 14 | // 15 | // For secure kedging (with a mapper value starting with https://) clientTls option needs to be set, and needs to point 16 | // to a tls.Config that has client side certificates configured. 17 | func DialThroughKedge(ctx context.Context, targetAuthority string, clientTls *tls.Config, mapper kedge_map.Mapper, grpcOpts ...grpc.DialOption) (*grpc.ClientConn, error) { 18 | // No need for port matching here. TODO: Add when required. 19 | kedgeUrl, err := mapper.Map(targetAuthority, "") 20 | if err != nil { 21 | return nil, err 22 | } 23 | if kedgeUrl.URL.Scheme == "https" { 24 | if clientTls == nil || (len(clientTls.Certificates) == 0 && clientTls.GetCertificate == nil) { 25 | return nil, errors.New("dialing through kedge requires a tls.Config with client-side certificates") 26 | } 27 | transportCreds := credentials.NewTLS(clientTls) 28 | transportCreds = &proxiedTlsCredentials{TransportCredentials: transportCreds, authorityNameOverride: targetAuthority} 29 | grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(transportCreds)) 30 | } else { 31 | grpcOpts = append(grpcOpts, grpc.WithInsecure(), grpc.WithAuthority(targetAuthority)) 32 | } 33 | // TODO(mwitkow): Add pooled dialing a-la: https://sourcegraph.com/github.com/google/google-api-go-client@master/-/blob/internal/pool.go#L25:1-27:32 34 | return grpc.DialContext(ctx, kedgeUrl.URL.Host, grpcOpts...) 35 | } 36 | 37 | type proxiedTlsCredentials struct { 38 | credentials.TransportCredentials 39 | authorityNameOverride string 40 | } 41 | 42 | func (c *proxiedTlsCredentials) Info() credentials.ProtocolInfo { 43 | info := c.TransportCredentials.Info() 44 | // gRPC Clients set authority per DialContext rules. For TLS, authority comes from ProtocolInfo. 45 | // This value is not overrideable inside the metadata, since two `:authority` headers would be written, causing an 46 | // error on the transport. 47 | info.ServerName = c.authorityNameOverride 48 | return info 49 | } 50 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/director/adhoc/adhoc.go: -------------------------------------------------------------------------------- 1 | package adhoc 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | 7 | "github.com/improbable-eng/kedge/pkg/kedge/common" 8 | "github.com/improbable-eng/kedge/pkg/kedge/grpc/director/router" 9 | kedge_config_common "github.com/improbable-eng/kedge/protogen/kedge/config/common" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | type static struct { 15 | rules []*kedge_config_common.Adhoc 16 | } 17 | 18 | func NewStaticAddresser(rules []*kedge_config_common.Adhoc) *static { 19 | return &static{rules: rules} 20 | } 21 | 22 | func (a *static) Address(hostString string) (string, error) { 23 | hostName, port, err := common.ExtractHostPort(hostString) 24 | if err != nil { 25 | return "", status.Errorf(codes.InvalidArgument, "adhoc: malformed port number: %v", err) 26 | } 27 | for _, rule := range a.rules { 28 | if !common.HostMatches(hostName, rule.DnsNameMatcher) { 29 | continue 30 | } 31 | portForRule := port 32 | if port == 0 { 33 | if defPort := rule.Port.Default; defPort != 0 { 34 | portForRule = int(defPort) 35 | } else { 36 | portForRule = 81 37 | } 38 | } 39 | if !common.PortAllowed(portForRule, rule.Port) { 40 | return "", status.Errorf(codes.InvalidArgument, "adhoc: port %d is not allowed", portForRule) 41 | } 42 | 43 | ipAddr, err := common.AdhocResolveHost(hostName, rule.DnsNameReplace) 44 | if err != nil { 45 | return "", status.Errorf(codes.NotFound, "adhoc: cannot resolve %s host: %v", hostString, err) 46 | } 47 | return net.JoinHostPort(ipAddr, strconv.FormatInt(int64(portForRule), 10)), nil 48 | 49 | } 50 | return "", router.ErrRouteNotFound 51 | } 52 | -------------------------------------------------------------------------------- /pkg/kedge/grpc/director/director.go: -------------------------------------------------------------------------------- 1 | package director 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Bplotka/oidc/authorize" 7 | grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 8 | grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" 9 | "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" 10 | "github.com/improbable-eng/kedge/pkg/grpcutils" 11 | "github.com/improbable-eng/kedge/pkg/kedge/common" 12 | "github.com/improbable-eng/kedge/pkg/kedge/grpc/backendpool" 13 | "github.com/improbable-eng/kedge/pkg/kedge/grpc/director/router" 14 | "github.com/mwitkow/grpc-proxy/proxy" 15 | "github.com/pkg/errors" 16 | "golang.org/x/net/context" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/codes" 19 | ) 20 | 21 | // New builds a StreamDirector based off a backend pool and a router. 22 | func New(pool backendpool.Pool, adhocRouter common.Addresser, grpcRouter router.Router) proxy.StreamDirector { 23 | return func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) { 24 | beName, err := grpcRouter.Route(ctx, fullMethodName) 25 | 26 | // Try adhoc router if RouteNotFound. 27 | if err == router.ErrRouteNotFound { 28 | ipPort, err := adhocRouter.Address(metautils.ExtractIncoming(ctx).Get(":authority")) 29 | if err != nil { 30 | return ctx, nil, err 31 | } 32 | 33 | var opts []grpc.DialOption 34 | opts = append(opts, 35 | grpc.WithInsecure(), 36 | grpc.WithBlock(), 37 | grpc.WithAuthority(fullMethodName), 38 | grpc.WithCodec(proxy.Codec()), 39 | ) 40 | cc, err := grpc.Dial(ipPort, opts...) 41 | if err != nil { 42 | return ctx, nil, errors.Wrapf(err, "failed to dial to adhoc backend %v", ipPort) 43 | } 44 | 45 | go func() { 46 | <-ctx.Done() 47 | cc.Close() 48 | }() 49 | grpc_ctxtags.Extract(ctx).Set("grpc.proxy.adhoc", ipPort) 50 | return grpcutils.CloneIncomingToOutgoingMD(ctx), cc, err 51 | } 52 | 53 | // Return all other errors. 54 | if err != nil { 55 | return ctx, nil, err 56 | } 57 | 58 | grpc_ctxtags.Extract(ctx).Set("grpc.proxy.backend", beName) 59 | cc, err := pool.Conn(beName) 60 | return grpcutils.CloneIncomingToOutgoingMD(ctx), cc, err 61 | } 62 | } 63 | 64 | // NewGRPCAuthorizer builds a grpc_auth.AuthFunc that checks authorization header from gRPC request. 65 | func NewGRPCAuthorizer(authorizer authorize.Authorizer) grpc_auth.AuthFunc { 66 | return func(ctx context.Context) (context.Context, error) { 67 | token, err := authFromMD(ctx) 68 | if err != nil { 69 | return ctx, err 70 | } 71 | 72 | return ctx, authorizer.IsAuthorized(ctx, token) 73 | } 74 | } 75 | 76 | func authFromMD(ctx context.Context) (string, error) { 77 | val := metautils.ExtractIncoming(ctx).Get("proxy-authorization") 78 | if val == "" { 79 | return "", grpc.Errorf(codes.Unauthenticated, "Request unauthenticated. No proxy-authorization header") 80 | 81 | } 82 | splits := strings.SplitN(val, " ", 2) 83 | if len(splits) != 2 { 84 | return "", grpc.Errorf(codes.Unauthenticated, "Bad authorization string") 85 | } 86 | if strings.ToLower(splits[0]) != "bearer" { 87 | return "", grpc.Errorf(codes.Unauthenticated, "Request unauthenticated. Not bearer type") 88 | } 89 | return splits[1], nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/kedge/http/backendpool/dynamic.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "hash/fnv" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/improbable-eng/kedge/pkg/metrics" 9 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/http/backends" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // dynamic is a Pool to which you can update or remove routes. 14 | type dynamic struct { 15 | mu sync.RWMutex 16 | 17 | backends map[string]*backend 18 | backendFactory func(backend *pb.Backend) (*backend, error) 19 | logger logrus.FieldLogger 20 | } 21 | 22 | func (s *dynamic) Close() { 23 | s.mu.Lock() 24 | defer s.mu.Unlock() 25 | for _, be := range s.backends { 26 | be.Close() 27 | } 28 | } 29 | 30 | // NewDynamic creates a pool with a dynamic allocator 31 | func NewDynamic(logger logrus.FieldLogger) *dynamic { 32 | s := &dynamic{backends: make(map[string]*backend), backendFactory: newBackend, logger: logger} 33 | return s 34 | } 35 | 36 | func (s *dynamic) Tripper(backendName string) (http.RoundTripper, error) { 37 | s.mu.RLock() 38 | defer s.mu.RUnlock() 39 | be, ok := s.backends[backendName] 40 | if !ok { 41 | return nil, ErrUnknownBackend 42 | } 43 | return be.Tripper(), nil 44 | } 45 | 46 | // AddOrUpdate checks tries to perform the least destructive operation of adding a new backend. 47 | // 48 | // If a backend of a given name already exists, and the configuration hasn't changed, no new work will be done. 49 | // If a backend requires changes, the previous one will be removed and closed. 50 | func (s *dynamic) AddOrUpdate(config *pb.Backend, logTestResolution bool) (changed bool, err error) { 51 | s.mu.RLock() 52 | existing, ok := s.backends[config.Name] 53 | s.mu.RUnlock() 54 | 55 | defer func() { 56 | if changed && logTestResolution { 57 | go s.backends[config.Name].LogTestResolution( 58 | s.logger.WithField("protocol", "http").WithField("backend", config.Name), 59 | ) 60 | } 61 | }() 62 | 63 | if !ok { 64 | err = s.addNewBackend(config) 65 | if err != nil { 66 | return changed, err 67 | } 68 | changed = true 69 | s.logger.Infof("Adding new http backend: %v", config.Name) 70 | metrics.BackendHTTPConfigurationCounter.WithLabelValues(config.Name, metrics.ConfiguationActionCreate).Inc() 71 | return changed, nil 72 | } 73 | 74 | var updated bool 75 | updated, err = s.updateBackendWithDiffing(existing, config) 76 | if err != nil { 77 | return changed, err 78 | } 79 | if updated { 80 | changed = true 81 | s.logger.Infof("Updated http backend: %v", config.Name) 82 | metrics.BackendHTTPConfigurationCounter.WithLabelValues(config.Name, metrics.ConfiguationActionChange).Inc() 83 | } 84 | return changed, nil 85 | } 86 | 87 | func (s *dynamic) addNewBackend(config *pb.Backend) error { 88 | be, err := s.backendFactory(config) 89 | if err != nil { 90 | return err 91 | } 92 | s.mu.Lock() 93 | s.backends[config.Name] = be 94 | s.mu.Unlock() 95 | return nil 96 | } 97 | 98 | func (s *dynamic) updateBackendWithDiffing(existing *backend, config *pb.Backend) (changed bool, err error) { 99 | if configsAreTheSame(existing.config, config) { 100 | return false, nil 101 | } 102 | if err := s.addNewBackend(config); err != nil { 103 | return true, err 104 | } 105 | // Make sure we clear up resources. 106 | existing.Close() 107 | return true, nil 108 | } 109 | 110 | // Remove removes and shuts down a previously active backend. 111 | func (s *dynamic) Remove(backendName string) error { 112 | s.mu.RLock() 113 | existing, ok := s.backends[backendName] 114 | s.mu.RUnlock() 115 | if !ok { 116 | return ErrUnknownBackend 117 | } 118 | s.mu.Lock() 119 | delete(s.backends, backendName) 120 | s.mu.Unlock() 121 | existing.Close() 122 | 123 | s.logger.Infof("Removed http backend: %v", backendName) 124 | metrics.BackendHTTPConfigurationCounter.WithLabelValues(backendName, metrics.ConfiguationActionDelete).Inc() 125 | return nil 126 | } 127 | 128 | // Configs returns a map of all active backends and their configuration. 129 | func (s *dynamic) Configs() map[string]*pb.Backend { 130 | ret := make(map[string]*pb.Backend) 131 | s.mu.RLock() 132 | for k, v := range s.backends { 133 | ret[k] = v.config 134 | } 135 | s.mu.RUnlock() 136 | return ret 137 | } 138 | 139 | func configsAreTheSame(c1 *pb.Backend, c2 *pb.Backend) bool { 140 | h1 := fnv.New64a() 141 | h2 := fnv.New64a() 142 | h1.Write([]byte(c1.String())) 143 | h2.Write([]byte(c2.String())) 144 | return h1.Sum64() == h2.Sum64() 145 | } 146 | -------------------------------------------------------------------------------- /pkg/kedge/http/backendpool/dynamic_test.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | pb_resolvers "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 8 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/http/backends" 9 | "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestDynamic_Operations(t *testing.T) { 15 | d := NewDynamic(logrus.New()) 16 | d.backendFactory = func(config *pb.Backend) (*backend, error) { 17 | b := &backend{config: config} 18 | b.ctx, b.cancel = context.WithCancel(context.Background()) 19 | return b, nil 20 | } 21 | assert.Len(t, d.Configs(), 0, "at first there needs to be nothing") 22 | changed, err := d.AddOrUpdate( 23 | &pb.Backend{ 24 | Name: "foobar", DisableConntracking: true, Resolver: &pb.Backend_K8S{ 25 | K8S: &pb_resolvers.K8SResolver{ 26 | DnsPortName: "some:port", 27 | }, 28 | }, 29 | }, 30 | false, 31 | ) 32 | require.NoError(t, err) 33 | assert.True(t, changed) 34 | 35 | changed, err = d.AddOrUpdate(&pb.Backend{Name: "carbar", DisableConntracking: true}, false) 36 | require.NoError(t, err) 37 | assert.True(t, changed) 38 | 39 | assert.Len(t, d.Configs(), 2, "we should have two") 40 | oldCarBar := d.backends["carbar"] 41 | changed, err = d.AddOrUpdate(&pb.Backend{Name: "carbar"}, false) 42 | require.NoError(t, err) 43 | assert.True(t, changed) 44 | assert.Error(t, oldCarBar.ctx.Err(), "oldCarBar should enter closed state") 45 | assert.Len(t, d.Configs(), 2, "we should still two") 46 | 47 | // Add totally the same one. 48 | changed, err = d.AddOrUpdate( 49 | &pb.Backend{ 50 | Name: "foobar", DisableConntracking: true, Resolver: &pb.Backend_K8S{ 51 | K8S: &pb_resolvers.K8SResolver{ 52 | DnsPortName: "some:port", 53 | }, 54 | }, 55 | }, 56 | false, 57 | ) 58 | require.NoError(t, err) 59 | assert.False(t, changed, "We tried to update totally the same backend, it should not change.") 60 | 61 | assert.Error(t, d.Remove("nonexisting"), "removing a non existing backend should return error") 62 | assert.NoError(t, d.Remove("foobar"), "removing a non existing backend should return error") 63 | assert.Len(t, d.Configs(), 1, "we now should have two") 64 | } 65 | -------------------------------------------------------------------------------- /pkg/kedge/http/backendpool/pool.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "net/http" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/codes" 8 | ) 9 | 10 | var ( 11 | ErrUnknownBackend = grpc.Errorf(codes.Unimplemented, "unknown backend") 12 | ) 13 | 14 | type Pool interface { 15 | // Tripper returns an already established http.RoundTripper just for this backend. 16 | Tripper(backendName string) (http.RoundTripper, error) 17 | Close() 18 | } 19 | -------------------------------------------------------------------------------- /pkg/kedge/http/backendpool/static.go: -------------------------------------------------------------------------------- 1 | package backendpool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "net/http" 7 | 8 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/http/backends" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // static is a Pool with a static configuration. 13 | type static struct { 14 | backends map[string]*backend 15 | } 16 | 17 | // NewStatic creates a backend pool that has static configuration. 18 | func NewStatic(backends []*pb.Backend) (*static, error) { 19 | s := &static{backends: make(map[string]*backend)} 20 | for _, beCnf := range backends { 21 | be, err := newBackend(beCnf) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed creating backend '%v': %v", beCnf.Name, err) 24 | } 25 | s.backends[beCnf.Name] = be 26 | } 27 | return s, nil 28 | } 29 | 30 | func (s *static) Tripper(backendName string) (http.RoundTripper, error) { 31 | be, ok := s.backends[backendName] 32 | if !ok { 33 | return nil, ErrUnknownBackend 34 | } 35 | return be.Tripper(), nil 36 | } 37 | 38 | func (s *static) LogTestResolution(logger logrus.FieldLogger) { 39 | for k, backend := range s.backends { 40 | backend.LogTestResolution(logger.WithField("backend", k)) 41 | } 42 | } 43 | 44 | func (s *static) Close() { 45 | for _, backend := range s.backends { 46 | backend.Close() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/kedge/http/client/client.go: -------------------------------------------------------------------------------- 1 | package kedge_http 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/improbable-eng/kedge/pkg/http/tripperware" 8 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 9 | "golang.org/x/net/http2" 10 | ) 11 | 12 | // NewClient constructs new HTTP client that supports proxying by kedge. 13 | // NOTE: No copy of parentTransport is done, so it will modify parent one. 14 | func NewClient(mapper kedge_map.Mapper, clientTls *tls.Config, parentTransport *http.Transport) *http.Client { 15 | if clientTls != nil { 16 | parentTransport.TLSClientConfig = clientTls 17 | if err := http2.ConfigureTransport(parentTransport); err != nil { 18 | panic(err) // this should never happen, but let's not lose an error. 19 | } 20 | } 21 | return &http.Client{ 22 | Transport: tripperware.WrapForMapping(mapper, 23 | tripperware.WrapForRouting( 24 | tripperware.DefaultWithTransport(parentTransport, clientTls), 25 | ), 26 | ), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/kedge/http/director/adhoc/adhoc.go: -------------------------------------------------------------------------------- 1 | package adhoc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/improbable-eng/kedge/pkg/kedge/common" 10 | "github.com/improbable-eng/kedge/pkg/kedge/http/director/router" 11 | kedge_config_common "github.com/improbable-eng/kedge/protogen/kedge/config/common" 12 | ) 13 | 14 | type static struct { 15 | rules []*kedge_config_common.Adhoc 16 | } 17 | 18 | func NewStaticAddresser(rules []*kedge_config_common.Adhoc) *static { 19 | return &static{rules: rules} 20 | } 21 | 22 | func (a *static) Address(hostPort string) (string, error) { 23 | hostName, port, err := common.ExtractHostPort(hostPort) 24 | if err != nil { 25 | return "", router.NewError(http.StatusBadRequest, fmt.Sprintf("adhoc: malformed port number: %v", err)) 26 | } 27 | for _, rule := range a.rules { 28 | if !common.HostMatches(hostName, rule.DnsNameMatcher) { 29 | continue 30 | } 31 | portForRule := port 32 | if port == 0 { 33 | if defPort := rule.Port.Default; defPort != 0 { 34 | portForRule = int(defPort) 35 | } else { 36 | portForRule = 80 37 | } 38 | } 39 | if !common.PortAllowed(portForRule, rule.Port) { 40 | return "", router.NewError(http.StatusBadRequest, fmt.Sprintf("adhoc: port %d is not allowed", portForRule)) 41 | } 42 | 43 | ipAddr, err := common.AdhocResolveHost(hostName, rule.DnsNameReplace) 44 | if err != nil { 45 | return "", router.NewError(http.StatusBadGateway, fmt.Sprintf("adhoc: cannot resolve %s host: %v", hostPort, err)) 46 | } 47 | return net.JoinHostPort(ipAddr, strconv.FormatInt(int64(portForRule), 10)), nil 48 | 49 | } 50 | return "", router.ErrRouteNotFound 51 | } 52 | -------------------------------------------------------------------------------- /pkg/kedge/http/director/proxyreq/request.go: -------------------------------------------------------------------------------- 1 | package proxyreq 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type ProxyMode int 10 | 11 | const ( 12 | MODE_FORWARD_PROXY ProxyMode = iota 13 | MODE_REVERSE_PROXY 14 | ) 15 | 16 | var ( 17 | typeMarker = "proxy_mode_marker" 18 | ) 19 | 20 | // NormalizeInboundRequest makes sure that the request received by the proxy has the destination inside URL.Host. 21 | func NormalizeInboundRequest(r *http.Request) *http.Request { 22 | t := unnormalizedRequestMode(r) 23 | reqCopy := r.WithContext(context.WithValue(r.Context(), typeMarker, t)) 24 | if t == MODE_REVERSE_PROXY { 25 | reqCopy.URL.Host = reqCopy.Host 26 | } 27 | return reqCopy 28 | } 29 | 30 | func unnormalizedRequestMode(r *http.Request) ProxyMode { 31 | if r.Method != "CONNECT" && strings.HasPrefix(r.RequestURI, "http") { 32 | // Forward Proxy requests embed the host information of the destination inside the RequestURI. 33 | return MODE_FORWARD_PROXY 34 | } else { 35 | return MODE_REVERSE_PROXY 36 | } 37 | } 38 | 39 | func GetProxyMode(r *http.Request) ProxyMode { 40 | v, ok := r.Context().Value(typeMarker).(ProxyMode) 41 | if ok { 42 | return v 43 | } 44 | return unnormalizedRequestMode(r) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/kedge/http/director/router/error.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | type Error struct { 4 | msg string 5 | status int 6 | } 7 | 8 | func NewError(status int, msg string) *Error { 9 | return &Error{msg: msg, status: status} 10 | } 11 | 12 | func (e *Error) Error() string { 13 | return e.msg 14 | } 15 | 16 | func (e *Error) StatusCode() int { 17 | return e.status 18 | } 19 | -------------------------------------------------------------------------------- /pkg/kedge/http/director/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/improbable-eng/kedge/pkg/kedge/http/director/proxyreq" 12 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/http/routes" 13 | "google.golang.org/grpc/metadata" 14 | ) 15 | 16 | var ( 17 | emptyMd = metadata.Pairs() 18 | ErrRouteNotFound = errors.New("unknown route to service") 19 | ) 20 | 21 | type Router interface { 22 | // Route returns a backend name for a given call, or an error. 23 | // Note: the request *must* be normalized. 24 | Route(req *http.Request) (backendName string, err error) 25 | } 26 | 27 | type dynamic struct { 28 | mu sync.RWMutex 29 | staticRouter *static 30 | } 31 | 32 | // NewDynamic creates a new dynamic router that can be have its routes updated. 33 | func NewDynamic() *dynamic { 34 | return &dynamic{staticRouter: NewStatic([]*pb.Route{})} 35 | } 36 | 37 | func (d *dynamic) Route(req *http.Request) (backendName string, err error) { 38 | d.mu.RLock() 39 | staticRouter := d.staticRouter 40 | d.mu.RUnlock() 41 | return staticRouter.Route(req) 42 | } 43 | 44 | // Update sets the routing table to the provided set of routes. 45 | func (d *dynamic) Update(routes []*pb.Route) { 46 | staticRouter := NewStatic(routes) 47 | d.mu.Lock() 48 | d.staticRouter = staticRouter 49 | d.mu.Unlock() 50 | } 51 | 52 | type static struct { 53 | routes []*pb.Route 54 | } 55 | 56 | func NewStatic(routes []*pb.Route) *static { 57 | return &static{routes: routes} 58 | } 59 | 60 | func (r *static) Route(req *http.Request) (backendName string, err error) { 61 | port := req.URL.Port() 62 | if port == "" { 63 | switch strings.ToLower(req.URL.Scheme) { 64 | case "", "http": 65 | port = "80" 66 | case "https": 67 | port = "443" 68 | } 69 | } 70 | 71 | for _, route := range r.routes { 72 | if !r.urlMatches(req.URL, route.PathRules) { 73 | continue 74 | } 75 | if !r.hostMatches(req.URL.Hostname(), route.HostMatcher) { 76 | continue 77 | } 78 | if !r.portMatches(port, route.PortMatcher) { 79 | continue 80 | } 81 | if !r.headersMatch(req.Header, route.HeaderMatcher) { 82 | continue 83 | } 84 | if !r.requestTypeMatch(proxyreq.GetProxyMode(req), route.ProxyMode) { 85 | continue 86 | } 87 | return route.BackendName, nil 88 | } 89 | return "", ErrRouteNotFound 90 | } 91 | 92 | func (r *static) urlMatches(u *url.URL, matchers []string) bool { 93 | if len(matchers) == 0 { 94 | return true 95 | } 96 | for _, m := range matchers { 97 | if m == "" { 98 | continue 99 | } 100 | if m[len(m)-1] == '*' { 101 | if strings.HasPrefix(u.Path, m[0:len(m)-1]) { 102 | return true 103 | } 104 | } 105 | if m == u.Path { 106 | return true 107 | } 108 | } 109 | return false 110 | } 111 | 112 | func (r *static) hostMatches(host string, matcher string) bool { 113 | if host == "" { 114 | return false // we can't handle empty hosts 115 | } 116 | if matcher == "" { 117 | return true // no matcher set, match all like a boss! 118 | } 119 | return host == matcher 120 | } 121 | 122 | func (r *static) portMatches(port string, matcher uint32) bool { 123 | if matcher == 0 { 124 | return true // no matcher set, match all like a boss! 125 | } 126 | 127 | if port == "" { 128 | return false // we expect certain port. 129 | } 130 | 131 | return port == fmt.Sprintf("%v", matcher) 132 | } 133 | 134 | func (r *static) headersMatch(header http.Header, expectedKv map[string]string) bool { 135 | for expK, expV := range expectedKv { 136 | headerVal := header.Get(expK) 137 | if headerVal == "" { 138 | return false // key doesn't exist 139 | } 140 | if headerVal != expV { 141 | return false 142 | } 143 | } 144 | return true 145 | } 146 | 147 | func (r *static) requestTypeMatch(requestMode proxyreq.ProxyMode, routeMode pb.ProxyMode) bool { 148 | if routeMode == pb.ProxyMode_ANY { 149 | return true 150 | } 151 | if requestMode == proxyreq.MODE_FORWARD_PROXY && routeMode == pb.ProxyMode_FORWARD_PROXY { 152 | return true 153 | } 154 | if requestMode == proxyreq.MODE_REVERSE_PROXY && routeMode == pb.ProxyMode_REVERSE_PROXY { 155 | return true 156 | } 157 | return false 158 | } 159 | -------------------------------------------------------------------------------- /pkg/kedge/http/integration.go: -------------------------------------------------------------------------------- 1 | package http_integration 2 | 3 | /** Go doesn't allow you to have a single package with a test without a test. */ 4 | -------------------------------------------------------------------------------- /pkg/kedge/http/lbtransport/body.go: -------------------------------------------------------------------------------- 1 | package lbtransport 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type replayableReader struct { 9 | wrapped io.Reader 10 | buf []byte 11 | offset int 12 | } 13 | 14 | func (*replayableReader) Close() error { 15 | return nil 16 | } 17 | 18 | // rewind allows replayableReader to be read again. 19 | func (b *replayableReader) rewind() { 20 | if b == nil { 21 | return 22 | } 23 | b.offset = 0 24 | } 25 | 26 | func (b *replayableReader) Read(p []byte) (n int, err error) { 27 | if b == nil { 28 | return 0, io.EOF 29 | } 30 | 31 | if len(b.buf)-b.offset > 0 { 32 | n, err = bytes.NewReader(b.buf[b.offset:]).Read(p) 33 | b.offset += n 34 | } 35 | 36 | if err == nil && n < len(p) { 37 | var n64 int64 38 | 39 | // Try to buffer rest (if needed) from wrapped io.Reader. 40 | tmp := bytes.NewBuffer(b.buf) 41 | n64, err = tmp.ReadFrom(io.LimitReader(b.wrapped, int64(len(p)-n))) 42 | b.buf = tmp.Bytes() 43 | if n64 > 0 { 44 | copy(p[n:], b.buf[b.offset:]) 45 | n += int(n64) 46 | b.offset += int(n64) 47 | } 48 | } 49 | 50 | // Buffer.ReadFrom masks io.EOF so we assume EOF once n == 0 and no error. 51 | if err == nil && n == 0 && len(p) > 0 { 52 | return 0, io.EOF 53 | } 54 | return n, err 55 | } 56 | 57 | // newReplayableReader returns replayableReader. 58 | // The content read from the source is buffered in a lazy fashion to keep storage requirements 59 | // limited to a minimum while still allowing for the reader to be rewinded and previously read 60 | // content to be replayed. 61 | func newReplayableReader(src io.Reader) *replayableReader { 62 | if src == nil { 63 | return nil 64 | } 65 | return &replayableReader{wrapped: src} 66 | } 67 | -------------------------------------------------------------------------------- /pkg/kedge/http/lbtransport/body_test.go: -------------------------------------------------------------------------------- 1 | package lbtransport 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestReplayableReader(t *testing.T) { 12 | for _, tcase := range []struct { 13 | name string 14 | src io.Reader 15 | sequentialReadBytes []int 16 | rewindBeforeRead []bool 17 | 18 | expectedBytes [][]byte 19 | expectedErrs []error 20 | }{ 21 | { 22 | name: "WrappedNil_Read_ShouldReturnEOF", 23 | src: nil, 24 | sequentialReadBytes: []int{10}, 25 | rewindBeforeRead: []bool{false}, 26 | 27 | expectedBytes: [][]byte{{}}, 28 | expectedErrs: []error{io.EOF}, 29 | }, 30 | { 31 | name: "WrappedNil_RewindRead_ShouldReturnEOF", 32 | src: nil, 33 | sequentialReadBytes: []int{10}, 34 | rewindBeforeRead: []bool{true}, 35 | 36 | expectedBytes: [][]byte{{}}, 37 | expectedErrs: []error{io.EOF}, 38 | }, 39 | { 40 | name: "SmallBigBigReads_FinishedWithEOF", 41 | src: bytes.NewReader([]byte{1, 2, 3, 4}), 42 | sequentialReadBytes: []int{1, 8192, 8192}, 43 | rewindBeforeRead: []bool{false, false, false}, 44 | 45 | expectedBytes: [][]byte{{1}, {2, 3, 4}, {}}, 46 | expectedErrs: []error{nil, nil, io.EOF}, 47 | }, 48 | { 49 | name: "SmallReads_FinishedWithEOF", 50 | src: bytes.NewReader([]byte{1, 2, 3, 4}), 51 | sequentialReadBytes: []int{1, 2, 4, 1}, 52 | rewindBeforeRead: []bool{false, false, false, false}, 53 | 54 | expectedBytes: [][]byte{{1}, {2, 3}, {4}, {}}, 55 | expectedErrs: []error{nil, nil, nil, io.EOF}, 56 | }, 57 | { 58 | name: "SmallReadsTakingExactBytes", 59 | src: bytes.NewReader([]byte{1, 2, 3, 4, 5}), 60 | sequentialReadBytes: []int{1, 2, 2}, 61 | rewindBeforeRead: []bool{false, false, false}, 62 | 63 | expectedBytes: [][]byte{{1}, {2, 3}, {4, 5}}, 64 | expectedErrs: []error{nil, nil, nil}, 65 | }, 66 | { 67 | name: "SmallReadsRewindSmallRead", 68 | src: bytes.NewReader([]byte{1, 2, 3, 4, 5}), 69 | sequentialReadBytes: []int{1, 2, 4, 2}, 70 | rewindBeforeRead: []bool{false, false, true, false}, 71 | 72 | expectedBytes: [][]byte{{1}, {2, 3}, {1, 2, 3, 4}, {5}}, 73 | expectedErrs: []error{nil, nil, nil, nil}, 74 | }, 75 | { 76 | name: "BigReadRewindSmallReads", 77 | src: bytes.NewReader([]byte{1, 2, 3, 4}), 78 | sequentialReadBytes: []int{8192, 2, 3}, 79 | rewindBeforeRead: []bool{false, true, false}, 80 | 81 | expectedBytes: [][]byte{{1, 2, 3, 4}, {1, 2}, {3, 4}}, 82 | expectedErrs: []error{nil, nil, nil}, 83 | }, 84 | { 85 | name: "BigReadRewindBigReadSmall_FinishedWithEOF", 86 | src: bytes.NewReader([]byte{1, 2, 3, 4}), 87 | sequentialReadBytes: []int{8192, 8192, 3}, 88 | rewindBeforeRead: []bool{false, true, false}, 89 | 90 | expectedBytes: [][]byte{{1, 2, 3, 4}, {1, 2, 3, 4}, {}}, 91 | expectedErrs: []error{nil, nil, io.EOF}, 92 | }, 93 | } { 94 | if ok := t.Run(tcase.name, func(t *testing.T) { 95 | b := newReplayableReader(tcase.src) 96 | 97 | for i, read := range tcase.sequentialReadBytes { 98 | if tcase.rewindBeforeRead[i] { 99 | b.rewind() 100 | } 101 | toRead := make([]byte, read) 102 | 103 | n, err := b.Read(toRead) 104 | require.Equal(t, tcase.expectedErrs[i], err, "read %d", i+1) 105 | require.Len(t, tcase.expectedBytes[i], n, "read %d", i+1) 106 | require.Equal(t, tcase.expectedBytes[i], toRead[:len(tcase.expectedBytes[i])], "read %d", i+1) 107 | } 108 | 109 | }); !ok { 110 | return 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/logstash/conn.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/jpillora/backoff" 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type dialFunc func() (net.Conn, error) 13 | 14 | // ReconnectingWriter wraps a net.Conn such that it will never fail to write and will attempted to reestablish 15 | // the connection whenever an error occurs. 16 | type ReconnectingWriter struct { 17 | underlyingConn net.Conn 18 | dial dialFunc 19 | backoff *backoff.Backoff 20 | errLogger logrus.FieldLogger 21 | } 22 | 23 | func NewReconnectingWriter(dial dialFunc, errLogger logrus.FieldLogger) *ReconnectingWriter { 24 | return &ReconnectingWriter{ 25 | dial: dial, 26 | errLogger: errLogger, 27 | backoff: &backoff.Backoff{ 28 | Factor: 2, 29 | Min: 50 * time.Millisecond, 30 | Max: 2 * time.Second, 31 | }, 32 | } 33 | } 34 | 35 | // Write will run the following retry all of the following in a loop until it manages one successful write: 36 | // * attempt to re-establish a connection if a connection is not available 37 | // * attempt to write the given bytes if a connection is available and exit if it succeeds 38 | // * reset the underlying connection if write fails 39 | // 40 | // Deadline is set for all writes as specified in the flag. 41 | func (c *ReconnectingWriter) Write(b []byte) (int, error) { 42 | for { 43 | n, err := c.send(b) 44 | if err != nil { 45 | c.errLogger.WithError(err).Warnf("failed to write") 46 | time.Sleep(c.backoff.Duration()) 47 | continue 48 | } 49 | return n, nil 50 | } 51 | } 52 | 53 | func (c *ReconnectingWriter) send(b []byte) (int, error) { 54 | if c.underlyingConn == nil { 55 | conn, err := c.dial() 56 | if err != nil { 57 | return 0, errors.Wrap(err, "attempted to reestablish connection but failed") 58 | } 59 | c.underlyingConn = conn 60 | } 61 | 62 | err := c.underlyingConn.SetWriteDeadline(time.Now().Add(*flagLogstashWriteTimeout)) 63 | if err != nil { 64 | c.resetConn() 65 | return 0, errors.Wrap(err, "could not set deadline for write") 66 | } 67 | n, err := c.underlyingConn.Write(b) 68 | if err != nil { 69 | c.resetConn() 70 | return 0, errors.Wrap(err, "could not write to external connection") 71 | } 72 | return n, nil 73 | } 74 | 75 | func (c *ReconnectingWriter) resetConn() { 76 | c.underlyingConn.Close() 77 | c.underlyingConn = nil 78 | c.backoff.Reset() 79 | } 80 | -------------------------------------------------------------------------------- /pkg/logstash/conn_test.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus/hooks/test" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestReconnectingWriter_WriteDoesNotReturnUntilWriteIsSuccessful(t *testing.T) { 14 | writerWithError := &mockWriterCloser{errOnWrite: errors.New("some expected error")} 15 | writerWithoutError := &mockWriterCloser{} 16 | dialReturns := []net.Conn{ 17 | writerWithError, 18 | writerWithoutError, 19 | } 20 | callCount := 0 21 | 22 | dial := func() (net.Conn, error) { 23 | if callCount >= len(dialReturns) { 24 | return nil, errors.New("called more than call count") 25 | } 26 | ret := dialReturns[callCount] 27 | callCount++ 28 | return ret, nil 29 | } 30 | 31 | errLogger, _ := test.NewNullLogger() 32 | writer := NewReconnectingWriter(dial, errLogger) 33 | 34 | var byteCount int 35 | var err error 36 | writeDone := make(chan struct{}) 37 | body := []byte("some random data") 38 | go func() { 39 | byteCount, err = writer.Write(body) 40 | close(writeDone) 41 | }() 42 | 43 | select { 44 | case <-time.After(3 * time.Second): 45 | t.Fatalf("timed out waiting for write to complete") 46 | case <-writeDone: 47 | } 48 | 49 | assert.Equal(t, 1, writerWithError.writeCallCount) 50 | assert.Equal(t, 1, writerWithoutError.writeCallCount) 51 | // verifies that the write is completed using the new writer 52 | assert.Equal(t, len(body), byteCount) 53 | assert.NoError(t, err, "must have successfully written a log after redial") 54 | } 55 | 56 | type mockWriterCloser struct { 57 | writeCallCount int 58 | errOnWrite error 59 | errOnClose error 60 | } 61 | 62 | func (w *mockWriterCloser) Write(body []byte) (int, error) { 63 | w.writeCallCount += 1 64 | if w.errOnWrite != nil { 65 | return 0, w.errOnWrite 66 | } 67 | return len(body), nil 68 | } 69 | 70 | func (w *mockWriterCloser) Close() error { 71 | return w.errOnClose 72 | } 73 | 74 | func (w *mockWriterCloser) Read(b []byte) (n int, err error) { 75 | panic("implement me") 76 | } 77 | 78 | func (w *mockWriterCloser) LocalAddr() net.Addr { 79 | panic("implement me") 80 | } 81 | 82 | func (w *mockWriterCloser) RemoteAddr() net.Addr { 83 | panic("implement me") 84 | } 85 | 86 | func (w *mockWriterCloser) SetDeadline(t time.Time) error { 87 | panic("implement me") 88 | } 89 | 90 | func (w *mockWriterCloser) SetReadDeadline(t time.Time) error { 91 | panic("implement me") 92 | } 93 | 94 | func (w *mockWriterCloser) SetWriteDeadline(t time.Time) error { 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/logstash/logstash.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | 8 | "github.com/improbable-eng/kedge/pkg/sharedflags" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | flagLogstashWriteTimeout = sharedflags.Set.Duration( 15 | "logstash_write_timeout", 2*time.Second, 16 | "Time to wait for a successfull write to logstash before dropping a log") 17 | flagLogstashWriteBufferSize = sharedflags.Set.Int( 18 | "logstash_write_buffer_size", 5000, 19 | "Size of the buffer for log entries for the async writer", 20 | ) 21 | ) 22 | 23 | // Hook sends logs to Logstash. 24 | type Hook struct { 25 | hostPort string 26 | conn io.Writer 27 | buffer chan *logrus.Entry 28 | errLogger *logrus.Entry 29 | formatter logrus.Formatter 30 | } 31 | 32 | // newHook creates a hook to be added to an instance of logger. It connects 33 | // to Logstash on host via TCP. 34 | func NewHook(hostPort string, formatter logrus.Formatter) (*Hook, error) { 35 | dial := func() (net.Conn, error) { 36 | conn, err := net.DialTimeout("tcp", hostPort, 2*time.Second) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "Failed to establish logstash connection.") 39 | } 40 | 41 | return conn, nil 42 | } 43 | 44 | // We ignore error because we will try to reconnect inside Fire() if there is no connection. 45 | errLogger := logrus.New().WithField("system", "logstash") 46 | hook := &Hook{ 47 | hostPort: hostPort, 48 | conn: NewReconnectingWriter(dial, errLogger), 49 | buffer: make(chan *logrus.Entry, *flagLogstashWriteBufferSize), 50 | errLogger: errLogger, 51 | formatter: formatter, 52 | } 53 | 54 | go hook.start() 55 | 56 | return hook, nil 57 | } 58 | 59 | // Fire implements the Fire method from the Hook interface. It writes to its local buffer for the Entry to be sent 60 | // off asynchronously. If the buffer is full, it will drop the entry. 61 | func (hook *Hook) Fire(entry *logrus.Entry) error { 62 | reportEntry(entry.Level) 63 | select { 64 | case hook.buffer <- entry: 65 | default: 66 | hook.errLogger.Error("Failed to write log message due to full buffer.") 67 | reportDropped(entry.Level, BufferFull) 68 | } 69 | return nil 70 | } 71 | 72 | // Levels returns all logrus levels. 73 | func (hook Hook) Levels() []logrus.Level { 74 | return logrus.AllLevels 75 | } 76 | 77 | func (hook *Hook) start() { 78 | for entry := range hook.buffer { 79 | payload, err := hook.formatter.Format((*logrus.Entry)(entry)) 80 | if err != nil { 81 | hook.errLogger.WithError(err).Errorf("Failed to write log message due to bad format: %v", err) 82 | reportDropped(entry.Level, BadFormat) 83 | continue 84 | } 85 | _, err = hook.conn.Write([]byte(payload)) 86 | if err != nil { 87 | reportDropped(entry.Level, FailedToWrite) 88 | hook.errLogger.WithError(err).Errorf("Failed to write log message to logstash due to connection issues: %v", err) 89 | continue 90 | } 91 | reportRemoteSuccess(entry.Level) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/logstash/logstash_formatter.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/improbable-eng/kedge/pkg/sharedflags" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const ( 14 | errorKey = "error" 15 | errorStackKey = "stack" 16 | 17 | // defaultTimestampFormat is the time format with milliseconds. 18 | defaultTimestampFormat string = "2006-01-02T15:04:05.999Z07:00" 19 | 20 | // maxMessageBodySize is the max length of the log message before it is trimmed. 21 | maxMessageBodySize int = 1 << 14 22 | ) 23 | 24 | var ( 25 | TimestampFormat string = defaultTimestampFormat 26 | flagLogstashLogTags = sharedflags.Set.StringSlice("logstash_log_tags", []string{"kedge"}, 27 | "@tags field to inject to while formatting log message for logstash.") 28 | ) 29 | 30 | func NewFormatter() (*logstashFormatter, error) { 31 | hostname, err := os.Hostname() 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &logstashFormatter{ 36 | Hostname: hostname, 37 | }, nil 38 | } 39 | 40 | // logstashFormatter generates json in logstash format. 41 | // See the Logstash website http://logstash.net/ 42 | type logstashFormatter struct { 43 | Hostname string 44 | } 45 | 46 | func trimMessage(message string) string { 47 | if len(message) > maxMessageBodySize { 48 | message = message[:maxMessageBodySize] + " ... (message has been trimmed due to extreme length)" 49 | } 50 | return message 51 | } 52 | 53 | // We can not modify entry.Data as it is shared between multiple threads 54 | func (f *logstashFormatter) Format(entry *logrus.Entry) ([]byte, error) { 55 | dataCopy := map[string]interface{}{} 56 | for key, val := range entry.Data { 57 | // Sanitize key. Logstash does not allow certain chars in key e.g '.' 58 | newKey := strings.Replace(key, ".", "_", -1) 59 | dataCopy[newKey] = val 60 | } 61 | dataCopy["@version"] = 1 62 | dataCopy["HOSTNAME"] = f.Hostname 63 | dataCopy["@timestamp"] = entry.Time.UTC().Format(TimestampFormat) 64 | 65 | // set message field 66 | message := entry.Message 67 | if dataCopy[errorKey] != nil { 68 | message = fmt.Sprintf("%v: %v", message, dataCopy["error"]) 69 | } 70 | dataCopy["message"] = trimMessage(message) 71 | 72 | if len(*flagLogstashLogTags) > 0 { 73 | dataCopy["@tags"] = *flagLogstashLogTags 74 | } 75 | 76 | // Error is also a candidate for being large 77 | if dataCopy[errorKey] != nil { 78 | dataCopy[errorKey] = trimMessage(fmt.Sprintf("%v", dataCopy[errorKey])) 79 | } 80 | if dataCopy[errorStackKey] != nil { 81 | dataCopy[errorStackKey] = trimMessage(fmt.Sprintf("%v", dataCopy[errorStackKey])) 82 | } 83 | 84 | level := strings.ToUpper(entry.Level.String()) 85 | // We assume that warning has level WARN inside kibana. 86 | if level == "WARNING" { 87 | level = "WARN" 88 | } 89 | dataCopy["level"] = level 90 | 91 | serialized, err := json.Marshal(dataCopy) 92 | if err != nil { 93 | return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) 94 | } 95 | return append(serialized, '\n'), nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/logstash/reporter.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | const ( 9 | namespace = "kedge" 10 | subsystem = "logging" 11 | ) 12 | 13 | var ( 14 | loggedCounter = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Namespace: namespace, 17 | Subsystem: subsystem, 18 | Name: "entries_total", 19 | Help: "Number of all entries incoming to the logger by log level.", 20 | }, []string{"level"}) 21 | 22 | remoteCounter = prometheus.NewCounterVec( 23 | prometheus.CounterOpts{ 24 | Namespace: namespace, 25 | Subsystem: subsystem, 26 | Name: "remote_entries_total", 27 | Help: "Number of successfully logged remote entries by log level.", 28 | }, []string{"level"}) 29 | 30 | droppedCounter = prometheus.NewCounterVec( 31 | prometheus.CounterOpts{ 32 | Namespace: namespace, 33 | Subsystem: subsystem, 34 | Name: "dropped_entries_total", 35 | Help: "Number of dropped entries by log level.", 36 | }, []string{"level", "reason"}) 37 | ) 38 | 39 | // dropReason for logs being dropped. 40 | type dropReason string 41 | 42 | const ( 43 | BufferFull dropReason = "buffer_full" 44 | FailedToWrite dropReason = "failed_to_write" 45 | BadFormat dropReason = "bad_format" 46 | ) 47 | 48 | func init() { 49 | prometheus.MustRegister(loggedCounter) 50 | prometheus.MustRegister(remoteCounter) 51 | prometheus.MustRegister(droppedCounter) 52 | registerAllLabels() 53 | } 54 | 55 | func registerAllLabels() { 56 | for _, level := range logrus.AllLevels { 57 | loggedCounter.WithLabelValues(level.String()) 58 | remoteCounter.WithLabelValues(level.String()) 59 | droppedCounter.WithLabelValues(level.String(), string(BufferFull)) 60 | droppedCounter.WithLabelValues(level.String(), string(FailedToWrite)) 61 | droppedCounter.WithLabelValues(level.String(), string(BadFormat)) 62 | } 63 | } 64 | 65 | func reportEntry(level logrus.Level) { 66 | loggedCounter.WithLabelValues(level.String()).Inc() 67 | } 68 | 69 | func reportRemoteSuccess(level logrus.Level) { 70 | remoteCounter.WithLabelValues(level.String()).Inc() 71 | } 72 | 73 | func reportDropped(level logrus.Level, r dropReason) { 74 | droppedCounter.WithLabelValues(level.String(), string(r)).Inc() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/map/iface.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/improbable-eng/kedge/pkg/tokenauth" 7 | ) 8 | 9 | // Mapper is an interface that allows you to direct traffic to different kedges including various auth. 10 | // These are used by client libraries. 11 | type Mapper interface { 12 | // Map maps a target's DNS name and optional port (e.g. myservice.prod.ext.europe-cluster.local and 80) to a Route. 13 | // If the targets shouldn't be proxied, ErrNotKedgeDestination should be returned. 14 | Map(targetAuthorityDnsName string, port string) (*Route, error) 15 | } 16 | 17 | type Route struct { 18 | // URL specifies public URL to the Kedge fronting route destination. 19 | // The returned Scheme is deciding whether the Kedge connection is secure. 20 | URL *url.URL 21 | 22 | // BackendAuth represents optional auth for end application. Sometimes it is required to be injected here, because of common 23 | // restriction blocking auth headers in plain HTTP requests (even when communication locally with local forward proxy). 24 | BackendAuth tokenauth.Source 25 | // ProxyAuth represents optional auth for kedge. 26 | ProxyAuth tokenauth.Source 27 | } 28 | 29 | // ErrorKedgeDestination displays that host/port is not a kedge destination. We are not using errors.New, because sometimes we need 30 | // to check for type of the error (for example in winch/grpc/proxy.go). 31 | type ErrorNotKedgeDestination struct { 32 | host string 33 | port string 34 | } 35 | 36 | func NotKedgeDestinationErr(host string, port string) error { 37 | return ErrorNotKedgeDestination{host: host, port: port} 38 | } 39 | 40 | func (e ErrorNotKedgeDestination) Error() string { 41 | msg := e.host 42 | if e.port != "" { 43 | msg += ":" + e.port 44 | } 45 | return msg + " is not a kedge destination" 46 | } 47 | 48 | func IsNotKedgeDestinationError(err error) bool { 49 | _, ok := err.(ErrorNotKedgeDestination) 50 | return ok 51 | } 52 | -------------------------------------------------------------------------------- /pkg/map/route.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type RouteGetter interface { 11 | Route(hostPort string) (*Route, bool, error) 12 | } 13 | 14 | var ipMatchRegexp = regexp.MustCompile(`^\d+\.\d+\.\d+\.\d+$`) 15 | 16 | type routeMapper struct { 17 | routes []RouteGetter 18 | } 19 | 20 | // RouteMapper is a mapper that resolves Route based on given DNS. 21 | func RouteMapper(r []RouteGetter) *routeMapper { 22 | return &routeMapper{ 23 | routes: r, 24 | } 25 | } 26 | 27 | func (m *routeMapper) Map(targetDnsName string, targetPort string) (*Route, error) { 28 | if ipMatchRegexp.MatchString(targetDnsName) { 29 | return nil, errors.Errorf("kedge requires hostname to proxy to but was given IP address %s:%s instead", targetDnsName, targetPort) 30 | } 31 | 32 | hostPort := targetDnsName 33 | if targetPort != "" { 34 | hostPort = fmt.Sprintf("%s:%s", targetDnsName, targetPort) 35 | } 36 | for _, route := range m.routes { 37 | r, ok, err := route.Route(hostPort) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if !ok { 43 | continue 44 | } 45 | 46 | return r, nil 47 | } 48 | 49 | return nil, NotKedgeDestinationErr(targetDnsName, targetPort) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/map/route_test.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIPMatch(t *testing.T) { 10 | rm := RouteMapper(nil) 11 | // IP address. 12 | _, err := rm.Map("1.2.30.44", "12") 13 | assert.Error(t, err, "route to IP should fail") 14 | assert.Contains(t, err.Error(), "was given IP address") 15 | 16 | // Not an IP address error, but is not a kedge destination. 17 | _, err = rm.Map("1.2.30.44abc", "12") 18 | assert.Error(t, err) 19 | assert.True(t, IsNotKedgeDestinationError(err)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/map/simple.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import "fmt" 4 | 5 | type simpleHost struct { 6 | mapping map[string]*Route 7 | } 8 | 9 | // SimpleHost is a kedge mapper that returns route based on map provided in constructor. It does not care about port. 10 | func SimpleHost(mapping map[string]*Route) Mapper { 11 | return &simpleHost{mapping: mapping} 12 | } 13 | 14 | func (s *simpleHost) Map(targetAuthorityDnsName string, _ string) (*Route, error) { 15 | r, ok := s.mapping[targetAuthorityDnsName] 16 | if !ok { 17 | return nil, NotKedgeDestinationErr(targetAuthorityDnsName, "") 18 | } 19 | 20 | return r, nil 21 | } 22 | 23 | type simpleHostPort struct { 24 | mapping map[string]*Route 25 | } 26 | 27 | // SimpleHostPort is a kedge mapper that returns route based on map provided in constructor. 28 | func SimpleHostPort(mapping map[string]*Route) Mapper { 29 | return &simpleHostPort{mapping: mapping} 30 | } 31 | 32 | func (s *simpleHostPort) Map(targetAuthorityDnsName string, port string) (*Route, error) { 33 | r, ok := s.mapping[fmt.Sprintf("%s:%s", targetAuthorityDnsName, port)] 34 | if !ok { 35 | return nil, NotKedgeDestinationErr(targetAuthorityDnsName, port) 36 | } 37 | 38 | return r, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/map/single.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/improbable-eng/kedge/pkg/tokenauth" 7 | ) 8 | 9 | type single struct { 10 | route *Route 11 | } 12 | 13 | // Single is a simplistic kedge mapper that forwards all traffic through the same kedge. 14 | // No auth is involved. 15 | func Single(kedgeUrl *url.URL) Mapper { 16 | return &single{route: &Route{URL: kedgeUrl}} 17 | } 18 | 19 | // SingleWithProxyAuth is kedge mapper that always returns predefined route with given Proxy auth and no 20 | // backend auth. Used for loadtest. 21 | func SingleWithProxyAuth(kedgeUrl *url.URL, proxyAuth tokenauth.Source) Mapper { 22 | return &single{route: &Route{URL: kedgeUrl, ProxyAuth: proxyAuth}} 23 | } 24 | 25 | func (s *single) Map(_ string, _ string) (*Route, error) { 26 | r := *s.route 27 | return &r, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/map/suffix.go: -------------------------------------------------------------------------------- 1 | package kedge_map 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | type suffixMapper struct { 10 | reverseMatchParts []string 11 | 12 | suffix string 13 | scheme string 14 | } 15 | 16 | // Suffix is a `kedge.Mapper` that copies over wildcarded parts of a URL. 17 | // 18 | // For example: given a matchPattern of *.*.clusters.local, and a URL of `myservice.mynamespace.svc.us1.prod.clusters.local` 19 | // and a suffixMapper of `.clusters.example.com`, it will return a kedge url of `us1.prod.clusters.example.com`. 20 | // 21 | // NOTE: It does not care about port. 22 | // 23 | // Scheme needs to be `http` or `https` 24 | func Suffix(matchPattern string, suffix string, scheme string) (Mapper, error) { 25 | if !strings.HasPrefix(suffix, ".") { 26 | return nil, errors.New("suffixMapper needs to start with a dot") 27 | } 28 | if !strings.HasPrefix(matchPattern, "*.") { 29 | return nil, errors.New("matchPattern must start with a wildcard match *.") 30 | } 31 | if scheme != "http" && scheme != "https" { 32 | return nil, errors.New("scheme must be http or https") 33 | } 34 | return &suffixMapper{ 35 | reverseMatchParts: reverse(strings.Split(matchPattern, ".")), 36 | suffix: suffix, 37 | scheme: scheme, 38 | }, nil 39 | } 40 | 41 | func (s *suffixMapper) Map(targetAuthorityDnsName string, _ string) (*Route, error) { 42 | reverseTargetParts := reverse(strings.Split(targetAuthorityDnsName, ".")) 43 | if len(reverseTargetParts) < len(s.reverseMatchParts) { 44 | return nil, NotKedgeDestinationErr(targetAuthorityDnsName, "") // target is shorter than the match, definitely no point. 45 | } 46 | wildcardParts := []string{} 47 | for i, part := range s.reverseMatchParts { 48 | if part == "*" { 49 | wildcardParts = append(wildcardParts, reverseTargetParts[i]) 50 | continue 51 | } 52 | if reverseTargetParts[i] != part { 53 | return nil, NotKedgeDestinationErr(targetAuthorityDnsName, "") 54 | } 55 | } 56 | kedgeUrl := s.scheme + "://" + strings.Join(reverse(wildcardParts), ".") + s.suffix 57 | 58 | u, err := url.Parse(kedgeUrl) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &Route{URL: u}, nil 63 | } 64 | 65 | func reverse(strings []string) []string { 66 | for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 { 67 | strings[i], strings[j] = strings[j], strings[i] 68 | } 69 | return strings 70 | } 71 | -------------------------------------------------------------------------------- /pkg/map/suffix_test.go: -------------------------------------------------------------------------------- 1 | package kedge_map_test 2 | 3 | import ( 4 | "testing" 5 | 6 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSuffix_MatchesOne(t *testing.T) { 12 | mapper, err := kedge_map.Suffix("*.clusters.local", ".example.com", "https") 13 | require.NoError(t, err, "no error on initialization") 14 | 15 | for _, tcase := range []struct { 16 | target string 17 | kedgeUrl string 18 | expectedErr string 19 | }{ 20 | { 21 | target: "myservice.mynamespace.us1.clusters.local", 22 | kedgeUrl: "https://us1.example.com", 23 | expectedErr: "", 24 | }, 25 | { 26 | target: "somethingmore.myservice.mynamespace.eu1.clusters.local", 27 | kedgeUrl: "https://eu1.example.com", 28 | expectedErr: "", 29 | }, 30 | { 31 | target: "something.google.com", 32 | kedgeUrl: "", 33 | expectedErr: "something.google.com is not a kedge destination", 34 | }, 35 | { 36 | target: "another.local", 37 | kedgeUrl: "", 38 | expectedErr: "another.local is not a kedge destination", 39 | }, 40 | } { 41 | t.Run(tcase.target, func(t *testing.T) { 42 | route, err := mapper.Map(tcase.target, "does not matter") 43 | if tcase.expectedErr == "" { 44 | require.NoError(t, err, "no error") 45 | assert.Equal(t, tcase.kedgeUrl, route.URL.String(), "urls must match") 46 | } else { 47 | require.Equal(t, tcase.expectedErr, err.Error(), "error expected") 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestSuffix_MatchesMulti(t *testing.T) { 54 | mapper, err := kedge_map.Suffix("*.*.clusters.local", ".example.com", "https") 55 | require.NoError(t, err, "no error on initialization") 56 | 57 | for _, tcase := range []struct { 58 | target string 59 | kedgeUrl string 60 | expectedErr string 61 | }{ 62 | { 63 | target: "myservice.mynamespace.us1.prod.clusters.local", 64 | kedgeUrl: "https://us1.prod.example.com", 65 | expectedErr: "", 66 | }, 67 | { 68 | target: "somethingmore.myservice.mynamespace.eu1.staging.clusters.local", 69 | kedgeUrl: "https://eu1.staging.example.com", 70 | expectedErr: "", 71 | }, 72 | { 73 | target: "something.something.google.com", 74 | kedgeUrl: "", 75 | expectedErr: "something.something.google.com is not a kedge destination", 76 | }, 77 | { 78 | target: "tooshort.clusters.local", 79 | kedgeUrl: "", 80 | expectedErr: "tooshort.clusters.local is not a kedge destination", 81 | }, 82 | } { 83 | t.Run(tcase.target, func(t *testing.T) { 84 | route, err := mapper.Map(tcase.target, "does not matter") 85 | if tcase.expectedErr == "" { 86 | require.NoError(t, err, "no error") 87 | assert.Equal(t, tcase.kedgeUrl, route.URL.String(), "urls must match") 88 | } else { 89 | require.Equal(t, tcase.expectedErr, err.Error(), "error expected") 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/metrics/backend_configuration.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | const ( 6 | ConfiguationActionCreate = "create" 7 | ConfiguationActionChange = "update" 8 | ConfiguationActionDelete = "delete" 9 | ) 10 | 11 | var ( 12 | BackendHTTPConfigurationCounter = prometheus.NewCounterVec( 13 | prometheus.CounterOpts{ 14 | Name: "kedge_http_backend_configuration_changes_total", 15 | Help: "Count of changes in HTTP backend configuration, done by flagz flag change. It can be both because use changed dynamic flag" + 16 | "or dynamic routing discovered a change", 17 | }, 18 | []string{"backend_name", "action"}, 19 | ) 20 | BackendGRPCConfigurationCounter = prometheus.NewCounterVec( 21 | prometheus.CounterOpts{ 22 | Name: "kedge_grpc_backend_configuration_changes_total", 23 | Help: "Count of changes in gRPC backend configuration, done by flagz flag change. It can be both because use changed dynamic flag" + 24 | "or dynamic routing discovered a change", 25 | }, 26 | []string{"backend_name", "action"}, 27 | ) 28 | ) 29 | 30 | func init() { 31 | prometheus.MustRegister(BackendHTTPConfigurationCounter) 32 | prometheus.MustRegister(BackendGRPCConfigurationCounter) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/metrics/kedge_error.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | KedgeProxyErrors = prometheus.NewCounterVec( 7 | prometheus.CounterOpts{ 8 | Name: "kedge_proxy_errors_total", 9 | Help: "Count of errors spotted during proxying request. These errors are fails that never went outside of kedge.", 10 | }, 11 | []string{"backend_name", "type"}, 12 | ) 13 | ) 14 | 15 | func init() { 16 | prometheus.MustRegister(KedgeProxyErrors) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/reporter/errtypes/errtypes.go: -------------------------------------------------------------------------------- 1 | package errtypes 2 | 3 | type Type string 4 | 5 | const ( 6 | OK Type = "" 7 | 8 | // Unauthorized is an error returned by proxy.AuthMiddleware indicating case when request is not authorized to be proxied. 9 | // NOTE: This is only for OIDC auth. Cert auth is done on http.Server level, and there is no reporting implemented yet on that. 10 | Unauthorized Type = "unauthorized" 11 | 12 | // NoRoute is an error returned by p.router.Route(req) indicating no route for given request. 13 | NoRoute Type = "no-route" 14 | 15 | // RouteUnknownError is an error returned by p.router.Route(req) indicating some unknown error than no route. 16 | RouteUnknownError Type = "unknown-route-error" 17 | 18 | // NoBackend is the only error that can be returned by backendpool.Tripper (ErrUnknownBackend) 19 | // It can happen on bug or wrong configuration (routing exists for not existing backend) or race in configuration. 20 | NoBackend Type = "no-backend" 21 | 22 | // BackendTransportClosed is an error returned by backendpool.closedTripper indicating that the backend should 23 | // not be used, but somehow it was still in usage. 24 | BackendTransportClosed Type = "backend-transport-closed" 25 | 26 | // NoConnToAllResolvedAddresses is an error returned by lbtransport.tripper when all addresses (IP:Port) returned by 27 | // our resolver (K8s or DNS) are not accessible (dial errors). This can happen for DNS when DNS itself is broken. 28 | NoConnToAllResolvedAddresses Type = "no-connection-to-all-resolved-addresses" 29 | 30 | // NoResolutionAvailable is an error returned by lbtransport.tripper when we have an resolution error constantly and there 31 | // is no (even old) resolution, so no target to even try. 32 | NoResolutionAvailable Type = "no-resolution-available" 33 | 34 | // TransportUnknownError. 35 | // For Kedge: it is an error reported by lbtransport when we get a non-dial error from http.Transport RoundTrip. 36 | // This includes EOF's (backend server timeout) and context cancellations (kedge tripperware timeout). 37 | // For Winch: it is an error reported by errHandler tripper when spotted any error on RoundTrip to kedge. 38 | // This includes EOF's (kedge server timeout) and context cancellations (winch tripperware timeout), as well as any 39 | // other winch internal error. 40 | TransportUnknownError Type = "transport-unknown-error" 41 | 42 | // IrrecoverableWatcherError indicates unlikely irrecoverable error from resolver's naming.Watcher. 43 | IrrecoverableWatcherError Type = "resolver-watcher-irrecoverable" 44 | ) 45 | -------------------------------------------------------------------------------- /pkg/resolvers/host/host.go: -------------------------------------------------------------------------------- 1 | package hostresolver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | grpcsrvlb "github.com/improbable-eng/go-srvlb/grpc" 9 | "github.com/improbable-eng/go-srvlb/srv" 10 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "google.golang.org/grpc/naming" 13 | ) 14 | 15 | type hostResolverFn func(host string) (addrs []string, err error) 16 | 17 | var ( 18 | ParentHostResolver hostResolverFn = net.LookupHost 19 | 20 | resolutions = prometheus.NewCounter( 21 | prometheus.CounterOpts{ 22 | Namespace: "kedge", 23 | Subsystem: "host_resolver", 24 | Name: "resolutions_attempts_total", 25 | Help: "Counter of all DNS resolutions attempts made by host resolver.", 26 | }, 27 | ) 28 | 29 | // TODO(bwplotka): Use real TTL for this. https://github.com/improbable-eng/kedge/issues/147 30 | resolutionTTL = 5 * time.Second 31 | ) 32 | 33 | func init() { 34 | prometheus.MustRegister(resolutions) 35 | } 36 | 37 | // NewFromConfig creates HOST resolver wrapped by grpcsrvlb that polls host resolver in frequency defined by returned TTL. 38 | func NewFromConfig(conf *pb.HostResolver) (target string, namer naming.Resolver, err error) { 39 | parent := ParentHostResolver 40 | return conf.GetDnsName(), grpcsrvlb.New(newHostResolver(conf.Port, parent)), nil 41 | } 42 | 43 | type hostResolver struct { 44 | hostResolverFn hostResolverFn 45 | port uint32 46 | } 47 | 48 | // newHostResolver uses results from parent resolver and adds a port to it to implement srv.Resolver. 49 | func newHostResolver(port uint32, hostResolverFn hostResolverFn) srv.Resolver { 50 | return &hostResolver{ 51 | hostResolverFn: hostResolverFn, 52 | port: port, 53 | } 54 | } 55 | 56 | func (r *hostResolver) Lookup(domainName string) ([]*srv.Target, error) { 57 | ips, err := r.hostResolverFn(domainName) 58 | resolutions.Inc() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | var targets []*srv.Target 64 | for _, ip := range ips { 65 | targets = append(targets, &srv.Target{ 66 | DialAddr: net.JoinHostPort(ip, fmt.Sprintf("%d", r.port)), 67 | Ttl: resolutionTTL, 68 | }) 69 | } 70 | 71 | return targets, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/resolvers/host/host_test.go: -------------------------------------------------------------------------------- 1 | package hostresolver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const testDomain = "domain.org" 11 | 12 | func TestPortOverrideSRVResolver_Lookup(t *testing.T) { 13 | l := func(host string) (addrs []string, err error) { 14 | require.Equal(t, testDomain, host) 15 | return []string{ 16 | "1.1.1.1", 17 | "1.1.1.2", 18 | "1.1.1.10", 19 | }, nil 20 | } 21 | 22 | p := newHostResolver(99, l) 23 | targets, err := p.Lookup(testDomain) 24 | require.NoError(t, err) 25 | 26 | assert.Equal(t, "1.1.1.1:99", targets[0].DialAddr) 27 | assert.Equal(t, resolutionTTL, targets[0].Ttl) 28 | assert.Equal(t, "1.1.1.2:99", targets[1].DialAddr) 29 | assert.Equal(t, resolutionTTL, targets[1].Ttl) 30 | assert.Equal(t, "1.1.1.10:99", targets[2].DialAddr) 31 | assert.Equal(t, resolutionTTL, targets[2].Ttl) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/resolvers/k8s/client.go: -------------------------------------------------------------------------------- 1 | package k8sresolver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/improbable-eng/kedge/pkg/k8s" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type endpointClient interface { 14 | StartChangeStream(ctx context.Context, t targetEntry) (io.ReadCloser, error) 15 | } 16 | 17 | type client struct { 18 | k8sClient *k8s.APIClient 19 | } 20 | 21 | // StartChangeStream starts stream of changes from watch endpoint. 22 | // See https://kubernetes.io/docs/api-reference/v1.7/#watch-132 23 | // NOTE: In the beginning of stream, k8s will give us sufficient info about current state. (No need to GET first) 24 | func (c *client) StartChangeStream(ctx context.Context, t targetEntry) (io.ReadCloser, error) { 25 | epWatchURL := fmt.Sprintf("%s/api/v1/watch/namespaces/%s/endpoints/%s", 26 | c.k8sClient.Address, 27 | t.namespace, 28 | t.service, 29 | ) 30 | 31 | return c.startGET(ctx, epWatchURL) 32 | } 33 | 34 | // NOTE: It is caller responsibility to read body through and close it. 35 | func (c *client) startGET(ctx context.Context, url string) (io.ReadCloser, error) { 36 | req, err := http.NewRequest("GET", url, nil) 37 | if err != nil { 38 | return nil, errors.Wrapf(err, "Failed to create new GET request %s", url) 39 | } 40 | 41 | resp, err := c.k8sClient.Do(req.WithContext(ctx)) 42 | if err != nil { 43 | return nil, errors.Wrapf(err, "Failed to do GET %s request", url) 44 | } 45 | 46 | if resp.StatusCode != http.StatusOK { 47 | resp.Body.Close() 48 | return nil, errors.Errorf("Invalid response code %d on GET %s request", resp.StatusCode, url) 49 | } 50 | 51 | return resp.Body, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/resolvers/k8s/resolver_test.go: -------------------------------------------------------------------------------- 1 | package k8sresolver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseTarget(t *testing.T) { 12 | for _, tcase := range []struct { 13 | target string 14 | 15 | expectgedTarget targetEntry 16 | expectedErr error 17 | }{ 18 | { 19 | target: "", 20 | expectedErr: errors.New("Failed to parse targetEntry. Empty string"), 21 | }, 22 | { 23 | target: "http://service1", 24 | expectedErr: errors.Errorf("Bad targetEntry name. It cannot contain any schema. Expected format: %s", ExpectedTargetFmt), 25 | }, 26 | { 27 | target: "service1.ns1:123:123", 28 | expectedErr: errors.Errorf("bad targetEntry - it contains multiple \":\" - expected format is %s", ExpectedTargetFmt), 29 | }, 30 | { 31 | target: "service1", 32 | expectgedTarget: targetEntry{ 33 | service: "service1", 34 | namespace: "default", 35 | port: noTargetPort, 36 | }, 37 | }, 38 | { 39 | target: "service1.ns1", 40 | expectgedTarget: targetEntry{ 41 | service: "service1", 42 | namespace: "ns1", 43 | port: noTargetPort, 44 | }, 45 | }, 46 | { 47 | // We allow that. 48 | target: "service2.ns2.svc.local", 49 | expectgedTarget: targetEntry{ 50 | service: "service2", 51 | namespace: "ns2", 52 | port: noTargetPort, 53 | }, 54 | }, 55 | { 56 | // We allow that. 57 | target: "service3.ns3.svc.cluster1.example.org", 58 | expectgedTarget: targetEntry{ 59 | service: "service3", 60 | namespace: "ns3", 61 | port: noTargetPort, 62 | }, 63 | }, 64 | { 65 | target: "service4.ns4:1010", 66 | expectgedTarget: targetEntry{ 67 | service: "service4", 68 | namespace: "ns4", 69 | port: targetPort{ 70 | value: "1010", 71 | isNamed: false, 72 | }, 73 | }, 74 | }, 75 | { 76 | target: "service5.ns5:some-port", 77 | expectgedTarget: targetEntry{ 78 | service: "service5", 79 | namespace: "ns5", 80 | port: targetPort{ 81 | value: "some-port", 82 | isNamed: true, 83 | }, 84 | }, 85 | }, 86 | { 87 | target: "service6.ns6:", 88 | expectgedTarget: targetEntry{ 89 | service: "service6", 90 | namespace: "ns6", 91 | port: noTargetPort, 92 | }, 93 | }, 94 | } { 95 | t.Logf("Case %s", tcase.target) 96 | 97 | res, err := parseTarget(tcase.target) 98 | if tcase.expectedErr != nil { 99 | require.Error(t, err) 100 | assert.Equal(t, tcase.expectedErr.Error(), err.Error()) 101 | continue 102 | } 103 | 104 | require.NoError(t, err) 105 | require.NotNil(t, res) 106 | assert.Equal(t, tcase.expectgedTarget, res) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/resolvers/srv/srv.go: -------------------------------------------------------------------------------- 1 | package srvresolver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | grpcsrvlb "github.com/improbable-eng/go-srvlb/grpc" 10 | "github.com/improbable-eng/go-srvlb/srv" 11 | pb "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "google.golang.org/grpc/naming" 14 | ) 15 | 16 | var ( 17 | ParentSrvResolver = srv.NewGoResolver(resolutionTTL) 18 | 19 | resolutions = prometheus.NewCounter( 20 | prometheus.CounterOpts{ 21 | Namespace: "kedge", 22 | Subsystem: "srv_resolver", 23 | Name: "resolutions_attempts_total", 24 | Help: "Counter of all DNS resolutions attempts made by SRV resolver.", 25 | }, 26 | ) 27 | 28 | // TODO(bwplotka): Use real TTL for this. https://github.com/improbable-eng/kedge/issues/147 29 | resolutionTTL = 5 * time.Second 30 | ) 31 | 32 | func init() { 33 | prometheus.MustRegister(resolutions) 34 | } 35 | 36 | // NewFromConfig creates SRV resolver wrapped by grpcsrvlb that polls SRV resolver in frequency defined by returned TTL. 37 | func NewFromConfig(conf *pb.SrvResolver) (target string, namer naming.Resolver, err error) { 38 | parent := ParentSrvResolver 39 | if conf.PortOverride != 0 { 40 | parent = newPortOverrideSRVResolver(conf.PortOverride, parent) 41 | } 42 | 43 | return conf.GetDnsName(), grpcsrvlb.New(parent), nil 44 | } 45 | 46 | // newPortOverrideSRVResolver uses results from parent resolver, but ignores port totally and specifies our own. 47 | func newPortOverrideSRVResolver(port uint32, resolver srv.Resolver) srv.Resolver { 48 | return &portOverrideSRVResolver{ 49 | parent: resolver, 50 | port: port, 51 | } 52 | } 53 | 54 | type portOverrideSRVResolver struct { 55 | parent srv.Resolver 56 | port uint32 57 | } 58 | 59 | func (r *portOverrideSRVResolver) Lookup(domainName string) ([]*srv.Target, error) { 60 | targets, err := r.parent.Lookup(domainName) 61 | resolutions.Inc() 62 | if err != nil { 63 | return targets, err 64 | } 65 | 66 | for _, target := range targets { 67 | splitted := strings.Split(target.DialAddr, ":") 68 | 69 | // Ignore port from SRV and use specified one. 70 | target.DialAddr = net.JoinHostPort(splitted[0], fmt.Sprintf("%d", r.port)) 71 | } 72 | 73 | return targets, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/resolvers/srv/srv_test.go: -------------------------------------------------------------------------------- 1 | package srvresolver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/improbable-eng/go-srvlb/srv" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | const testDomain = "domain.org" 12 | 13 | type testLookup struct { 14 | t *testing.T 15 | } 16 | 17 | func (l *testLookup) Lookup(domainName string) ([]*srv.Target, error) { 18 | require.Equal(l.t, testDomain, domainName) 19 | return []*srv.Target{ 20 | { 21 | DialAddr: "1.1.1.1:80", 22 | Ttl: resolutionTTL, 23 | }, 24 | { 25 | DialAddr: "1.1.1.2", 26 | Ttl: resolutionTTL, 27 | }, 28 | { 29 | DialAddr: "1.1.1.10:81", 30 | Ttl: resolutionTTL, 31 | }, 32 | }, nil 33 | } 34 | 35 | func TestPortOverrideSRVResolver_Lookup(t *testing.T) { 36 | l := &testLookup{ 37 | t: t, 38 | } 39 | 40 | p := newPortOverrideSRVResolver(99, l) 41 | targets, err := p.Lookup(testDomain) 42 | require.NoError(t, err) 43 | 44 | assert.Equal(t, "1.1.1.1:99", targets[0].DialAddr) 45 | assert.Equal(t, resolutionTTL, targets[0].Ttl) 46 | assert.Equal(t, "1.1.1.2:99", targets[1].DialAddr) 47 | assert.Equal(t, resolutionTTL, targets[0].Ttl) 48 | assert.Equal(t, "1.1.1.10:99", targets[2].DialAddr) 49 | assert.Equal(t, resolutionTTL, targets[0].Ttl) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/sharedflags/set.go: -------------------------------------------------------------------------------- 1 | package sharedflags 2 | 3 | import "github.com/spf13/pflag" 4 | 5 | var ( 6 | // Set is a common set of flags that are used throughout the libraries and services of grpc director. 7 | // They can be dynamically manipulated through go-flagz 8 | Set = pflag.NewFlagSet("cmd", pflag.ExitOnError) 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/tls/client.go: -------------------------------------------------------------------------------- 1 | package kedge_tls 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | 8 | "github.com/improbable-eng/kedge/pkg/sharedflags" 9 | "github.com/mwitkow/go-conntrack/connhelpers" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var ( 15 | flagTLSClientCert = sharedflags.Set.String( 16 | "client_tls_cert_file", "", 17 | "Path to the optional PEM certificate that client will use. This is required when kedge is configured with server_tls_client_cert_required=true.") 18 | flagTLSClientKey = sharedflags.Set.String( 19 | "client_tls_key_file", ".", 20 | "Path to the optional PEM key for the certificate that the client will use. This is required when kedge is configured with server_tls_client_cert_required=true.") 21 | flagTLSRootCAFiles = sharedflags.Set.StringSlice( 22 | "client_tls_root_ca_files", []string{}, 23 | "Paths (comma separated) to custom PEM certificate CA chains used for root trusted CA for server verification. If nil, root CAs will fetched from host.", 24 | ) 25 | flagTLSInsecureSkipVerify = sharedflags.Set.Bool( 26 | "client_insecure", false, 27 | " Controls whether a client verifies the server's certificate chain and host name. If is true, TLS accepts any certificate presented by the server and any host name in that certificate."+ 28 | " In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.") 29 | ) 30 | 31 | // BuildClientTLSConfigFromFlags creates TLS config for Kedge client. 32 | // Used by all clients that communicates with kedge (e.g Winch and LoadTest) 33 | func BuildClientTLSConfigFromFlags() (*tls.Config, error) { 34 | var tlsConfig *tls.Config 35 | 36 | // Add client certs if specified. 37 | if *flagTLSClientCert == "" || *flagTLSClientKey == "" { 38 | log.Debug("Either key or cert for TLS client certificates are not present.") 39 | tlsConfig = &tls.Config{} 40 | } else { 41 | // TlsConfigForServerCerts name is misleading - it Certificates field is used for client as well when used on http.Client 42 | var err error 43 | tlsConfig, err = connhelpers.TlsConfigForServerCerts(*flagTLSClientCert, *flagTLSClientKey) 44 | if err != nil { 45 | return nil, errors.Wrapf(err, "failed reading TLS client keys.") 46 | } 47 | } 48 | tlsConfig.MinVersion = tls.VersionTLS12 49 | 50 | // Add root CA if specified. 51 | if len(*flagTLSRootCAFiles) > 0 { 52 | tlsConfig.RootCAs = x509.NewCertPool() 53 | for _, path := range *flagTLSRootCAFiles { 54 | data, err := ioutil.ReadFile(path) 55 | if err != nil { 56 | return nil, errors.Wrapf(err, "failed reading root CA file %v", path) 57 | } 58 | if ok := tlsConfig.RootCAs.AppendCertsFromPEM(data); !ok { 59 | return nil, errors.Errorf("failed processing root CA file %v", path) 60 | } 61 | } 62 | } 63 | 64 | // Set no verify if specified. 65 | if *flagTLSInsecureSkipVerify { 66 | tlsConfig.InsecureSkipVerify = true 67 | } 68 | return tlsConfig, nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/tokenauth/http/tripper.go: -------------------------------------------------------------------------------- 1 | package httpauth 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/improbable-eng/kedge/pkg/tokenauth" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Tripper struct { 12 | parent http.RoundTripper 13 | auth tokenauth.Source 14 | headerName string 15 | } 16 | 17 | // NewTripper constructs Tripper that is able to inject any token as Bearer inside given Header name. 18 | func NewTripper(parent http.RoundTripper, auth tokenauth.Source, headerName string) http.RoundTripper { 19 | return &Tripper{ 20 | parent: parent, 21 | auth: auth, 22 | headerName: headerName, 23 | } 24 | } 25 | 26 | // RoundTrip wraps parent RoundTrip and injects retrieved Token into specified Header. 27 | func (t *Tripper) RoundTrip(req *http.Request) (*http.Response, error) { 28 | token, err := t.auth.Token(req.Context()) 29 | if err != nil { 30 | return nil, errors.Wrap(err, "httpauth.Tripper: failed to retrieve valid Auth Token") 31 | } 32 | 33 | req.Header.Set(t.headerName, fmt.Sprintf("Bearer %s", token)) 34 | return t.parent.RoundTrip(req) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/tokenauth/source.go: -------------------------------------------------------------------------------- 1 | package tokenauth 2 | 3 | import "context" 4 | 5 | // Source represents a way to get client auth in a form of token. 6 | type Source interface { 7 | // Name of the auth source. 8 | Name() string 9 | 10 | // Token allows the source to return a valid token for specific authorization type in a form of string. 11 | // 12 | // Example usage: 13 | // - filling Authorization HTTP header with valid auth. 14 | // In that case it is up to caller to properly save it into specific http request header (usually called "Authorization") 15 | // and add "bearer" prefix if needed. 16 | Token(context.Context) (string, error) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/tokenauth/sources/direct/direct.go: -------------------------------------------------------------------------------- 1 | package directauth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/improbable-eng/kedge/pkg/tokenauth" 7 | ) 8 | 9 | type source struct { 10 | name string 11 | token string 12 | } 13 | 14 | // New returns new direct auth source. 15 | func New(name string, token string) tokenauth.Source { 16 | return &source{ 17 | name: name, 18 | token: token, 19 | } 20 | } 21 | 22 | // Name of the auth source. 23 | func (s *source) Name() string { 24 | return s.name 25 | } 26 | 27 | // Token returns a token given in constructor. 28 | func (s *source) Token(_ context.Context) (string, error) { 29 | return s.token, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/tokenauth/sources/k8s/k8s.go: -------------------------------------------------------------------------------- 1 | package k8sauth 2 | 3 | import ( 4 | "context" 5 | 6 | k8s "github.com/Bplotka/oidc/login/k8scache" 7 | "github.com/improbable-eng/kedge/pkg/tokenauth" 8 | directauth "github.com/improbable-eng/kedge/pkg/tokenauth/sources/direct" 9 | oauth2auth "github.com/improbable-eng/kedge/pkg/tokenauth/sources/oauth2" 10 | oidcauth "github.com/improbable-eng/kedge/pkg/tokenauth/sources/oidc" 11 | "github.com/pkg/errors" 12 | cfg "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | // New constructs appropriate tokenAuth Source to the given AuthInfo from kube config referenced by user. 16 | // This is really convenient if you want to reuse well configured kube config. 17 | func New(ctx context.Context, name string, configPath string, userName string) (tokenauth.Source, error) { 18 | if configPath == "" { 19 | configPath = k8s.DefaultKubeConfigPath 20 | } 21 | k8sConfig, err := cfg.LoadFromFile(configPath) 22 | if err != nil { 23 | return nil, errors.Wrapf(err, "Failed to load k8s config from file %v. Make sure it is there or change"+ 24 | " permissions.", configPath) 25 | } 26 | 27 | info, ok := k8sConfig.AuthInfos[userName] 28 | if !ok { 29 | return nil, errors.Errorf("Failed to find user %s inside k8s config AuthInfo from file %v", userName, configPath) 30 | } 31 | 32 | // Currently supported: 33 | // - token 34 | // - OIDC 35 | // - Google compute platform via Oauth2 36 | if info.AuthProvider != nil { 37 | switch info.AuthProvider.Name { 38 | case "oidc": 39 | cache, err := k8s.NewCacheFromUser(configPath, userName) 40 | if err != nil { 41 | return nil, errors.Wrap(err, "Failed to get OIDC configuration from user. ") 42 | } 43 | s, _, err := oidcauth.NewWithCache(ctx, name, cache, nil) 44 | return s, err 45 | case "gcp": 46 | c, err := oauth2auth.NewConfigFromMap(info.AuthProvider.Config) 47 | if err != nil { 48 | return nil, errors.Wrap(err, "Failed to create OAuth2 config from map.") 49 | } 50 | return oauth2auth.NewGCP(name, userName, configPath, c) 51 | default: 52 | // TODO(bplotka): Add support for more of them if needed. 53 | return nil, errors.Errorf("Not supported k8s Auth provider %v", info.AuthProvider.Name) 54 | } 55 | } 56 | 57 | if info.Token != "" { 58 | return directauth.New(name, info.Token), nil 59 | } 60 | 61 | return nil, errors.Errorf("Not found supported auth source called %s from k8s config %+v", userName, info) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/tokenauth/sources/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2auth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/improbable-eng/kedge/pkg/tokenauth" 7 | "github.com/pkg/errors" 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | type source struct { 12 | name string 13 | tokenSource oauth2.TokenSource 14 | } 15 | 16 | // New constructs Oauth2 tokenauth that returns valid access token. It is up to token source to do login or not. 17 | func New(name string, tokenSource oauth2.TokenSource) tokenauth.Source { 18 | return &source{ 19 | name: name, 20 | tokenSource: tokenSource, 21 | } 22 | } 23 | 24 | // Name of the auth source. 25 | func (s *source) Name() string { 26 | return s.name 27 | } 28 | 29 | // Token returns valid ID token or error. 30 | func (s *source) Token(_ context.Context) (string, error) { 31 | token, err := s.tokenSource.Token() 32 | if err != nil { 33 | return "", errors.Wrap(err, "Failed to obtain Oauth2 Token.") 34 | } 35 | 36 | return token.AccessToken, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/tokenauth/sources/oidc/oidc.go: -------------------------------------------------------------------------------- 1 | package oidcauth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/Bplotka/oidc" 10 | "github.com/Bplotka/oidc/gsa" 11 | "github.com/Bplotka/oidc/login" 12 | disk "github.com/Bplotka/oidc/login/diskcache" 13 | "github.com/improbable-eng/kedge/pkg/tokenauth" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type source struct { 18 | name string 19 | tokenSource oidc.TokenSource 20 | } 21 | 22 | // New constructs OIDC tokenauth.Source that optionally supports logging in if callbackSrc is not nil. 23 | // Additionally it returns clearIDToken function that can be used to clear the token if needed. 24 | // TokenSource is by default configured to use disk as cache for tokens. 25 | func New(ctx context.Context, name string, config login.OIDCConfig, path string, callbackSrv *login.CallbackServer) (tokenauth.Source, func() error, error) { 26 | return NewWithCache(ctx, name, disk.NewCache(path, config), callbackSrv) 27 | } 28 | 29 | // NewWithCache is same as New but allows to pass custom cache e.g k8s one. 30 | func NewWithCache(ctx context.Context, name string, cache login.Cache, callbackSrv *login.CallbackServer) (tokenauth.Source, func() error, error) { 31 | tokenSource, clearIDTokenFunc, err := login.NewOIDCTokenSource( 32 | ctx, 33 | log.New(os.Stdout, fmt.Sprintf("OIDC Auth %s ", name), 0), 34 | login.Config{ 35 | NonceCheck: false, 36 | }, 37 | cache, 38 | callbackSrv, 39 | ) 40 | if err != nil { 41 | return nil, nil, errors.Wrap(err, "failed to create OIDC Token Source") 42 | } 43 | 44 | return &source{ 45 | name: name, 46 | tokenSource: tokenSource, 47 | }, clearIDTokenFunc, nil 48 | } 49 | 50 | // NewGoogleFromServiceAccount constructs tokenauth.Source that is able to return valid OIDC token from given Google Service Account. 51 | func NewGoogleFromServiceAccount(ctx context.Context, name string, config login.OIDCConfig, googleServiceAccountJSON []byte) (tokenauth.Source, error) { 52 | tokenSource, _, err := gsa.NewOIDCTokenSource( 53 | ctx, 54 | log.New(os.Stdout, "", log.LstdFlags), 55 | googleServiceAccountJSON, 56 | config.Provider, 57 | gsa.OIDCConfig{ 58 | ClientID: config.ClientID, 59 | ClientSecret: config.ClientSecret, 60 | Scopes: config.Scopes, 61 | }, 62 | ) 63 | if err != nil { 64 | return nil, errors.Wrap(err, "failed to create Google Service Account Token Source") 65 | } 66 | 67 | return &source{ 68 | name: name, 69 | tokenSource: tokenSource, 70 | }, nil 71 | } 72 | 73 | // Name of the auth source. 74 | func (s *source) Name() string { 75 | return s.name 76 | } 77 | 78 | // Token returns valid ID token or error. 79 | func (s *source) Token(ctx context.Context) (string, error) { 80 | token, err := s.tokenSource.OIDCToken(ctx) 81 | if err != nil { 82 | return "", errors.Wrap(err, "Failed to obtain OIDC Token.") 83 | } 84 | 85 | return token.IDToken, nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/tokenauth/sources/test/test.go: -------------------------------------------------------------------------------- 1 | package testauth 2 | 3 | import "context" 4 | 5 | // Source is testing tokenauth.Source that can be mocked. 6 | type Source struct { 7 | NameValue string 8 | TokenValue string 9 | Err error 10 | } 11 | 12 | // Name of the auth Source. 13 | func (s *Source) Name() string { 14 | return s.NameValue 15 | } 16 | 17 | // Token returns a token given while constructing. 18 | func (s *Source) Token(_ context.Context) (string, error) { 19 | return s.TokenValue, s.Err 20 | } 21 | -------------------------------------------------------------------------------- /pkg/winch/pac.go: -------------------------------------------------------------------------------- 1 | package winch 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "text/template" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | 12 | http_ctxtags "github.com/improbable-eng/go-httpwares/tags" 13 | "github.com/improbable-eng/kedge/pkg/sharedflags" 14 | ) 15 | 16 | var ( 17 | // TODO(bplotka): Consider another default, autodeducted from routing mapper (complex) 18 | flagShExpressions = sharedflags.Set.StringSlice("pac_redirect_sh_expressions", []string{}, 19 | "Comma delimited array of shExpMatch expressions for host in the PAC. They will influence on what host"+ 20 | " browser will redirect to winch. If empty it will redirect everything via winch.") 21 | flagPACFile = sharedflags.Set.String("pac_file", "", 22 | "Path to PAC file that should be read. This flag has priority over 'pac_redirect_sh_expressions'") 23 | ) 24 | 25 | func NewPacFromFlags(winchHostPort string) (pac *Pac, err error) { 26 | pac = &Pac{modTime: time.Now()} 27 | 28 | if *flagPACFile != "" { 29 | if len(*flagShExpressions) > 0 { 30 | return nil, errors.New("flag 'pac_redirect_sh_expressions' cannot be specified with 'pac_file'") 31 | } 32 | 33 | b, err := ioutil.ReadFile(*flagPACFile) 34 | if err != nil { 35 | return nil, errors.Wrapf(err, "failed to read PAC bytes from %s file", *flagPACFile) 36 | 37 | } 38 | pac.PAC = b 39 | return pac, nil 40 | } 41 | 42 | b, err := generatePAC(winchHostPort, *flagShExpressions) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | pac.PAC = b 48 | return pac, nil 49 | } 50 | 51 | // Pac is a handler that serves auto generated PAC file based on mapping routes. 52 | type Pac struct { 53 | PAC []byte 54 | modTime time.Time 55 | } 56 | 57 | func (p *Pac) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 58 | // TODO(bplotka): Pass only local connections. 59 | 60 | tags := http_ctxtags.ExtractInbound(req) 61 | tags.Set(http_ctxtags.TagForCallService, "PAC") 62 | 63 | resp.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig") 64 | http.ServeContent(resp, req, "wpad.dat", p.modTime, bytes.NewReader(p.PAC)) // or proxy.pac 65 | return 66 | } 67 | 68 | var ( 69 | pacTemplate = `function FindProxyForURL(url, host) { 70 | var proxy = "PROXY {{.WinchHostPort}}; DIRECT"; 71 | var direct = "DIRECT"; 72 | 73 | // no proxy for local hosts without domain: 74 | if(isPlainHostName(host)) return direct; 75 | 76 | // We only proxy http, not even https. 77 | if ( 78 | url.substring(0, 4) == "ftp:" || 79 | url.substring(0, 6) == "rsync:" || 80 | url.substring(0, 6) == "https:" 81 | ) 82 | return direct; 83 | 84 | // Commented for debug purposes. 85 | // Use direct connection whenever we have direct network connectivity. 86 | //if (isResolvable(host)) { 87 | // return direct 88 | //} 89 | {{- if .Routes }} 90 | {{- range .Routes}} 91 | if (shExpMatch(host, "{{ . }}")) { 92 | return proxy; 93 | } 94 | {{- end}} 95 | 96 | return direct; 97 | {{- else }} 98 | return proxy; 99 | {{- end }} 100 | }` 101 | ) 102 | 103 | func generatePAC(winchHostPort string, rules []string) ([]byte, error) { 104 | tmpl, err := template.New("PAC").Parse(pacTemplate) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | buf := &bytes.Buffer{} 110 | err = tmpl.Execute(buf, struct { 111 | WinchHostPort string 112 | Routes []string 113 | }{ 114 | WinchHostPort: winchHostPort, 115 | Routes: rules, 116 | }) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return buf.Bytes(), nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/winch/routes_test.go: -------------------------------------------------------------------------------- 1 | package winch 2 | 3 | import ( 4 | "testing" 5 | 6 | kedge_map "github.com/improbable-eng/kedge/pkg/map" 7 | pb "github.com/improbable-eng/kedge/protogen/winch/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRegexpRoute(t *testing.T) { 13 | r, err := newRegexp(&pb.RegexpRoute{ 14 | Exp: `(?P[a-z0-9-].*)[.](?P[a-z0-9-].*)[.]internal[.]example[.]org(?P|:[0-9].*)`, 15 | Url: "${arg1}-${ARG2}", 16 | }, &kedge_map.Route{}) 17 | require.NoError(t, err) 18 | 19 | _, ok, err := r.Route("value1.value2.internal.unknown.org:1234") 20 | require.NoError(t, err) 21 | assert.False(t, ok) 22 | 23 | route, ok, err := r.Route("value1.value2.internal.example.org:1234") 24 | require.NoError(t, err) 25 | require.True(t, ok) 26 | 27 | assert.Equal(t, "value1-value2", route.URL.String()) 28 | } 29 | 30 | func TestDirectRoute(t *testing.T) { 31 | r, err := newDirect(&pb.DirectRoute{ 32 | Key: "value1.value2.internal.example.org", 33 | Url: "some-url.com", 34 | }, &kedge_map.Route{}) 35 | require.NoError(t, err) 36 | 37 | _, ok, err := r.Route("value1.value2.internal.unknown.org") 38 | require.NoError(t, err) 39 | assert.False(t, ok) 40 | 41 | route, ok, err := r.Route("value1.value2.internal.example.org") 42 | require.NoError(t, err) 43 | require.True(t, ok) 44 | 45 | assert.Equal(t, "some-url.com", route.URL.String()) 46 | } 47 | -------------------------------------------------------------------------------- /proto/e2e/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.helloworld; 4 | 5 | // The greeting service definition. 6 | service Greeter { 7 | // Sends a greeting 8 | rpc SayHello (HelloRequest) returns (HelloReply) {} 9 | } 10 | 11 | // The request message containing the user's name. 12 | message HelloRequest { 13 | string name = 1; 14 | } 15 | 16 | // The response message containing the greetings 17 | message HelloReply { 18 | string message = 1; 19 | } 20 | -------------------------------------------------------------------------------- /proto/kedge/config/backendpool.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | package kedge.config; 5 | 6 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 7 | 8 | import "kedge/config/grpc/backends/backend.proto"; 9 | import "kedge/config/http/backends/backend.proto"; 10 | 11 | 12 | /// Config is the top level configuration message for a backend pool. 13 | message BackendPoolConfig { 14 | message Grpc { 15 | repeated kedge.config.grpc.backends.Backend backends = 1; 16 | } 17 | message Http { 18 | repeated kedge.config.http.backends.Backend backends = 1; 19 | } 20 | 21 | repeated TlsServerConfig tls_server_configs = 1; 22 | Grpc grpc = 2; 23 | Http http = 3; 24 | 25 | } 26 | 27 | message TlsServerConfig { 28 | string name = 1 [(validator.field) = {regex: "^[a-z_.]{2,64}$"}]; 29 | // TODO(mwitkow): add tls-config declarations. 30 | } 31 | 32 | -------------------------------------------------------------------------------- /proto/kedge/config/common/adhoc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.common; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | 7 | /// Adhoc describes an adhoc proxying method that is not backed by a backend, but dials a "free form" DNS record. 8 | message Adhoc { 9 | /// dns_name_matcher matches the hostname that will be used to resolve A records. 10 | /// The names are matched with a * prefix. For example: 11 | /// - *.pod.cluster.local 12 | /// - *.my_namespace.svc.cluster.local 13 | /// - *.local 14 | /// The first rule that matches a DNS name will be used, and its ports will be checked. 15 | string dns_name_matcher = 1 [(validator.field) = {msg_exists : true}]; 16 | 17 | /// Port controls the :port behaviour of the URI requested. 18 | Port port = 2 [(validator.field) = {msg_exists : true}]; 19 | 20 | /// dns_name_replace is an optional replacement pattern to alter hostname before A records resolution. 21 | /// This is useful when exposed domain is different than local resolvable one. 22 | /// 23 | /// Example: 24 | /// Inside cluster you can resolve only 'abc.default.svc.cluster.local' however since you have multiple clusters 25 | /// you want this abc service/pod to be accessible as 'abc.default.svc.cluster1.example.com'. In this case you want 26 | /// to set dns_name_replace.pattern "cluster1.example.com" , dns_name_replace.substitution="cluster.local" 27 | Replace dns_name_replace = 3; 28 | 29 | /// Port controls how the :port part of the URI is processed. 30 | message Port { 31 | /// default is the default port used if no entry is present. 32 | /// This defaults to 80. 33 | uint32 default = 1; 34 | 35 | // TODO(mwitkow): Add SRV resolution that overrides default. 36 | 37 | /// allowed ports is a list of whitelisted ports that this Adhoc rule will allow. 38 | repeated uint32 allowed = 3; 39 | 40 | /// allowed_ranges is a list of whitelisted port ranges that this Adhoc rule will allow. 41 | repeated Range allowed_ranges = 4; 42 | message Range { 43 | /// from is an inclusive lower bound for the port range 44 | uint32 from = 1; 45 | /// to is an inclusive upper bound for the port range 46 | uint32 to = 2; 47 | } 48 | } 49 | 50 | message Replace { 51 | // pattern specified pattern to substitute the hostname with. If not pattern is not found error is returned (!). 52 | string pattern = 1 [(validator.field) = {msg_exists : true}]; 53 | string substitution = 2 [(validator.field) = {msg_exists : true}]; 54 | } 55 | // TODO(mwitkow): Add authorization. 56 | } -------------------------------------------------------------------------------- /proto/kedge/config/common/resolvers/resolvers.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.common.resolvers; 4 | 5 | /// SrvBackend describes a backend that is resolved and load balanced using SRV. 6 | message SrvResolver { 7 | /// dns_name specifies the address to look up using DNS SRV. Needs to be a FQDN. 8 | /// E.g. "_grpc._tcp.someservice.somenamespace.svc.cluster.local" 9 | string dns_name = 1; 10 | /// not recommended, but port override allows to ignore port form SRV record and use one defined here. 11 | /// Useful when there are multiple services identifed under the same domain. 12 | uint32 port_override = 2; 13 | } 14 | 15 | /// K8sResolver uses the Kubernetes Endpoints API to identify the service. 16 | /// It watched Endpoint API for changes using the pod's credentails to fetch the service information. 17 | message K8sResolver { 18 | // Common kube DNS name with optional port: "<|.namespace>(.whatever suffix)<|:port_name|:value number>" 19 | // to resolve by this resolver using endpoints API. 20 | // e.g "backend1.namespace1:http_port1" 21 | string dns_port_name = 1; 22 | } 23 | 24 | /// HostResolver describes a backend that is resolved and load balanced using host resultion with pinned port. 25 | message HostResolver { 26 | /// dns_name specifies the address to look up using A record. 27 | string dns_name = 1; 28 | /// port specified the port that the load balancer should go after host lookup. 29 | uint32 port = 2; 30 | } -------------------------------------------------------------------------------- /proto/kedge/config/director.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | 7 | import "kedge/config/common/adhoc.proto"; 8 | import "kedge/config/grpc/routes/routes.proto"; 9 | import "kedge/config/http/routes/routes.proto"; 10 | 11 | /// DirectorConfig is the top level configuration message the director. 12 | message DirectorConfig { 13 | message Grpc { 14 | repeated kedge.config.grpc.routes.Route routes = 1; 15 | repeated kedge.config.common.Adhoc adhoc_rules = 2; 16 | } 17 | message Http { 18 | repeated kedge.config.http.routes.Route routes = 1; 19 | repeated kedge.config.common.Adhoc adhoc_rules = 2; 20 | } 21 | 22 | Grpc grpc = 1 [(validator.field) = {msg_exists : true}]; 23 | Http http = 2 [(validator.field) = {msg_exists : true}]; 24 | } 25 | -------------------------------------------------------------------------------- /proto/kedge/config/grpc/backends/backend.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.grpc.backends; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | import "kedge/config/common/resolvers/resolvers.proto"; 7 | 8 | /// Backend data will be used to set up pool of gRPC ClientConns to specified endpoint that will be kept open. 9 | message Backend { 10 | /// name is the string identifying the bakcend in all other configs. 11 | string name = 1 [(validator.field) = {regex: "^[a-z_0-9.]{2,64}$"}]; 12 | 13 | /// balancer decides which balancing policy to use. 14 | Balancer balancer = 2; 15 | 16 | /// disable_conntracking turns off the /debug/events tracing and Prometheus monitoring of the pool sie for this backend. 17 | bool disable_conntracking = 3; 18 | 19 | /// security controls the TLS connection details for the backend. If not present, Insecure (plain text) mode is used. 20 | Security security = 4; 21 | 22 | /// interceptors controls what interceptors will be enabled for this backend. 23 | repeated Interceptor interceptors = 5; 24 | 25 | oneof resolver { 26 | common.resolvers.SrvResolver srv = 10; 27 | common.resolvers.K8sResolver k8s = 11; 28 | common.resolvers.HostResolver host = 12; 29 | } 30 | 31 | bool autogenerated = 6; 32 | } 33 | 34 | /// Balancer chooses which gRPC balancing policy to use. 35 | enum Balancer { 36 | // ROUND_ROBIN is the simpliest and default load balancing policy 37 | ROUND_ROBIN = 0; 38 | } 39 | 40 | message Interceptor { 41 | oneof interceptor { 42 | bool prometheus = 1; 43 | } 44 | } 45 | 46 | /// Security settings for a backend. 47 | message Security { 48 | /// insecure_skip_verify skips the server certificate verification completely. 49 | /// No TLS config (for testclient or server) will be used. This should *not* be used in production software. 50 | bool insecure_skip_verify = 1; 51 | 52 | /// config_name indicates the TlsServerConfig to be used for this connection. 53 | string config_name = 2; 54 | // TODO(mwitkow): add tls-config specification for server-side (CA certs etc.). 55 | // TODO(mwitkow): add tls-config specification for testclient-side (testclient-cert etc.). 56 | } 57 | 58 | -------------------------------------------------------------------------------- /proto/kedge/config/grpc/routes/routes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.grpc.routes; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | 7 | 8 | /// Route is a mapping between invoked gRPC requests and backends that should serve it. 9 | message Route { 10 | /// backend_name is the string identifying the backend to send data to. 11 | string backend_name = 1 [(validator.field) = {regex: "^[a-z_0-9.]{2,64}$"}]; 12 | 13 | /// service_name_matcher is a globbing expression that matches a full gRPC service name. 14 | /// For example a method call to 'com.example.MyService/Create' would be matched by: 15 | /// - com.example.MyService 16 | /// - com.example.* 17 | /// - com.* 18 | /// - * 19 | /// If not present, '*' is default. 20 | string service_name_matcher = 2; 21 | 22 | /// authority_host_matcher matches on the host part of the ':authority' header (a.k.a. Host header) enabling Virtual Host-like proxying. 23 | /// The matching is done through lower-case string-equality. 24 | /// If none are present, the route skips ':authority' checks. 25 | string authority_host_matcher = 3; 26 | 27 | /// metadata_matcher matches any gRPC inbound request metadata. 28 | /// Each key provided must find a match for the route to match. 29 | /// The matching is done through lower-case key match and explicit string-equality of values. 30 | /// If a given metadata entry has more than one string value, at least one of them needs to match. 31 | /// If none are present, the route skips metadata checks. 32 | map metadata_matcher = 4; 33 | 34 | /// authority_port_matcher is optional port matcher. It matches on the port part of the ':authority' header. 35 | // If 0 route will ignore port. 36 | uint32 authority_port_matcher = 5; 37 | 38 | bool autogenerated = 6; 39 | /// TODO(mwitkow): Add fields that require TLS Client auth, or :authorization keys. 40 | } 41 | -------------------------------------------------------------------------------- /proto/kedge/config/http/backends/backend.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.http.backends; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | import "kedge/config/common/resolvers/resolvers.proto"; 7 | 8 | /// Backend data will be used to set up pool of HTTP connection to specified endpoint that will be kept open. 9 | message Backend { 10 | /// name is the string identifying the backend in all other configs. 11 | string name = 1 [(validator.field) = {regex: "^[a-z_0-9.]{2,64}$"}]; 12 | 13 | /// balancer decides which balancing policy to use. 14 | Balancer balancer = 2; 15 | 16 | /// disable_conntracking turns off the /debug/events tracing and Prometheus monitoring of the pool sie for this backend. 17 | bool disable_conntracking = 3; 18 | 19 | /// security controls the TLS connection details for the backend (HTTPS). If not present, insecure HTTP mode is used. 20 | Security security = 4; 21 | 22 | // TODO(Bplotka): Uncomment when it will be implemented. 23 | /// interceptors controls what middleware will be available on every call made to this backend. 24 | /// These will be executed in order from left to right. 25 | //repeated Middleware middlewares = 5; 26 | 27 | oneof resolver { 28 | common.resolvers.SrvResolver srv = 10; 29 | common.resolvers.K8sResolver k8s = 11; 30 | common.resolvers.HostResolver host = 12; 31 | } 32 | 33 | bool autogenerated = 6; 34 | } 35 | 36 | /// Balancer chooses which HTTP backend balancing policy to use. 37 | enum Balancer { 38 | // ROUND_ROBIN is the simpliest and default load balancing policy 39 | ROUND_ROBIN = 0; 40 | } 41 | 42 | 43 | // TODO(bplotka): Implemment that. Not really supported now. 44 | message Middleware { 45 | message Retry { 46 | /// retry_count specifies how many times to retry. 47 | uint32 retry_count = 1; 48 | /// on_codes specifies the list of codes to retry on. 49 | repeated uint32 on_codes = 2; 50 | } 51 | 52 | oneof Middleware { 53 | Retry retry = 1; 54 | } 55 | } 56 | 57 | /// Security settings for a backend. 58 | message Security { 59 | /// insecure_skip_verify skips the server certificate verification completely. 60 | /// No TLS config (for testclient or server) will be used. This should *not* be used in production software. 61 | bool insecure_skip_verify = 1; 62 | 63 | /// config_name indicates the TlsServerConfig to be used for this connection. 64 | string config_name = 2; 65 | // TODO(mwitkow): add tls-config specification for server-side (CA certs etc.). 66 | // TODO(mwitkow): add tls-config specification for testclient-side (testclient-cert etc.). 67 | } 68 | 69 | -------------------------------------------------------------------------------- /proto/kedge/config/http/routes/routes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package kedge.config.http.routes; 4 | 5 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 6 | 7 | /// Route describes a mapping between a stable proxying endpoint and a pre-defined backend. 8 | message Route { 9 | /// backend_name is the string identifying the HTTP backend pool to send data to. 10 | string backend_name = 1 [(validator.field) = {regex: "^[a-z_0-9.]{2,64}$"}]; 11 | 12 | /// path_rules is a globbing expression that matches a URL path of the request. 13 | /// See: https://cloud.google.com/compute/docs/load-balancing/http/url-map 14 | /// If not present, '/*' is default. 15 | repeated string path_rules = 2; 16 | 17 | /// host_matcher matches on the ':authority' header (a.k.a. Host header) enabling Virtual Host-like proxying. 18 | /// The matching is done through lower-case string-equality. 19 | /// If none are present, the route skips ':authority' checks. 20 | string host_matcher = 3; 21 | 22 | /// metadata_matcher matches any HTTP inbound request Headers. 23 | /// Eeach key provided must find a match for the route to match. 24 | /// The matching is done through lower-case key match and explicit string-equality of values. 25 | /// If none are present, the route skips metadata checks. 26 | map header_matcher = 4; 27 | 28 | /// proxy_mode controlls what kind of inbound requests this route matches. See 29 | ProxyMode proxy_mode = 5; 30 | 31 | /// Optional port matcher. If 0 route will ignore port. 32 | // TODO(bplotka): Type is not consistend with authority_host_matcher 33 | uint32 port_matcher = 6; 34 | 35 | /// TODO(mwitkow): Add fields that require TLS Client auth, or :authorization keys. 36 | 37 | bool autogenerated = 7; 38 | } 39 | 40 | enum ProxyMode { 41 | ANY = 0; 42 | /// Reverse Proxy is when the FE serves an authority (Host) publicly and clients connect to that authority 43 | /// directly. This is used to expose publicly DNS-resolvable names. 44 | REVERSE_PROXY = 1; 45 | /// Forward Proxy is when the FE serves as an HTTP_PROXY for a browser or an application. The resolution of the 46 | /// backend is done by the FE itself, so non-public names can be addressed. 47 | /// This may be from the 90s, but it still is very useful. 48 | /// 49 | /// IMPORTANT: If you have a PAC file configured in Firefox, the HTTPS rule behaves differently than in Chrome. The 50 | /// proxied requests are not FORWARD_PROXY requests but REVERSE_PROXY_REQUESTS. 51 | FORWARD_PROXY = 2; 52 | } -------------------------------------------------------------------------------- /proto/winch/config/auth.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | package winch.config; 5 | 6 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 7 | 8 | /// AuthConfig is the top level configuration message for a winch auth. 9 | message AuthConfig { 10 | repeated AuthSource auth_sources = 1; 11 | } 12 | 13 | /// AuthSource specifies the kind of the backend auth we need to inject on winch reqeuest. 14 | message AuthSource { 15 | // name is an ID of auth source. It can be referenced inside winch routing. 16 | string name = 1; 17 | oneof type { 18 | DummyAccess dummy = 2; 19 | KubernetesAccess kube = 3; 20 | OIDCAccess oidc = 4; 21 | TokenAccess token = 5; 22 | GoogleServiceAccountOIDCAccess service_account_oidc = 6; 23 | } 24 | } 25 | 26 | /// KubernetesAccess is an convenient way of specifying auth for backend. It grabs the data inside already used 27 | /// ~/.kube/config (or any specified config path) and deducts the auth type based on that. NOTE that only these types are 28 | /// supported: 29 | /// - OIDC 30 | message KubernetesAccess { 31 | // User to reference access credentials from. 32 | string user = 1 [(validator.field) = {msg_exists : true}]; 33 | // By default ~/.kube/config as usual. 34 | string path = 2; 35 | // TODO(bplotka): Consider enabling login for OIDC from kube config. 36 | } 37 | 38 | // OIDCAccess is an access based on OIDC flow with user login (if refresh token is not in given path). 39 | message OIDCAccess { 40 | string provider = 1 [(validator.field) = {msg_exists : true}]; 41 | string client_id = 2 [(validator.field) = {msg_exists : true}]; 42 | string secret = 3 [(validator.field) = {msg_exists : true}]; 43 | repeated string scopes = 4; 44 | string path = 5; 45 | 46 | // login_callback_path specifies URL path for redirect URL to specify when doing OIDC login. 47 | // If empty login will be disabled which means in case of no refresh token or not valid one, error will be returned 48 | // thus not needing user interaction. 49 | string login_callback_path = 6; 50 | } 51 | 52 | // GoogleServiceAccountOIDCAccess is an access based on custom OIDC flow that supports Google Service Accounts. 53 | message GoogleServiceAccountOIDCAccess { 54 | string provider = 1 [(validator.field) = {msg_exists : true}]; 55 | string client_id = 2 [(validator.field) = {msg_exists : true}]; 56 | string secret = 3 [(validator.field) = {msg_exists : true}]; 57 | repeated string scopes = 4; 58 | 59 | // service_account_json_path specifies path to the JSON credential file that works as Service Account against certain 60 | // OIDC servers that supports it. 61 | string service_account_json_path = 5 [(validator.field) = {msg_exists : true}]; 62 | } 63 | 64 | // DummyAccess just directly passes specified value into auth header. If value is not specified it will return error. 65 | message DummyAccess { 66 | string value = 1; 67 | } 68 | 69 | // TokenAccess passes specified token into auth header as a bearer. 70 | message TokenAccess { 71 | string token = 1 [(validator.field) = {msg_exists : true}]; 72 | } -------------------------------------------------------------------------------- /proto/winch/config/mapper.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | package winch.config; 5 | 6 | import "github.com/mwitkow/go-proto-validators/validator.proto"; 7 | 8 | /// MapperConfig is the top level configuration message for a winch mapper. 9 | message MapperConfig { 10 | repeated Route routes = 1; 11 | } 12 | 13 | enum Protocol { 14 | ANY = 0; 15 | HTTP = 1; 16 | GRPC = 2; 17 | } 18 | 19 | message Route { 20 | // Optional auth injection. Reference to AuthSource. 21 | string backend_auth = 1; 22 | string proxy_auth = 2; 23 | oneof type { 24 | DirectRoute direct = 3; 25 | RegexpRoute regexp = 4; 26 | } 27 | Protocol protocol = 5; 28 | } 29 | 30 | /// Simplest routing mechanism using just direct mapping between dns and (proxy) kedge target. 31 | message DirectRoute { 32 | // Key needs to be in host:port format. 33 | string key = 1 [(validator.field) = {msg_exists : true}]; 34 | string url = 2 [(validator.field) = {msg_exists : true}]; 35 | } 36 | 37 | message RegexpRoute { 38 | // Regexp RE2 expression that will be applied on given domain:port 39 | string exp = 1 [(validator.field) = {msg_exists : true}]; 40 | 41 | // Kedge URL to be used if we have a match. It can be a string including variable from regexp expression in a form 42 | // of bash-like variable. E.g 43 | // exp = ([a-z0-9-].*)[.](?P[a-z0-9-].*)[.]internal[.]example[.]org 44 | // in that case you can use following variable: 45 | // - ${cluster} 46 | // NOTE: https:// prefix is required here. 47 | string url = 2 [(validator.field) = {msg_exists : true}]; 48 | } -------------------------------------------------------------------------------- /protogen/e2e/hello.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: e2e/hello.proto 3 | 4 | /* 5 | Package e2e_helloworld is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | e2e/hello.proto 9 | 10 | It has these top-level messages: 11 | HelloRequest 12 | HelloReply 13 | */ 14 | package e2e_helloworld 15 | 16 | import proto "github.com/golang/protobuf/proto" 17 | import fmt "fmt" 18 | import math "math" 19 | 20 | // Reference imports to suppress errors if they are not otherwise used. 21 | var _ = proto.Marshal 22 | var _ = fmt.Errorf 23 | var _ = math.Inf 24 | 25 | func (this *HelloRequest) Validate() error { 26 | return nil 27 | } 28 | func (this *HelloReply) Validate() error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /protogen/kedge/config/backendpool.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/backendpool.proto 3 | 4 | /* 5 | Package kedge_config is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/backendpool.proto 9 | kedge/config/director.proto 10 | 11 | It has these top-level messages: 12 | BackendPoolConfig 13 | TlsServerConfig 14 | DirectorConfig 15 | */ 16 | package kedge_config 17 | 18 | import regexp "regexp" 19 | import fmt "fmt" 20 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 21 | import proto "github.com/golang/protobuf/proto" 22 | import math "math" 23 | import _ "github.com/mwitkow/go-proto-validators" 24 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/grpc/backends" 25 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/http/backends" 26 | 27 | // Reference imports to suppress errors if they are not otherwise used. 28 | var _ = proto.Marshal 29 | var _ = fmt.Errorf 30 | var _ = math.Inf 31 | 32 | func (this *BackendPoolConfig) Validate() error { 33 | for _, item := range this.TlsServerConfigs { 34 | if item != nil { 35 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 36 | return go_proto_validators.FieldError("TlsServerConfigs", err) 37 | } 38 | } 39 | } 40 | if this.Grpc != nil { 41 | if err := go_proto_validators.CallValidatorIfExists(this.Grpc); err != nil { 42 | return go_proto_validators.FieldError("Grpc", err) 43 | } 44 | } 45 | if this.Http != nil { 46 | if err := go_proto_validators.CallValidatorIfExists(this.Http); err != nil { 47 | return go_proto_validators.FieldError("Http", err) 48 | } 49 | } 50 | return nil 51 | } 52 | func (this *BackendPoolConfig_Grpc) Validate() error { 53 | for _, item := range this.Backends { 54 | if item != nil { 55 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 56 | return go_proto_validators.FieldError("Backends", err) 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | func (this *BackendPoolConfig_Http) Validate() error { 63 | for _, item := range this.Backends { 64 | if item != nil { 65 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 66 | return go_proto_validators.FieldError("Backends", err) 67 | } 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | var _regex_TlsServerConfig_Name = regexp.MustCompile(`^[a-z_.]{2,64}$`) 74 | 75 | func (this *TlsServerConfig) Validate() error { 76 | if !_regex_TlsServerConfig_Name.MatchString(this.Name) { 77 | return go_proto_validators.FieldError("Name", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z_.]{2,64}$"`, this.Name)) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /protogen/kedge/config/common/adhoc.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/common/adhoc.proto 3 | 4 | /* 5 | Package kedge_config_common is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/common/adhoc.proto 9 | 10 | It has these top-level messages: 11 | Adhoc 12 | */ 13 | package kedge_config_common 14 | 15 | import fmt "fmt" 16 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 17 | import proto "github.com/golang/protobuf/proto" 18 | import math "math" 19 | import _ "github.com/mwitkow/go-proto-validators" 20 | 21 | // Reference imports to suppress errors if they are not otherwise used. 22 | var _ = proto.Marshal 23 | var _ = fmt.Errorf 24 | var _ = math.Inf 25 | 26 | func (this *Adhoc) Validate() error { 27 | if nil == this.Port { 28 | return go_proto_validators.FieldError("Port", fmt.Errorf("message must exist")) 29 | } 30 | if this.Port != nil { 31 | if err := go_proto_validators.CallValidatorIfExists(this.Port); err != nil { 32 | return go_proto_validators.FieldError("Port", err) 33 | } 34 | } 35 | if this.DnsNameReplace != nil { 36 | if err := go_proto_validators.CallValidatorIfExists(this.DnsNameReplace); err != nil { 37 | return go_proto_validators.FieldError("DnsNameReplace", err) 38 | } 39 | } 40 | return nil 41 | } 42 | func (this *Adhoc_Port) Validate() error { 43 | for _, item := range this.AllowedRanges { 44 | if item != nil { 45 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 46 | return go_proto_validators.FieldError("AllowedRanges", err) 47 | } 48 | } 49 | } 50 | return nil 51 | } 52 | func (this *Adhoc_Port_Range) Validate() error { 53 | return nil 54 | } 55 | func (this *Adhoc_Replace) Validate() error { 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /protogen/kedge/config/common/resolvers/resolvers.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/common/resolvers/resolvers.proto 3 | 4 | /* 5 | Package kedge_config_common_resolvers is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/common/resolvers/resolvers.proto 9 | 10 | It has these top-level messages: 11 | SrvResolver 12 | K8SResolver 13 | HostResolver 14 | */ 15 | package kedge_config_common_resolvers 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | 21 | // Reference imports to suppress errors if they are not otherwise used. 22 | var _ = proto.Marshal 23 | var _ = fmt.Errorf 24 | var _ = math.Inf 25 | 26 | func (this *SrvResolver) Validate() error { 27 | return nil 28 | } 29 | func (this *K8SResolver) Validate() error { 30 | return nil 31 | } 32 | func (this *HostResolver) Validate() error { 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /protogen/kedge/config/director.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/director.proto 3 | 4 | package kedge_config 5 | 6 | import fmt "fmt" 7 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 8 | import proto "github.com/golang/protobuf/proto" 9 | import math "math" 10 | import _ "github.com/mwitkow/go-proto-validators" 11 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/common" 12 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/grpc/routes" 13 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/http/routes" 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | func (this *DirectorConfig) Validate() error { 21 | if nil == this.Grpc { 22 | return go_proto_validators.FieldError("Grpc", fmt.Errorf("message must exist")) 23 | } 24 | if this.Grpc != nil { 25 | if err := go_proto_validators.CallValidatorIfExists(this.Grpc); err != nil { 26 | return go_proto_validators.FieldError("Grpc", err) 27 | } 28 | } 29 | if nil == this.Http { 30 | return go_proto_validators.FieldError("Http", fmt.Errorf("message must exist")) 31 | } 32 | if this.Http != nil { 33 | if err := go_proto_validators.CallValidatorIfExists(this.Http); err != nil { 34 | return go_proto_validators.FieldError("Http", err) 35 | } 36 | } 37 | return nil 38 | } 39 | func (this *DirectorConfig_Grpc) Validate() error { 40 | for _, item := range this.Routes { 41 | if item != nil { 42 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 43 | return go_proto_validators.FieldError("Routes", err) 44 | } 45 | } 46 | } 47 | for _, item := range this.AdhocRules { 48 | if item != nil { 49 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 50 | return go_proto_validators.FieldError("AdhocRules", err) 51 | } 52 | } 53 | } 54 | return nil 55 | } 56 | func (this *DirectorConfig_Http) Validate() error { 57 | for _, item := range this.Routes { 58 | if item != nil { 59 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 60 | return go_proto_validators.FieldError("Routes", err) 61 | } 62 | } 63 | } 64 | for _, item := range this.AdhocRules { 65 | if item != nil { 66 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 67 | return go_proto_validators.FieldError("AdhocRules", err) 68 | } 69 | } 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /protogen/kedge/config/grpc/backends/backend.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/grpc/backends/backend.proto 3 | 4 | /* 5 | Package kedge_config_grpc_backends is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/grpc/backends/backend.proto 9 | 10 | It has these top-level messages: 11 | Backend 12 | Interceptor 13 | Security 14 | */ 15 | package kedge_config_grpc_backends 16 | 17 | import regexp "regexp" 18 | import fmt "fmt" 19 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 20 | import proto "github.com/golang/protobuf/proto" 21 | import math "math" 22 | import _ "github.com/mwitkow/go-proto-validators" 23 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 24 | 25 | // Reference imports to suppress errors if they are not otherwise used. 26 | var _ = proto.Marshal 27 | var _ = fmt.Errorf 28 | var _ = math.Inf 29 | 30 | var _regex_Backend_Name = regexp.MustCompile(`^[a-z_0-9.]{2,64}$`) 31 | 32 | func (this *Backend) Validate() error { 33 | if !_regex_Backend_Name.MatchString(this.Name) { 34 | return go_proto_validators.FieldError("Name", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z_0-9.]{2,64}$"`, this.Name)) 35 | } 36 | if this.Security != nil { 37 | if err := go_proto_validators.CallValidatorIfExists(this.Security); err != nil { 38 | return go_proto_validators.FieldError("Security", err) 39 | } 40 | } 41 | for _, item := range this.Interceptors { 42 | if item != nil { 43 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 44 | return go_proto_validators.FieldError("Interceptors", err) 45 | } 46 | } 47 | } 48 | if oneOfNester, ok := this.GetResolver().(*Backend_Srv); ok { 49 | if oneOfNester.Srv != nil { 50 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Srv); err != nil { 51 | return go_proto_validators.FieldError("Srv", err) 52 | } 53 | } 54 | } 55 | if oneOfNester, ok := this.GetResolver().(*Backend_K8S); ok { 56 | if oneOfNester.K8S != nil { 57 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.K8S); err != nil { 58 | return go_proto_validators.FieldError("K8S", err) 59 | } 60 | } 61 | } 62 | if oneOfNester, ok := this.GetResolver().(*Backend_Host); ok { 63 | if oneOfNester.Host != nil { 64 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Host); err != nil { 65 | return go_proto_validators.FieldError("Host", err) 66 | } 67 | } 68 | } 69 | return nil 70 | } 71 | func (this *Interceptor) Validate() error { 72 | return nil 73 | } 74 | func (this *Security) Validate() error { 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /protogen/kedge/config/grpc/routes/routes.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/grpc/routes/routes.proto 3 | 4 | /* 5 | Package kedge_config_grpc_routes is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/grpc/routes/routes.proto 9 | 10 | It has these top-level messages: 11 | Route 12 | */ 13 | package kedge_config_grpc_routes 14 | 15 | import regexp "regexp" 16 | import fmt "fmt" 17 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 18 | import proto "github.com/golang/protobuf/proto" 19 | import math "math" 20 | import _ "github.com/mwitkow/go-proto-validators" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | var _regex_Route_BackendName = regexp.MustCompile(`^[a-z_0-9.]{2,64}$`) 28 | 29 | func (this *Route) Validate() error { 30 | if !_regex_Route_BackendName.MatchString(this.BackendName) { 31 | return go_proto_validators.FieldError("BackendName", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z_0-9.]{2,64}$"`, this.BackendName)) 32 | } 33 | // Validation of proto3 map<> fields is unsupported. 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /protogen/kedge/config/http/backends/backend.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/http/backends/backend.proto 3 | 4 | /* 5 | Package kedge_config_http_backends is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/http/backends/backend.proto 9 | 10 | It has these top-level messages: 11 | Backend 12 | Middleware 13 | Security 14 | */ 15 | package kedge_config_http_backends 16 | 17 | import regexp "regexp" 18 | import fmt "fmt" 19 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 20 | import proto "github.com/golang/protobuf/proto" 21 | import math "math" 22 | import _ "github.com/mwitkow/go-proto-validators" 23 | import _ "github.com/improbable-eng/kedge/protogen/kedge/config/common/resolvers" 24 | 25 | // Reference imports to suppress errors if they are not otherwise used. 26 | var _ = proto.Marshal 27 | var _ = fmt.Errorf 28 | var _ = math.Inf 29 | 30 | var _regex_Backend_Name = regexp.MustCompile(`^[a-z_0-9.]{2,64}$`) 31 | 32 | func (this *Backend) Validate() error { 33 | if !_regex_Backend_Name.MatchString(this.Name) { 34 | return go_proto_validators.FieldError("Name", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z_0-9.]{2,64}$"`, this.Name)) 35 | } 36 | if this.Security != nil { 37 | if err := go_proto_validators.CallValidatorIfExists(this.Security); err != nil { 38 | return go_proto_validators.FieldError("Security", err) 39 | } 40 | } 41 | if oneOfNester, ok := this.GetResolver().(*Backend_Srv); ok { 42 | if oneOfNester.Srv != nil { 43 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Srv); err != nil { 44 | return go_proto_validators.FieldError("Srv", err) 45 | } 46 | } 47 | } 48 | if oneOfNester, ok := this.GetResolver().(*Backend_K8S); ok { 49 | if oneOfNester.K8S != nil { 50 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.K8S); err != nil { 51 | return go_proto_validators.FieldError("K8S", err) 52 | } 53 | } 54 | } 55 | if oneOfNester, ok := this.GetResolver().(*Backend_Host); ok { 56 | if oneOfNester.Host != nil { 57 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Host); err != nil { 58 | return go_proto_validators.FieldError("Host", err) 59 | } 60 | } 61 | } 62 | return nil 63 | } 64 | func (this *Middleware) Validate() error { 65 | if oneOfNester, ok := this.GetMiddleware().(*Middleware_Retry_); ok { 66 | if oneOfNester.Retry != nil { 67 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Retry); err != nil { 68 | return go_proto_validators.FieldError("Retry", err) 69 | } 70 | } 71 | } 72 | return nil 73 | } 74 | func (this *Middleware_Retry) Validate() error { 75 | return nil 76 | } 77 | func (this *Security) Validate() error { 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /protogen/kedge/config/http/routes/routes.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: kedge/config/http/routes/routes.proto 3 | 4 | /* 5 | Package kedge_config_http_routes is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | kedge/config/http/routes/routes.proto 9 | 10 | It has these top-level messages: 11 | Route 12 | */ 13 | package kedge_config_http_routes 14 | 15 | import regexp "regexp" 16 | import fmt "fmt" 17 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 18 | import proto "github.com/golang/protobuf/proto" 19 | import math "math" 20 | import _ "github.com/mwitkow/go-proto-validators" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | var _regex_Route_BackendName = regexp.MustCompile(`^[a-z_0-9.]{2,64}$`) 28 | 29 | func (this *Route) Validate() error { 30 | if !_regex_Route_BackendName.MatchString(this.BackendName) { 31 | return go_proto_validators.FieldError("BackendName", fmt.Errorf(`value '%v' must be a string conforming to regex "^[a-z_0-9.]{2,64}$"`, this.BackendName)) 32 | } 33 | // Validation of proto3 map<> fields is unsupported. 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /protogen/winch/config/auth.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: winch/config/auth.proto 3 | 4 | /* 5 | Package winch_config is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | winch/config/auth.proto 9 | winch/config/mapper.proto 10 | 11 | It has these top-level messages: 12 | AuthConfig 13 | AuthSource 14 | KubernetesAccess 15 | OIDCAccess 16 | GoogleServiceAccountOIDCAccess 17 | DummyAccess 18 | TokenAccess 19 | MapperConfig 20 | Route 21 | DirectRoute 22 | RegexpRoute 23 | */ 24 | package winch_config 25 | 26 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 27 | import proto "github.com/golang/protobuf/proto" 28 | import fmt "fmt" 29 | import math "math" 30 | import _ "github.com/mwitkow/go-proto-validators" 31 | 32 | // Reference imports to suppress errors if they are not otherwise used. 33 | var _ = proto.Marshal 34 | var _ = fmt.Errorf 35 | var _ = math.Inf 36 | 37 | func (this *AuthConfig) Validate() error { 38 | for _, item := range this.AuthSources { 39 | if item != nil { 40 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 41 | return go_proto_validators.FieldError("AuthSources", err) 42 | } 43 | } 44 | } 45 | return nil 46 | } 47 | func (this *AuthSource) Validate() error { 48 | if oneOfNester, ok := this.GetType().(*AuthSource_Dummy); ok { 49 | if oneOfNester.Dummy != nil { 50 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Dummy); err != nil { 51 | return go_proto_validators.FieldError("Dummy", err) 52 | } 53 | } 54 | } 55 | if oneOfNester, ok := this.GetType().(*AuthSource_Kube); ok { 56 | if oneOfNester.Kube != nil { 57 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Kube); err != nil { 58 | return go_proto_validators.FieldError("Kube", err) 59 | } 60 | } 61 | } 62 | if oneOfNester, ok := this.GetType().(*AuthSource_Oidc); ok { 63 | if oneOfNester.Oidc != nil { 64 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Oidc); err != nil { 65 | return go_proto_validators.FieldError("Oidc", err) 66 | } 67 | } 68 | } 69 | if oneOfNester, ok := this.GetType().(*AuthSource_Token); ok { 70 | if oneOfNester.Token != nil { 71 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Token); err != nil { 72 | return go_proto_validators.FieldError("Token", err) 73 | } 74 | } 75 | } 76 | if oneOfNester, ok := this.GetType().(*AuthSource_ServiceAccountOidc); ok { 77 | if oneOfNester.ServiceAccountOidc != nil { 78 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.ServiceAccountOidc); err != nil { 79 | return go_proto_validators.FieldError("ServiceAccountOidc", err) 80 | } 81 | } 82 | } 83 | return nil 84 | } 85 | func (this *KubernetesAccess) Validate() error { 86 | return nil 87 | } 88 | func (this *OIDCAccess) Validate() error { 89 | return nil 90 | } 91 | func (this *GoogleServiceAccountOIDCAccess) Validate() error { 92 | return nil 93 | } 94 | func (this *DummyAccess) Validate() error { 95 | return nil 96 | } 97 | func (this *TokenAccess) Validate() error { 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /protogen/winch/config/mapper.validator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-gogo. DO NOT EDIT. 2 | // source: winch/config/mapper.proto 3 | 4 | package winch_config 5 | 6 | import go_proto_validators "github.com/mwitkow/go-proto-validators" 7 | import proto "github.com/golang/protobuf/proto" 8 | import fmt "fmt" 9 | import math "math" 10 | import _ "github.com/mwitkow/go-proto-validators" 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | func (this *MapperConfig) Validate() error { 18 | for _, item := range this.Routes { 19 | if item != nil { 20 | if err := go_proto_validators.CallValidatorIfExists(item); err != nil { 21 | return go_proto_validators.FieldError("Routes", err) 22 | } 23 | } 24 | } 25 | return nil 26 | } 27 | func (this *Route) Validate() error { 28 | if oneOfNester, ok := this.GetType().(*Route_Direct); ok { 29 | if oneOfNester.Direct != nil { 30 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Direct); err != nil { 31 | return go_proto_validators.FieldError("Direct", err) 32 | } 33 | } 34 | } 35 | if oneOfNester, ok := this.GetType().(*Route_Regexp); ok { 36 | if oneOfNester.Regexp != nil { 37 | if err := go_proto_validators.CallValidatorIfExists(oneOfNester.Regexp); err != nil { 38 | return go_proto_validators.FieldError("Regexp", err) 39 | } 40 | } 41 | } 42 | return nil 43 | } 44 | func (this *DirectRoute) Validate() error { 45 | return nil 46 | } 47 | func (this *RegexpRoute) Validate() error { 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /scripts/protogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generates protobuf Go datastructures from the proto directory. 3 | 4 | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" 5 | PROTOBUF_DIR=${PROTOBUF_DIR-${ROOT_DIR}/proto} 6 | PROTOGEN_DIR=protogen 7 | GENERATION_DIR=${GENERATION_DIR-${ROOT_DIR}/${PROTOGEN_DIR}} 8 | IMPORT_PREFIX="github.com/improbable-eng/kedge/${PROTOGEN_DIR}" 9 | 10 | # Builds all .proto files in a given package directory. 11 | # NOTE: All .proto files in a given package must be processed *together*, otherwise the self-referencing 12 | # between files in the same proto package will not work. 13 | function proto_build_dir { 14 | DIR_FULL=${1} 15 | DIR_REL=${1##${PROTOBUF_DIR}} 16 | DIR_REL=${DIR_REL#/} 17 | echo -n "proto_build: $DIR_REL " 18 | mkdir -p ${GENERATION_DIR}/${DIR_REL} 2> /dev/null 19 | PATH=${GOPATH}/bin:$PATH protoc \ 20 | --proto_path=${PROTOBUF_DIR} \ 21 | --proto_path=${GOPATH}/src/github.com/gogo/protobuf/protobuf \ 22 | --proto_path=${GOPATH}/src \ 23 | --go_out=plugins=grpc:${GENERATION_DIR} \ 24 | --govalidators_out=${GENERATION_DIR} \ 25 | ${DIR_FULL}/*.proto || exit $? 26 | fix_imports ${GENERATION_DIR}/${DIR_REL} 27 | echo "DONE" 28 | } 29 | 30 | function fix_imports { 31 | DIR_FULL=${1} 32 | for file in $(ls ${DIR_FULL}/*.go 2>/dev/null); do 33 | # This is a massive hack (prefix of "kedge") 34 | # See https://github.com/golang/protobuf/issues/63 35 | sed --in-place='' -r "s~^import(.*) \"kedge(.*)\"$~import \1 \"${IMPORT_PREFIX}/kedge\2\"~" ${file}; 36 | done 37 | } 38 | 39 | # Generate files for each proto package directory. 40 | for dir in `find -L ${PROTOBUF_DIR} -type d`; do 41 | if [[ "$dir" == ${PROTOGEN_DIR} ]]; then 42 | continue 43 | fi 44 | if [ -n "$(ls $dir/*.proto 2>/dev/null)" ]; then 45 | proto_build_dir ${dir} || exit 1 46 | fi 47 | done 48 | -------------------------------------------------------------------------------- /tools/discovery/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "time" 10 | 11 | "github.com/improbable-eng/kedge/pkg/discovery" 12 | "github.com/improbable-eng/kedge/pkg/sharedflags" 13 | pb_config "github.com/improbable-eng/kedge/protogen/kedge/config" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ( 18 | flagLogLevel = sharedflags.Set.String("log_level", "info", "Log level") 19 | ) 20 | 21 | func main() { 22 | if err := sharedflags.Set.Parse(os.Args); err != nil { 23 | logrus.WithError(err).Fatal("failed parsing flags") 24 | } 25 | 26 | lvl, err := logrus.ParseLevel(*flagLogLevel) 27 | if err != nil { 28 | logrus.WithError(err).Fatalf("Cannot parse log level: %s", *flagLogLevel) 29 | } 30 | logrus.SetLevel(lvl) 31 | 32 | logger := logrus.StandardLogger() 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | err = generateRoutings(ctx, logger) 37 | if err != nil { 38 | logger.WithError(err).Fatal("Failed to generate Routings") 39 | } 40 | } 41 | 42 | func generateRoutings(ctx context.Context, logger logrus.FieldLogger) error { 43 | backendDiscovery, err := discovery.NewFromFlags(logger, &pb_config.DirectorConfig{}, &pb_config.BackendPoolConfig{}) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | director, backendpool, err := backendDiscovery.DiscoverOnce(ctx, 3*time.Second) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | directorCfg, err := json.MarshalIndent(director, "", " ") 54 | if err != nil { 55 | return err 56 | } 57 | 58 | backendpoolCfg, err := json.MarshalIndent(backendpool, "", " ") 59 | if err != nil { 60 | return err 61 | } 62 | 63 | fmt.Println(string(directorCfg)) 64 | fmt.Println(string(backendpoolCfg)) 65 | return nil 66 | 67 | } 68 | -------------------------------------------------------------------------------- /tools/loadtest/README.md: -------------------------------------------------------------------------------- 1 | # Kedge Load Tester 2 | 3 | This tool is aimed to perform stress test of target Kedge endpoint. It allows 4 | to set up X workers that will perform simple GET HTTP request for defined resource every Y seconds. The goal is to 5 | test how many QPS (queries per second) Kedge with or without Winch can handle. 6 | 7 | By specifying duration of the test, we calculate QPS from single load test as (X / Y) QPS. 8 | 9 | ## Usage 10 | 11 | The main required setup is to define what will be you backend you are accessing. It needs to be a quick, lightweight resource. 12 | Any kubernetes health endpoint seems reasonable, but make sure to not use Kedge own healthz endpoint, to have just proxy 13 | results (not impacted by handling healthz load). It is recommended to use lot's of replicas of the backend to actually hit 14 | Kedge limits. 15 | 16 | NOTE: ulimit commands are to increase number of file descriptors we can open. You might want to increase hard limit of these if needed. 17 | NOTE: we are using health endpoint, so our expected response is OK response from it. 18 | 19 | ### Through Winch 20 | This test will pass all request through winch. This is useful to estimate where is the bottleneck in your setup. 21 | 22 | ``` 23 | ulimit -n 20000 24 | go run tools/loadtest/*.go \ 25 | --winch_url=http://127.0.0.1:8070 \ 26 | --scenario_yaml=' 27 | target_url: http:// 28 | duration: 5m 29 | workers: 300 30 | tick_on: 1s 31 | expected_response: "{\"is_ok\":true}"' 32 | ``` 33 | This will start 300 go routines that will try to GET target every second through winch to maintain 300 QPS for 5 minutes. 34 | In this case your local winch is deciding what kedge we load test. 35 | 36 | ### Directly Kedge 37 | In this mode, loadtest will behave as winch, so it requires almost the same flags (except mapper.json) 38 | 39 | ``` 40 | ulimit -n 20000 41 | go run tools/loadtest/*.go \ 42 | --kedge_url= \ 43 | --auth_config_path= \ 44 | --client_tls_root_ca_files= \ 45 | --auth_source_name= \ 46 | --scenario_yaml=' 47 | target_url: http:// 48 | duration: 5m 49 | workers: 300 50 | tick_on: 1s 51 | expected_response: "{\"is_ok\":true}"' 52 | ``` 53 | This will start 300 go routines that will try to GET target every second against target Kedge to maintain 300 QPS for 5 minutes. 54 | 55 | ### Remote test 56 | 57 | In root directory you can find `Dockerfile_loadtest` that enables to build an interactive container and spin that as a pod e.g like this: 58 | ``` 59 | docker build -t "" -f Dockerfile_loadtester . 60 | # Push your docker image to the place accessible by your k8s 61 | 62 | uuid=$(uuidgen | cut -c1-5) 63 | kubectl run kedge-loadtest-${uuid} \ 64 | --rm -i --tty -v --image "" -- bash 65 | ``` 66 | 67 | It is interactive to make sure you start all clients in the same time and gather output (there is no sync between loadtest instances) 68 | 69 | ## Output 70 | 71 | Output is in form of Prometheus metrics with aggregation of errors found during test. 72 | 73 | ## TODO: 74 | * [ ] Prometheus scrape endpoint to allow any Prometheus to check load test results. -------------------------------------------------------------------------------- /tools/resolver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | k8sresolver "github.com/improbable-eng/kedge/pkg/resolvers/k8s" 11 | "github.com/improbable-eng/kedge/pkg/sharedflags" 12 | "github.com/oklog/run" 13 | "github.com/pkg/errors" 14 | "github.com/sirupsen/logrus" 15 | "google.golang.org/grpc/naming" 16 | ) 17 | 18 | var ( 19 | flagLogLevel = sharedflags.Set.String("log_level", "info", "Log level") 20 | flagTarget = sharedflags.Set.String("target", "", "Target entry to resolver and watch for new resolutions") 21 | ) 22 | 23 | func main() { 24 | if err := sharedflags.Set.Parse(os.Args); err != nil { 25 | logrus.WithError(err).Fatal("failed parsing flags") 26 | } 27 | 28 | lvl, err := logrus.ParseLevel(*flagLogLevel) 29 | if err != nil { 30 | logrus.WithError(err).Fatalf("Cannot parse log level: %s", *flagLogLevel) 31 | } 32 | logrus.SetLevel(lvl) 33 | logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) 34 | 35 | resolver, err := k8sresolver.NewFromFlags(logrus.StandardLogger()) 36 | if err != nil { 37 | logrus.WithError(err).Fatal("Cannot create k8s resolver") 38 | } 39 | 40 | watcher, err := resolver.Resolve(*flagTarget) 41 | if err != nil { 42 | logrus.WithError(err).Fatalf("Failed to resolve %s", *flagTarget) 43 | } 44 | defer watcher.Close() 45 | 46 | var ( 47 | g run.Group 48 | state = map[string]struct{}{} 49 | ) 50 | 51 | { 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | g.Add(func() error { 54 | for ctx.Err() == nil { 55 | updates, err := watcher.Next() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | var msg string 61 | for _, up := range updates { 62 | if up.Op == naming.Add { 63 | state[up.Addr] = struct{}{} 64 | } else { 65 | delete(state, up.Addr) 66 | } 67 | msg += fmt.Sprintf("[op: %v, addr: %s]", up.Op, up.Addr) 68 | } 69 | fmt.Printf("Got Updates: %s\nOverall state: %v\n", msg, state) 70 | } 71 | 72 | return nil 73 | }, func(error) { 74 | watcher.Close() 75 | cancel() 76 | }) 77 | } 78 | { 79 | cancel := make(chan struct{}) 80 | g.Add(func() error { 81 | return interrupt(cancel) 82 | }, func(error) { 83 | logrus.Infof("\nReceived an interrupt, stopping services...\n") 84 | close(cancel) 85 | }) 86 | } 87 | 88 | logrus.Info("Starting standalone resolver") 89 | if err := g.Run(); err != nil { 90 | logrus.WithError(err).Fatal("Command finished.") 91 | } 92 | } 93 | 94 | func interrupt(cancel <-chan struct{}) error { 95 | c := make(chan os.Signal, 1) 96 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 97 | select { 98 | case <-c: 99 | return nil 100 | case <-cancel: 101 | return errors.New("canceled") 102 | } 103 | } 104 | --------------------------------------------------------------------------------