From 27ba623ab57d162ce6135a4d485cebf3073e81a7 Mon Sep 17 00:00:00 2001 From: Jan Lauber Date: Mon, 5 Jun 2023 09:36:52 +0200 Subject: [PATCH 1/6] feat: enhance kubelab-agent and shell Signed-off-by: Jan Lauber --- kubelab-agent/.dockerignore | 11 + kubelab-agent/.gitignore | 1 + kubelab-agent/Dockerfile | 47 ++- kubelab-agent/Makefile | 144 ++++++++ kubelab-agent/cmd/kubelab-agent/config.go | 81 +++++ kubelab-agent/cmd/kubelab-agent/log.go | 39 ++ kubelab-agent/cmd/kubelab-agent/main.go | 143 ++++++++ kubelab-agent/cmd/kubelab-agent/middleware.go | 20 ++ kubelab-agent/go.mod | 34 +- kubelab-agent/go.sum | 337 ++++++++++++++++++ .../internal/constants/key_sequences.go | 12 + kubelab-agent/internal/log/constants.go | 41 +++ kubelab-agent/internal/log/log.go | 116 ++++++ kubelab-agent/old.go | 87 ----- kubelab-agent/pkg/xtermjs/constants.go | 11 + .../pkg/xtermjs/handler_websocket.go | 221 ++++++++++++ kubelab-agent/pkg/xtermjs/types.go | 41 +++ kubelab-agent/pkg/xtermjs/utils.go | 33 ++ kubelab-ui/src/lib/components/base/Nav.svelte | 24 +- kubelab-ui/src/lib/terminal.ts | 113 ++---- 20 files changed, 1361 insertions(+), 195 deletions(-) create mode 100644 kubelab-agent/.dockerignore create mode 100644 kubelab-agent/.gitignore create mode 100644 kubelab-agent/Makefile create mode 100644 kubelab-agent/cmd/kubelab-agent/config.go create mode 100644 kubelab-agent/cmd/kubelab-agent/log.go create mode 100644 kubelab-agent/cmd/kubelab-agent/main.go create mode 100644 kubelab-agent/cmd/kubelab-agent/middleware.go create mode 100644 kubelab-agent/internal/constants/key_sequences.go create mode 100644 kubelab-agent/internal/log/constants.go create mode 100644 kubelab-agent/internal/log/log.go delete mode 100644 kubelab-agent/old.go create mode 100644 kubelab-agent/pkg/xtermjs/constants.go create mode 100644 kubelab-agent/pkg/xtermjs/handler_websocket.go create mode 100644 kubelab-agent/pkg/xtermjs/types.go create mode 100644 kubelab-agent/pkg/xtermjs/utils.go diff --git a/kubelab-agent/.dockerignore b/kubelab-agent/.dockerignore new file mode 100644 index 0000000..7928904 --- /dev/null +++ b/kubelab-agent/.dockerignore @@ -0,0 +1,11 @@ +.Dockerfile.yaml +.hadolint.yaml +Dockerfile +LICENSE +Makefile +Makefile.properties +README.md +deploy +examples +node_modules +vendor diff --git a/kubelab-agent/.gitignore b/kubelab-agent/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/kubelab-agent/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/kubelab-agent/Dockerfile b/kubelab-agent/Dockerfile index f338a86..3378560 100644 --- a/kubelab-agent/Dockerfile +++ b/kubelab-agent/Dockerfile @@ -1,18 +1,24 @@ -# Build stage -FROM golang:1.19 as build -WORKDIR /go/src/github.com/natrongmbh/kubelab-agent -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kubelab-agent . +FROM golang:1.20-alpine AS backend +WORKDIR /go/src/kubelab-agent +COPY ./cmd ./cmd +COPY ./internal ./internal +COPY ./pkg ./pkg +COPY ./go.mod . +COPY ./go.sum . +ENV CGO_ENABLED=0 +RUN go mod vendor +ARG VERSION_INFO=dev-build +RUN go build -a -v \ + -ldflags " \ + -s -w \ + -extldflags 'static' \ + -X main.VersionInfo='${VERSION_INFO}' \ + " \ + -o ./bin/kubelab-agent \ + ./cmd/kubelab-agent -# Production build FROM ubuntu:22.04 - WORKDIR /app - -# Adding group and user -RUN groupadd -g 1001 kubelab-agent -RUN useradd -s /bin/bash -u 1001 -g 1001 -m kubelab-agent - # Adding base utilities RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -49,10 +55,13 @@ RUN curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/ # Installing cri-o RUN curl https://raw.githubusercontent.com/cri-o/cri-o/main/scripts/get | bash -# Podman and CRI-O are not officially available for Ubuntu, it may be necessary to build them from source or use another base image if you need to use them. - -COPY --from=build --chown=kubelab-agent:kubelab-agent /go/src/github.com/natrongmbh/kubelab-agent/kubelab-agent ./ - -EXPOSE 8090 - -CMD ["./kubelab-agent"] +COPY --from=backend /go/src/kubelab-agent/bin/kubelab-agent /app/kubelab-agent +RUN ln -s /app/kubelab-agent /usr/bin/kubelab-agent +RUN groupadd -g 1001 kubelab-agent +RUN useradd -s /bin/bash -u 1001 -g 1001 -m kubelab-agent +RUN mkdir -p /home/kubelab-agent +RUN chown kubelab-agent:kubelab-agent /app -R +WORKDIR / +ENV WORKDIR=/app +USER kubelab-agent +ENTRYPOINT ["/app/kubelab-agent"] diff --git a/kubelab-agent/Makefile b/kubelab-agent/Makefile new file mode 100644 index 0000000..79f3a2e --- /dev/null +++ b/kubelab-agent/Makefile @@ -0,0 +1,144 @@ +changes ?= $(shell git status --porcelain --untracked-files=no | wc -l) +version ?= $(shell git rev-parse HEAD | head -c 8) +ifeq ($(shell test $(changes) -gt 0; echo $$?),0) +version := $(version)-dev +endif +export_path ?= ./images + +# use this to override/set settings +-include Makefile.properties + +# image_namespace specifies THIS_PART/namespace/image:tag of the Docker image path +image_registry ?= docker.io +# image_namespace specifies docker.io/THIS_PART/image:tag of the Docker image path +image_namespace ?= zephinzer +# image_name specifies docker.io/namespace/THIS_PART:tag of the Docker image path +image_name ?= kubelab-agent +# image_name specifies docker.io/namespace/image:THIS_PART of the Docker image path +image_tag ?= $(version) + +image_url := $(image_registry)/$(image_namespace)/$(image_name) + +binary_name := $(image_name)-${GOOS}-${GOARCH}${BIN_EXT} + +# initialises the project (run this before all else) +init: + go mod vendor + +# start the application (use this in development) +start: + go run ./cmd/kubelab-agent + +# runs the application in packaged form +run: package + docker run -it -p 8376:8376 $(image_url):latest + +# builds the application binary +build: + CGO_ENABLED=0 \ + go build -a -v \ + -ldflags " \ + -s -w \ + -extldflags 'static' \ + -X main.VersionInfo='$(version)' \ + " \ + -o ./bin/$(binary_name) ./cmd/kubelab-agent + +# compresses the application binary +compress: + ls -lah ./bin/$(binary_name) + upx -9 -v -o ./bin/.$(binary_name) \ + ./bin/$(binary_name) + upx -t ./bin/.$(binary_name) + rm -rf ./bin/$(binary_name) + mv ./bin/.$(binary_name) \ + ./bin/$(binary_name) + sha256sum -b ./bin/$(binary_name) \ + | cut -f 1 -d ' ' > ./bin/$(binary_name).sha256 + ls -lah ./bin/$(binary_name) + +# lints this image for best-practices +lint: + hadolint ./Dockerfile + +# tests this iamge for structure integrity +test: package + container-structure-test test --config ./.Dockerfile.yaml --image $(image_url):latest + +# scans this image for known vulnerabilities +scan: package + trivy image \ + --output trivy.json \ + --format json \ + $(image_url):$(version) + trivy image $(image_url):$(version) + +# packages project into a docker image +package: + docker build ${build_args} \ + --build-arg VERSION_INFO=$(version) \ + --tag $(image_url):latest \ + . + docker tag $(image_url):latest \ + $(image_url):$(version) + +# packages example project in this project into a docker image using the docker build cache +package-example: package + if [ "${id}" = "" ]; then \ + printf -- '\033[1m\033[31m$${id} was not specified\033[0m\n'; \ + exit 1; \ + fi + docker build \ + --build-arg IMAGE_NAMESPACE=$(image_registry)/$(image_namespace) \ + --build-arg IMAGE_NAME=$(image_name) \ + --build-arg IMAGE_TAG=$(version) \ + --tag $(image_url)-${id}:latest \ + --file ./examples/${id}/Dockerfile \ + . + docker tag $(image_url)-${id}:latest \ + $(image_url)-${id}:$(version) + +# publishes primary docker image of this project +publish: + @$(MAKE) package + @$(MAKE) publish-ci + +# publishes primary docker image of this project without running package +publish-ci: + -docker push $(image_url):latest + docker push $(image_url):$(version) + +# publishes example docker image of this project +publish-example: + @$(MAKE) package-example id=${id} + @$(MAKE) publish-example-ci id=${id} + +# publishes example docker image of this project without running package +publish-example-ci: + -docker push $(image_url)-${id}:latest + docker push $(image_url)-${id}:$(version) + +# exports this image into a tarball (use in ci cache) +export: package + mkdir -p $(export_path) + docker save $(image_namespace)/$(image_name):latest -o $(export_path)/$(image_namespace)-$(image_name).tar.gz + +# exports the example image into a tarball (use in ci cache) +export-example: package-example + mkdir -p $(export_path) + docker save $(image_namespace)/$(image_name)-${id}:latest -o $(export_path)/$(image_namespace)-$(image_name)-${id}.tar.gz + +# import this image from a tarball (use in ci cache) +import: + mkdir -p $(export_path) + -docker load -i $(export_path)/$(image_namespace)-$(image_name).tar.gz + +# import the example image into a tarball (use in ci cache) +import-example: + mkdir -p $(export_path) + -docker load -i $(export_path)/$(image_namespace)-$(image_name)-${id}.tar.gz + +.ssh: + mkdir -p ./.ssh + ssh-keygen -t rsa -b 8192 -f ./.ssh/id_rsa -q -N "" + cat ./.ssh/id_rsa | base64 -w 0 > ./.ssh/id_rsa.base64 diff --git a/kubelab-agent/cmd/kubelab-agent/config.go b/kubelab-agent/cmd/kubelab-agent/config.go new file mode 100644 index 0000000..5200bff --- /dev/null +++ b/kubelab-agent/cmd/kubelab-agent/config.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/natrontech/kubelab-agent/internal/log" + "github.com/usvc/go-config" +) + +var conf = config.Map{ + "allowed-hostnames": &config.StringSlice{ + Default: []string{"localhost"}, + Usage: "comma-delimited list of hostnames that are allowed to connect to the websocket", + Shorthand: "H", + }, + "arguments": &config.StringSlice{ + Default: []string{}, + Usage: "comma-delimited list of arguments that should be passed to the terminal command", + Shorthand: "r", + }, + "command": &config.String{ + Default: "/bin/bash", + Usage: "absolute path to command to run", + Shorthand: "t", + }, + "connection-error-limit": &config.Int{ + Default: 10, + Usage: "number of times a connection should be re-attempted before it's considered dead", + Shorthand: "l", + }, + "keepalive-ping-timeout": &config.Int{ + Default: 20, + Usage: "maximum duration in seconds between a ping message and its response to tolerate", + Shorthand: "k", + }, + "max-buffer-size-bytes": &config.Int{ + Default: 512, + Usage: "maximum length of input from terminal", + Shorthand: "B", + }, + "log-format": &config.String{ + Default: "text", + Usage: fmt.Sprintf("defines the format of the logs - one of ['%s']", strings.Join(log.ValidFormatStrings, "', '")), + }, + "log-level": &config.String{ + Default: "debug", + Usage: fmt.Sprintf("defines the minimum level of logs to show - one of ['%s']", strings.Join(log.ValidLevelStrings, "', '")), + }, + "path-liveness": &config.String{ + Default: "/healthz", + Usage: "url path to the liveness probe endpoint", + }, + "path-metrics": &config.String{ + Default: "/metrics", + Usage: "url path to the prometheus metrics endpoint", + }, + "path-readiness": &config.String{ + Default: "/readyz", + Usage: "url path to the readiness probe endpoint", + }, + "path-xtermjs": &config.String{ + Default: "/xterm.js", + Usage: "url path to the endpoint that xterm.js should attach to", + }, + "server-addr": &config.String{ + Default: "0.0.0.0", + Usage: "ip interface the server should listen on", + Shorthand: "a", + }, + "server-port": &config.Int{ + Default: 8376, + Usage: "port the server should listen on", + Shorthand: "p", + }, + "workdir": &config.String{ + Default: ".", + Usage: "working directory", + Shorthand: "w", + }, +} diff --git a/kubelab-agent/cmd/kubelab-agent/log.go b/kubelab-agent/cmd/kubelab-agent/log.go new file mode 100644 index 0000000..2cb92a6 --- /dev/null +++ b/kubelab-agent/cmd/kubelab-agent/log.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "runtime" + + "github.com/natrontech/kubelab-agent/internal/log" +) + +// createRequestLog returns a logger with relevant request fields +func createRequestLog(r *http.Request, additionalFields ...map[string]interface{}) log.Logger { + fields := map[string]interface{}{} + if len(additionalFields) > 0 { + fields = additionalFields[0] + } + if r != nil { + fields["host"] = r.Host + fields["remote_addr"] = r.RemoteAddr + fields["method"] = r.Method + fields["protocol"] = r.Proto + fields["path"] = r.URL.Path + fields["request_url"] = r.URL.String() + fields["user_agent"] = r.UserAgent() + fields["cookies"] = r.Cookies() + } + return log.WithFields(fields) +} + +func createMemoryLog() log.Logger { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + return log.WithFields(map[string]interface{}{ + "alloc": memStats.Alloc, + "heap_alloc": memStats.HeapAlloc, + "total_alloc": memStats.TotalAlloc, + "sys_alloc": memStats.Sys, + "gc_count": memStats.NumGC, + }) +} diff --git a/kubelab-agent/cmd/kubelab-agent/main.go b/kubelab-agent/cmd/kubelab-agent/main.go new file mode 100644 index 0000000..7aff378 --- /dev/null +++ b/kubelab-agent/cmd/kubelab-agent/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/natrontech/kubelab-agent/internal/log" + "github.com/natrontech/kubelab-agent/pkg/xtermjs" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spf13/cobra" +) + +var VersionInfo string + +func main() { + if VersionInfo == "" { + VersionInfo = "dev" + } + command := cobra.Command{ + Use: "kubelab-agent", + Short: "Creates a web-based shell using xterm.js that links to an actual shell", + Version: VersionInfo, + RunE: runE, + } + conf.ApplyToCobra(&command) + command.Execute() +} + +func runE(_ *cobra.Command, _ []string) error { + // initialise the logger + log.Init(log.Format(conf.GetString("log-format")), log.Level(conf.GetString("log-level"))) + + // debug stuff + command := conf.GetString("command") + connectionErrorLimit := conf.GetInt("connection-error-limit") + arguments := conf.GetStringSlice("arguments") + allowedHostnames := conf.GetStringSlice("allowed-hostnames") + keepalivePingTimeout := time.Duration(conf.GetInt("keepalive-ping-timeout")) * time.Second + maxBufferSizeBytes := conf.GetInt("max-buffer-size-bytes") + pathLiveness := conf.GetString("path-liveness") + pathMetrics := conf.GetString("path-metrics") + pathReadiness := conf.GetString("path-readiness") + pathXTermJS := conf.GetString("path-xtermjs") + serverAddress := conf.GetString("server-addr") + serverPort := conf.GetInt("server-port") + workingDirectory := conf.GetString("workdir") + if !path.IsAbs(workingDirectory) { + wd, err := os.Getwd() + if err != nil { + message := fmt.Sprintf("failed to get working directory: %s", err) + log.Error(message) + return errors.New(message) + } + workingDirectory = path.Join(wd, workingDirectory) + } + log.Infof("working directory : '%s'", workingDirectory) + log.Infof("command : '%s'", command) + log.Infof("arguments : ['%s']", strings.Join(arguments, "', '")) + + log.Infof("allowed hosts : ['%s']", strings.Join(allowedHostnames, "', '")) + log.Infof("connection error limit: %v", connectionErrorLimit) + log.Infof("keepalive ping timeout: %v", keepalivePingTimeout) + log.Infof("max buffer size : %v bytes", maxBufferSizeBytes) + log.Infof("server address : '%s' ", serverAddress) + log.Infof("server port : %v", serverPort) + + log.Infof("liveness checks path : '%s'", pathLiveness) + log.Infof("readiness checks path : '%s'", pathReadiness) + log.Infof("metrics endpoint path : '%s'", pathMetrics) + log.Infof("xtermjs endpoint path : '%s'", pathXTermJS) + + // configure routing + router := mux.NewRouter() + + // this is the endpoint for xterm.js to connect to + xtermjsHandlerOptions := xtermjs.HandlerOpts{ + AllowedHostnames: allowedHostnames, + Arguments: arguments, + Command: command, + ConnectionErrorLimit: connectionErrorLimit, + CreateLogger: func(connectionUUID string, r *http.Request) xtermjs.Logger { + createRequestLog(r, map[string]interface{}{"connection_uuid": connectionUUID}).Infof("created logger for connection '%s'", connectionUUID) + return createRequestLog(nil, map[string]interface{}{"connection_uuid": connectionUUID}) + }, + KeepalivePingTimeout: keepalivePingTimeout, + MaxBufferSizeBytes: maxBufferSizeBytes, + } + router.HandleFunc(pathXTermJS, xtermjs.GetHandler(xtermjsHandlerOptions)) + + // readiness probe endpoint + router.HandleFunc(pathReadiness, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + // liveness probe endpoint + router.HandleFunc(pathLiveness, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + // metrics endpoint + router.Handle(pathMetrics, promhttp.Handler()) + + // version endpoint + router.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(VersionInfo)) + }) + + // this is the endpoint for serving xterm.js assets + depenenciesDirectory := path.Join(workingDirectory, "./node_modules") + router.PathPrefix("/assets").Handler(http.StripPrefix("/assets", http.FileServer(http.Dir(depenenciesDirectory)))) + + // this is the endpoint for the root path aka website + publicAssetsDirectory := path.Join(workingDirectory, "./public") + router.PathPrefix("/").Handler(http.FileServer(http.Dir(publicAssetsDirectory))) + + // start memory logging pulse + logWithMemory := createMemoryLog() + go func(tick *time.Ticker) { + for { + logWithMemory.Debug("tick") + <-tick.C + } + }(time.NewTicker(time.Second * 30)) + + // listen + listenOnAddress := fmt.Sprintf("%s:%v", serverAddress, serverPort) + server := http.Server{ + Addr: listenOnAddress, + Handler: addIncomingRequestLogging(router), + } + + log.Infof("starting server on interface:port '%s'...", listenOnAddress) + return server.ListenAndServe() +} diff --git a/kubelab-agent/cmd/kubelab-agent/middleware.go b/kubelab-agent/cmd/kubelab-agent/middleware.go new file mode 100644 index 0000000..0011781 --- /dev/null +++ b/kubelab-agent/cmd/kubelab-agent/middleware.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + "time" +) + +func addIncomingRequestLogging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + then := time.Now() + defer func() { + if recovered := recover(); recovered != nil { + createRequestLog(r).Info("request errored out") + } + }() + next.ServeHTTP(w, r) + duration := time.Now().Sub(then) + createRequestLog(r).Infof("request completed in %vms", float64(duration.Nanoseconds())/1000000) + }) +} diff --git a/kubelab-agent/go.mod b/kubelab-agent/go.mod index d7c6477..8214a91 100644 --- a/kubelab-agent/go.mod +++ b/kubelab-agent/go.mod @@ -5,6 +5,38 @@ go 1.20 require github.com/gorilla/websocket v1.5.0 require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.7.1 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/ini.v1 v1.51.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +require ( + github.com/creack/pty v1.1.18 + github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 + github.com/prometheus/client_golang v1.15.1 github.com/sirupsen/logrus v1.9.2 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + github.com/spf13/cobra v1.7.0 + github.com/usvc/go-config v0.4.1 + golang.org/x/sys v0.6.0 // indirect ) diff --git a/kubelab-agent/go.sum b/kubelab-agent/go.sum index 446f332..746679d 100644 --- a/kubelab-agent/go.sum +++ b/kubelab-agent/go.sum @@ -1,19 +1,356 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/usvc/go-config v0.4.1 h1:2T7nHhq+K6VwdbNLD5/yNpBpfoXrdIyCUnbjnF6syMo= +github.com/usvc/go-config v0.4.1/go.mod h1:w1l+oFofLI1XFk9RpKIu9j5ztpJ3t04f/bvPchQTqS4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/kubelab-agent/internal/constants/key_sequences.go b/kubelab-agent/internal/constants/key_sequences.go new file mode 100644 index 0000000..6c08bca --- /dev/null +++ b/kubelab-agent/internal/constants/key_sequences.go @@ -0,0 +1,12 @@ +package constants + +type KeySequence []byte + +var ( + KeySeqBackspace = []byte{127} + KeySeqDownArrow = []byte{27, 91, 66} + KeySeqLinefeed = []byte{13} + KeySeqUpArrow = []byte{27, 91, 65} + KeySeqSigInt = []byte{3} + KeySeqEOF = []byte{4} +) diff --git a/kubelab-agent/internal/log/constants.go b/kubelab-agent/internal/log/constants.go new file mode 100644 index 0000000..ead657c --- /dev/null +++ b/kubelab-agent/internal/log/constants.go @@ -0,0 +1,41 @@ +package log + +import "github.com/sirupsen/logrus" + +type Level string + +const ( + LevelTrace Level = "trace" + LevelDebug Level = "debug" + LevelInfo Level = "info" + LevelWarn Level = "warn" + LevelError Level = "error" +) + +var ValidLevelStrings = []string{ + string(LevelTrace), + string(LevelDebug), + string(LevelInfo), + string(LevelWarn), + string(LevelError), +} + +var LevelMap = map[Level]logrus.Level{ + LevelTrace: logrus.TraceLevel, + LevelDebug: logrus.DebugLevel, + LevelInfo: logrus.InfoLevel, + LevelWarn: logrus.WarnLevel, + LevelError: logrus.ErrorLevel, +} + +type Format string + +const ( + FormatJSON Format = "json" + FormatText Format = "text" +) + +var ValidFormatStrings = []string{ + string(FormatJSON), + string(FormatText), +} diff --git a/kubelab-agent/internal/log/log.go b/kubelab-agent/internal/log/log.go new file mode 100644 index 0000000..d737c13 --- /dev/null +++ b/kubelab-agent/internal/log/log.go @@ -0,0 +1,116 @@ +package log + +import ( + "fmt" + "os" + "path" + "runtime" + + "github.com/sirupsen/logrus" +) + +var logger = logrus.New() + +type Logger interface { + Trace(...interface{}) + Tracef(string, ...interface{}) + Debug(...interface{}) + Debugf(string, ...interface{}) + Info(...interface{}) + Infof(string, ...interface{}) + Warn(...interface{}) + Warnf(string, ...interface{}) + Error(...interface{}) + Errorf(string, ...interface{}) +} + +// Log defines the function signature of the logger +type Log func(l ...interface{}) + +// Log defines the function signature of the logger when called with format parameters +type Logf func(s string, l ...interface{}) + +var WithField, + WithFields = logger.WithField, + logger.WithFields + +// Trace logs at the Trace level +var Trace, + // Debug logs at the Debug level + Debug, + // Info logs at the Info level + Info, + // Warn logs at the Warn level + Warn, + // Error logs at the Error level + Error, + Print Log = logger.Trace, + logger.Debug, + logger.Info, + logger.Warn, + logger.Error, + func(l ...interface{}) { + fmt.Println(l...) + } + +// Tracef logs at the trace level with formatting +var Tracef, + // Debugf logs at the debug level with formatting + Debugf, + // Infof logs at the info level with formatting + Infof, + // Warnf logs at the warn level with formatting + Warnf, + // Errorf logs at the error level with formatting + Errorf, + Printf Logf = logger.Tracef, + logger.Debugf, + logger.Infof, + logger.Warnf, + logger.Errorf, + func(s string, l ...interface{}) { + fmt.Printf(s, l...) + fmt.Printf("\n") + } + +func Init( + logFormat Format, + logLevel Level, +) { + logger.SetLevel(LevelMap[logLevel]) + + logger.SetOutput(os.Stderr) + logger.SetReportCaller(true) + if logFormat == FormatJSON { + logger.SetFormatter(&logrus.JSONFormatter{ + CallerPrettyfier: func(frame *runtime.Frame) (function string, file string) { + function = frame.Function + file = path.Base(frame.File) + return + }, + DataKey: "@data", + TimestampFormat: "2006-01-02T15:04:05-0700", + FieldMap: logrus.FieldMap{ + logrus.FieldKeyFile: "@file", + logrus.FieldKeyFunc: "@func", + logrus.FieldKeyLevel: "@level", + logrus.FieldKeyMsg: "@message", + logrus.FieldKeyTime: "@timestamp", + logrus.FieldKeyLogrusError: "@error", + }, + }) + return + } + logger.SetFormatter(&logrus.TextFormatter{ + CallerPrettyfier: func(frame *runtime.Frame) (function string, file string) { + function = path.Base(frame.Function) + file = path.Base(frame.File) + return + }, + TimestampFormat: "15:04:05", + DisableSorting: false, + FullTimestamp: true, + QuoteEmptyFields: true, + ForceQuote: true, + }) +} diff --git a/kubelab-agent/old.go b/kubelab-agent/old.go deleted file mode 100644 index 184c32e..0000000 --- a/kubelab-agent/old.go +++ /dev/null @@ -1,87 +0,0 @@ -// package main - -// import ( -// "io" -// "log" -// "net/http" -// "os" -// "os/exec" -// "strings" - -// "github.com/gorilla/websocket" -// ) - -// var upgrader = websocket.Upgrader{ -// ReadBufferSize: 1024, -// WriteBufferSize: 1024, -// CheckOrigin: func(r *http.Request) bool { -// return true -// }, -// } - -// func handleConnections(w http.ResponseWriter, r *http.Request) { -// ws, err := upgrader.Upgrade(w, r, nil) -// if err != nil { -// log.Fatal(err) -// } - -// for { -// _, message, err := ws.ReadMessage() -// if err != nil { -// log.Println("read:", err) -// return -// } - -// msg := string(message) -// if strings.HasPrefix(msg, "\t") { -// // Remove the tab character -// msg = strings.TrimPrefix(msg, "\t") -// // Execute a compgen command to generate auto-complete suggestions -// cmd := exec.Command("bash", "-c", "compgen -c "+msg) -// output, err := cmd.Output() -// if err != nil { -// ws.WriteMessage(websocket.TextMessage, []byte(err.Error()+"\n")) -// continue -// } -// // Send back a list of suggestions separated by commas -// ws.WriteMessage(websocket.TextMessage, []byte("\t"+strings.Join(strings.Fields(string(output)), ","))) -// } else if strings.HasPrefix(msg, "less ") { -// // Open the file -// file, err := os.Open(strings.TrimPrefix(msg, "less ")) -// if err != nil { -// ws.WriteMessage(websocket.TextMessage, []byte(err.Error()+"\n")) -// continue -// } - -// // Read the file content -// content, err := io.ReadAll(file) -// if err != nil { -// ws.WriteMessage(websocket.TextMessage, []byte(err.Error()+"\n")) -// continue -// } - -// // Send the file content -// ws.WriteMessage(websocket.TextMessage, append(content, '\n')) - -// file.Close() -// } else { -// cmd := exec.Command("bash", "-c", msg) -// output, err := cmd.Output() -// if err != nil { -// ws.WriteMessage(websocket.TextMessage, []byte(err.Error()+"\n")) -// continue -// } - -// ws.WriteMessage(websocket.TextMessage, output) -// } -// } -// } - -// func main() { -// http.HandleFunc("/ws", handleConnections) - -// err := http.ListenAndServe(":8080", nil) -// if err != nil { -// log.Fatal("ListenAndServe: ", err) -// } -// } diff --git a/kubelab-agent/pkg/xtermjs/constants.go b/kubelab-agent/pkg/xtermjs/constants.go new file mode 100644 index 0000000..ffae5d3 --- /dev/null +++ b/kubelab-agent/pkg/xtermjs/constants.go @@ -0,0 +1,11 @@ +package xtermjs + +import "github.com/gorilla/websocket" + +var WebsocketMessageType = map[int]string{ + websocket.BinaryMessage: "binary", + websocket.TextMessage: "text", + websocket.CloseMessage: "close", + websocket.PingMessage: "ping", + websocket.PongMessage: "pong", +} diff --git a/kubelab-agent/pkg/xtermjs/handler_websocket.go b/kubelab-agent/pkg/xtermjs/handler_websocket.go new file mode 100644 index 0000000..92d1aae --- /dev/null +++ b/kubelab-agent/pkg/xtermjs/handler_websocket.go @@ -0,0 +1,221 @@ +package xtermjs + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "strings" + "sync" + "time" + + "github.com/creack/pty" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/natrontech/kubelab-agent/internal/log" +) + +const DefaultConnectionErrorLimit = 10 + +type HandlerOpts struct { + // AllowedHostnames is a list of strings which will be matched to the client + // requesting for a connection upgrade to a websocket connection + AllowedHostnames []string + // Arguments is a list of strings to pass as arguments to the specified COmmand + Arguments []string + // Command is the path to the binary we should create a TTY for + Command string + // ConnectionErrorLimit defines the number of consecutive errors that can happen + // before a connection is considered unusable + ConnectionErrorLimit int + // CreateLogger when specified should return a logger that the handler will use. + // The string argument being passed in will be a unique identifier for the + // current connection. When not specified, logs will be sent to stdout + CreateLogger func(string, *http.Request) Logger + // KeepalivePingTimeout defines the maximum duration between which a ping and pong + // cycle should be tolerated, beyond this the connection should be deemed dead + KeepalivePingTimeout time.Duration + MaxBufferSizeBytes int +} + +func GetHandler(opts HandlerOpts) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + connectionErrorLimit := opts.ConnectionErrorLimit + if connectionErrorLimit < 0 { + connectionErrorLimit = DefaultConnectionErrorLimit + } + maxBufferSizeBytes := opts.MaxBufferSizeBytes + keepalivePingTimeout := opts.KeepalivePingTimeout + if keepalivePingTimeout <= time.Second { + keepalivePingTimeout = 20 * time.Second + } + + connectionUUID, err := uuid.NewUUID() + if err != nil { + message := "failed to get a connection uuid" + log.Errorf("%s: %s", message, err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(message)) + return + } + var clog Logger = defaultLogger + if opts.CreateLogger != nil { + clog = opts.CreateLogger(connectionUUID.String(), r) + } + clog.Info("established connection identity") + + allowedHostnames := opts.AllowedHostnames + upgrader := getConnectionUpgrader(allowedHostnames, maxBufferSizeBytes, clog) + connection, err := upgrader.Upgrade(w, r, nil) + if err != nil { + clog.Warnf("failed to upgrade connection: %s", err) + return + } + + terminal := opts.Command + args := opts.Arguments + clog.Debugf("starting new tty using command '%s' with arguments ['%s']...", terminal, strings.Join(args, "', '")) + cmd := exec.Command(terminal, args...) + cmd.Env = os.Environ() + tty, err := pty.Start(cmd) + if err != nil { + message := fmt.Sprintf("failed to start tty: %s", err) + clog.Warn(message) + connection.WriteMessage(websocket.TextMessage, []byte(message)) + return + } + defer func() { + clog.Info("gracefully stopping spawned tty...") + if err := cmd.Process.Kill(); err != nil { + clog.Warnf("failed to kill process: %s", err) + } + if _, err := cmd.Process.Wait(); err != nil { + clog.Warnf("failed to wait for process to exit: %s", err) + } + if err := tty.Close(); err != nil { + clog.Warnf("failed to close spawned tty gracefully: %s", err) + } + if err := connection.Close(); err != nil { + clog.Warnf("failed to close webscoket connection: %s", err) + } + }() + + var connectionClosed bool + var waiter sync.WaitGroup + waiter.Add(1) + + // this is a keep-alive loop that ensures connection does not hang-up itself + lastPongTime := time.Now() + connection.SetPongHandler(func(msg string) error { + lastPongTime = time.Now() + return nil + }) + go func() { + for { + if err := connection.WriteMessage(websocket.PingMessage, []byte("keepalive")); err != nil { + clog.Warn("failed to write ping message") + return + } + time.Sleep(keepalivePingTimeout / 2) + if time.Now().Sub(lastPongTime) > keepalivePingTimeout { + clog.Warn("failed to get response from ping, triggering disconnect now...") + waiter.Done() + return + } + clog.Debug("received response from ping successfully") + } + }() + + // tty >> xterm.js + go func() { + errorCounter := 0 + for { + // consider the connection closed/errored out so that the socket handler + // can be terminated - this frees up memory so the service doesn't get + // overloaded + if errorCounter > connectionErrorLimit { + waiter.Done() + break + } + buffer := make([]byte, maxBufferSizeBytes) + readLength, err := tty.Read(buffer) + if err != nil { + clog.Warnf("failed to read from tty: %s", err) + if err := connection.WriteMessage(websocket.TextMessage, []byte("bye!")); err != nil { + clog.Warnf("failed to send termination message from tty to xterm.js: %s", err) + } + waiter.Done() + return + } + if err := connection.WriteMessage(websocket.BinaryMessage, buffer[:readLength]); err != nil { + clog.Warnf("failed to send %v bytes from tty to xterm.js", readLength) + errorCounter++ + continue + } + clog.Tracef("sent message of size %v bytes from tty to xterm.js", readLength) + errorCounter = 0 + } + }() + + // tty << xterm.js + go func() { + for { + // data processing + messageType, data, err := connection.ReadMessage() + if err != nil { + if !connectionClosed { + clog.Warnf("failed to get next reader: %s", err) + } + return + } + dataLength := len(data) + dataBuffer := bytes.Trim(data, "\x00") + dataType, ok := WebsocketMessageType[messageType] + if !ok { + dataType = "uunknown" + } + clog.Infof("received %s (type: %v) message of size %v byte(s) from xterm.js with key sequence: %v", dataType, messageType, dataLength, dataBuffer) + + // process + if dataLength == -1 { // invalid + clog.Warn("failed to get the correct number of bytes read, ignoring message") + continue + } + + // handle resizing + if messageType == websocket.BinaryMessage { + if dataBuffer[0] == 1 { + ttySize := &TTYSize{} + resizeMessage := bytes.Trim(dataBuffer[1:], " \n\r\t\x00\x01") + if err := json.Unmarshal(resizeMessage, ttySize); err != nil { + clog.Warnf("failed to unmarshal received resize message '%s': %s", string(resizeMessage), err) + continue + } + clog.Infof("resizing tty to use %v rows and %v columns...", ttySize.Rows, ttySize.Cols) + if err := pty.Setsize(tty, &pty.Winsize{ + Rows: ttySize.Rows, + Cols: ttySize.Cols, + }); err != nil { + clog.Warnf("failed to resize tty, error: %s", err) + } + continue + } + } + + // write to tty + bytesWritten, err := tty.Write(dataBuffer) + if err != nil { + clog.Warn(fmt.Sprintf("failed to write %v bytes to tty: %s", len(dataBuffer), err)) + continue + } + clog.Tracef("%v bytes written to tty...", bytesWritten) + } + }() + + waiter.Wait() + log.Info("closing connection...") + connectionClosed = true + } +} diff --git a/kubelab-agent/pkg/xtermjs/types.go b/kubelab-agent/pkg/xtermjs/types.go new file mode 100644 index 0000000..9ac81e4 --- /dev/null +++ b/kubelab-agent/pkg/xtermjs/types.go @@ -0,0 +1,41 @@ +package xtermjs + +import "fmt" + +// TTYSize represents a JSON structure to be sent by the frontend +// xterm.js implementation to the xterm.js websocket handler +type TTYSize struct { + Cols uint16 `json:"cols"` + Rows uint16 `json:"rows"` + X uint16 `json:"x"` + Y uint16 `json:"y"` +} + +// Logger is the logging interface used by the xterm.js handler +type Logger interface { + Trace(...interface{}) + Tracef(string, ...interface{}) + Debug(...interface{}) + Debugf(string, ...interface{}) + Info(...interface{}) + Infof(string, ...interface{}) + Warn(...interface{}) + Warnf(string, ...interface{}) + Error(...interface{}) + Errorf(string, ...interface{}) +} + +type logger struct{} + +func (l logger) Trace(i ...interface{}) { fmt.Println(append([]interface{}{"[trace] "}, i...)...) } +func (l logger) Tracef(s string, i ...interface{}) { fmt.Printf("[trace] "+s+"\n", i...) } +func (l logger) Debug(i ...interface{}) { fmt.Println(append([]interface{}{"[debug] "}, i...)...) } +func (l logger) Debugf(s string, i ...interface{}) { fmt.Printf("[debug] "+s+"\n", i...) } +func (l logger) Info(i ...interface{}) { fmt.Println(append([]interface{}{"[info] "}, i...)...) } +func (l logger) Infof(s string, i ...interface{}) { fmt.Printf("[info] "+s+"\n", i...) } +func (l logger) Warn(i ...interface{}) { fmt.Println(append([]interface{}{"[warn] "}, i...)...) } +func (l logger) Warnf(s string, i ...interface{}) { fmt.Printf("[warn] "+s+"\n", i...) } +func (l logger) Error(i ...interface{}) { fmt.Println(append([]interface{}{"[error] "}, i...)...) } +func (l logger) Errorf(s string, i ...interface{}) { fmt.Printf("[error] "+s+"\n", i...) } + +var defaultLogger = logger{} diff --git a/kubelab-agent/pkg/xtermjs/utils.go b/kubelab-agent/pkg/xtermjs/utils.go new file mode 100644 index 0000000..67484a4 --- /dev/null +++ b/kubelab-agent/pkg/xtermjs/utils.go @@ -0,0 +1,33 @@ +package xtermjs + +import ( + "net/http" + "strings" + + "github.com/gorilla/websocket" +) + +func getConnectionUpgrader( + allowedHostnames []string, + maxBufferSizeBytes int, + logger Logger, +) websocket.Upgrader { + return websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + requesterHostname := r.Host + if strings.Index(requesterHostname, ":") != -1 { + requesterHostname = strings.Split(requesterHostname, ":")[0] + } + for _, allowedHostname := range allowedHostnames { + if requesterHostname == allowedHostname { + return true + } + } + logger.Warnf("failed to find '%s' in the list of allowed hostnames ('%s')", requesterHostname) + return false + }, + HandshakeTimeout: 0, + ReadBufferSize: maxBufferSizeBytes, + WriteBufferSize: maxBufferSizeBytes, + } +} diff --git a/kubelab-ui/src/lib/components/base/Nav.svelte b/kubelab-ui/src/lib/components/base/Nav.svelte index eef7184..cffb59d 100644 --- a/kubelab-ui/src/lib/components/base/Nav.svelte +++ b/kubelab-ui/src/lib/components/base/Nav.svelte @@ -1,19 +1,19 @@