From ac594927a69f616130163da3e31b0da35debcfee Mon Sep 17 00:00:00 2001 From: nirav-rafay <93963752+nirav-rafay@users.noreply.github.com> Date: Tue, 22 Feb 2022 11:42:15 +0530 Subject: [PATCH] initial commit for relay (#17) initial commit for relay --- components/relay/Dockerfile | 50 + components/relay/Dockerfile.agent | 65 + components/relay/Dockerfile.dev | 8 + components/relay/Makefile | 33 + components/relay/README.md | 137 ++ components/relay/go.mod | 114 + components/relay/go.sum | 1301 +++++++++++ components/relay/main.go | 148 ++ components/relay/pkg/agent/agent.go | 595 +++++ components/relay/pkg/agent/lib.go | 36 + components/relay/pkg/audit/audit.go | 97 + components/relay/pkg/audit/logger.go | 30 + components/relay/pkg/cleanup/authz.go | 150 ++ components/relay/pkg/profile/profile.go | 31 + components/relay/pkg/proxy/kube_proxy.go | 299 +++ components/relay/pkg/proxy/proxy.go | 12 + components/relay/pkg/proxy/round_tripper.go | 214 ++ components/relay/pkg/proxy/service_account.go | 157 ++ components/relay/pkg/proxy/tcpproxy.go | 244 ++ components/relay/pkg/proxy/testdata/role.yaml | 34 + components/relay/pkg/relay/relay.go | 888 +++++++ .../relay/pkg/relaylogger/relaylogger.go | 145 ++ components/relay/pkg/sessions/usersession.go | 161 ++ .../relay/pkg/tail/follower/follower.go | 322 +++ components/relay/pkg/tail/register.go | 109 + components/relay/pkg/tail/run.go | 213 ++ components/relay/pkg/tail/tail.go | 241 ++ components/relay/pkg/tail/transform.go | 217 ++ components/relay/pkg/tail/types.go | 80 + components/relay/pkg/tail/url_test.go | 76 + components/relay/pkg/tunnel/authz.go | 397 ++++ components/relay/pkg/tunnel/backoff.go | 15 + components/relay/pkg/tunnel/client.go | 651 ++++++ components/relay/pkg/tunnel/client_config.go | 38 + components/relay/pkg/tunnel/client_test.go | 70 + components/relay/pkg/tunnel/common.go | 189 ++ components/relay/pkg/tunnel/dialin_pool.go | 381 +++ components/relay/pkg/tunnel/errors.go | 11 + components/relay/pkg/tunnel/peer.go | 168 ++ components/relay/pkg/tunnel/server.go | 2052 +++++++++++++++++ components/relay/pkg/tunnel/server_config.go | 43 + components/relay/pkg/utils/utils.go | 713 ++++++ components/relay/testdata/README.md | 89 + ...layserver1-ABCD-123456.relay.rafay.dev.crt | 26 + ...layserver1-ABCD-123456.relay.rafay.dev.csr | 17 + ...layserver1-ABCD-123456.relay.rafay.dev.key | 27 + .../testdata/certs/clientcerts/star.admin.crt | 25 + .../testdata/certs/clientcerts/star.admin.csr | 16 + .../testdata/certs/clientcerts/star.admin.key | 27 + .../certs/clientcerts/star.default.crt | 26 + .../certs/clientcerts/star.default.csr | 17 + .../certs/clientcerts/star.default.key | 27 + .../clientcerts/star.namespace-admin-sa.crt | 25 + .../clientcerts/star.namespace-admin-sa.csr | 17 + .../clientcerts/star.namespace-admin-sa.key | 27 + components/relay/testdata/certs/gencert.sh | 15 + components/relay/testdata/certs/rootCA.crt | 30 + components/relay/testdata/certs/rootCA.key | 51 + components/relay/testdata/certs/rootCA.srl | 1 + components/relay/testdata/certs/run.sh | 53 + .../relay/testdata/certs/servercerts/.srl | 1 + .../certs/servercerts/relay.rafay.dev.crt | 25 + .../certs/servercerts/relay.rafay.dev.csr | 17 + .../certs/servercerts/relay.rafay.dev.key | 27 + .../star.kubectl.relay.rafay.dev.crt | 25 + .../star.kubectl.relay.rafay.dev.csr | 17 + .../star.kubectl.relay.rafay.dev.key | 27 + .../star.kubectldialin.relay.rafay.dev.crt | 25 + .../star.kubectldialin.relay.rafay.dev.csr | 17 + .../star.kubectldialin.relay.rafay.dev.key | 27 + .../star.kubeweb.relay.rafay.dev.crt | 25 + .../star.kubeweb.relay.rafay.dev.csr | 17 + .../star.kubeweb.relay.rafay.dev.key | 27 + .../star.kubewebdialin.relay.rafay.dev.crt | 25 + .../star.kubewebdialin.relay.rafay.dev.csr | 17 + .../star.kubewebdialin.relay.rafay.dev.key | 27 + .../star.kubectl.peer.relay.rafay.dev.crt | 26 + .../star.kubectl.peer.relay.rafay.dev.csr | 17 + .../star.kubectl.peer.relay.rafay.dev.key | 27 + .../star-probe/star.probe.relay.rafay.dev.crt | 25 + .../star-probe/star.probe.relay.rafay.dev.csr | 17 + .../star-probe/star.probe.relay.rafay.dev.key | 27 + .../star.kubectl.peer.relay.rafay.dev.crt | 26 + .../star.kubectl.peer.relay.rafay.dev.csr | 17 + .../star.kubectl.peer.relay.rafay.dev.key | 27 + .../relay/testdata/client-cluster1.yaml | 16 + .../relay/testdata/client-cluster2.yaml | 16 + .../relay/testdata/images/testsetup.png | Bin 0 -> 185566 bytes .../relay/testdata/kubectlconfigs/kubeconfig | 19 + .../testdata/kubectlconfigs/kubeconfig-c-5 | 20 + .../testdata/kubectlconfigs/kubeconfig-sa | 19 + ...configproxy-123abcd.user.relay.rafay.local | 19 + .../kubectlconfigs/kubeconfigproxy-cluster1 | 24 + .../kubectlconfigs/kubeconfigproxy-cluster2 | 21 + .../testdata/kubectlconfigs/kubeproxyconfig | 23 + components/relay/testdata/pod.yaml | 17 + .../relay/testdata/relay-agent-config.yaml | 8 + components/relay/testdata/role.yaml | 70 + .../relay/testdata/sample-kubeconfig.yaml | 19 + .../relay/testdata/scripts/dialinclient.sh | 1 + components/relay/testdata/scripts/loop.sh | 4 + .../relay/testdata/scripts/relayclient.sh | 16 + components/relay/testdata/server.yaml | 33 + components/relay/testdata/server2.yaml | 33 + .../relay/testdata/testclients/loadtest.go | 85 + .../relay/testdata/testclients/testclient.go | 78 + 106 files changed, 12547 insertions(+) create mode 100644 components/relay/Dockerfile create mode 100644 components/relay/Dockerfile.agent create mode 100644 components/relay/Dockerfile.dev create mode 100644 components/relay/Makefile create mode 100644 components/relay/README.md create mode 100644 components/relay/go.mod create mode 100644 components/relay/go.sum create mode 100644 components/relay/main.go create mode 100644 components/relay/pkg/agent/agent.go create mode 100644 components/relay/pkg/agent/lib.go create mode 100644 components/relay/pkg/audit/audit.go create mode 100644 components/relay/pkg/audit/logger.go create mode 100644 components/relay/pkg/cleanup/authz.go create mode 100644 components/relay/pkg/profile/profile.go create mode 100644 components/relay/pkg/proxy/kube_proxy.go create mode 100644 components/relay/pkg/proxy/proxy.go create mode 100644 components/relay/pkg/proxy/round_tripper.go create mode 100644 components/relay/pkg/proxy/service_account.go create mode 100644 components/relay/pkg/proxy/tcpproxy.go create mode 100644 components/relay/pkg/proxy/testdata/role.yaml create mode 100644 components/relay/pkg/relay/relay.go create mode 100644 components/relay/pkg/relaylogger/relaylogger.go create mode 100644 components/relay/pkg/sessions/usersession.go create mode 100644 components/relay/pkg/tail/follower/follower.go create mode 100644 components/relay/pkg/tail/register.go create mode 100644 components/relay/pkg/tail/run.go create mode 100644 components/relay/pkg/tail/tail.go create mode 100644 components/relay/pkg/tail/transform.go create mode 100644 components/relay/pkg/tail/types.go create mode 100644 components/relay/pkg/tail/url_test.go create mode 100644 components/relay/pkg/tunnel/authz.go create mode 100644 components/relay/pkg/tunnel/backoff.go create mode 100644 components/relay/pkg/tunnel/client.go create mode 100644 components/relay/pkg/tunnel/client_config.go create mode 100644 components/relay/pkg/tunnel/client_test.go create mode 100644 components/relay/pkg/tunnel/common.go create mode 100644 components/relay/pkg/tunnel/dialin_pool.go create mode 100644 components/relay/pkg/tunnel/errors.go create mode 100644 components/relay/pkg/tunnel/peer.go create mode 100644 components/relay/pkg/tunnel/server.go create mode 100644 components/relay/pkg/tunnel/server_config.go create mode 100644 components/relay/pkg/utils/utils.go create mode 100644 components/relay/testdata/README.md create mode 100644 components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/clientcerts/star.admin.crt create mode 100644 components/relay/testdata/certs/clientcerts/star.admin.csr create mode 100644 components/relay/testdata/certs/clientcerts/star.admin.key create mode 100644 components/relay/testdata/certs/clientcerts/star.default.crt create mode 100644 components/relay/testdata/certs/clientcerts/star.default.csr create mode 100644 components/relay/testdata/certs/clientcerts/star.default.key create mode 100644 components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt create mode 100644 components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.csr create mode 100644 components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.key create mode 100644 components/relay/testdata/certs/gencert.sh create mode 100644 components/relay/testdata/certs/rootCA.crt create mode 100644 components/relay/testdata/certs/rootCA.key create mode 100644 components/relay/testdata/certs/rootCA.srl create mode 100644 components/relay/testdata/certs/run.sh create mode 100644 components/relay/testdata/certs/servercerts/.srl create mode 100644 components/relay/testdata/certs/servercerts/relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.key create mode 100644 components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.crt create mode 100644 components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.csr create mode 100644 components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.key create mode 100644 components/relay/testdata/client-cluster1.yaml create mode 100644 components/relay/testdata/client-cluster2.yaml create mode 100644 components/relay/testdata/images/testsetup.png create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfig create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfig-c-5 create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfig-sa create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster1 create mode 100644 components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster2 create mode 100644 components/relay/testdata/kubectlconfigs/kubeproxyconfig create mode 100644 components/relay/testdata/pod.yaml create mode 100644 components/relay/testdata/relay-agent-config.yaml create mode 100644 components/relay/testdata/role.yaml create mode 100644 components/relay/testdata/sample-kubeconfig.yaml create mode 100644 components/relay/testdata/scripts/dialinclient.sh create mode 100755 components/relay/testdata/scripts/loop.sh create mode 100644 components/relay/testdata/scripts/relayclient.sh create mode 100644 components/relay/testdata/server.yaml create mode 100644 components/relay/testdata/server2.yaml create mode 100644 components/relay/testdata/testclients/loadtest.go create mode 100644 components/relay/testdata/testclients/testclient.go diff --git a/components/relay/Dockerfile b/components/relay/Dockerfile new file mode 100644 index 0000000..89370d9 --- /dev/null +++ b/components/relay/Dockerfile @@ -0,0 +1,50 @@ +# Build the manager binary +FROM golang:1.16.6 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# setup github user and token +ARG BUILD_USR="nothing" +ARG BUILD_PWD="nothing" + +# Create the user and group files that will be used in the running +# container to run the process as an unprivileged user. +RUN mkdir /user && \ + echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \ + echo 'nobody:x:65534:' > /user/group + +# Create a netrc file using the credentials specified using --build-arg +RUN printf "machine github.com\n\ + login ${BUILD_USR}\n\ + password ${BUILD_PWD}\n\ + \n\ + machine api.github.com\n\ + login ${BUILD_USR}\n\ + password ${BUILD_PWD}\n"\ + >> /root/.netrc +RUN chmod 600 /root/.netrc + +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN GOPRIVATE=github.com/RafaySystems/* go mod download + +# Copy the go source +COPY main.go main.go +COPY pkg/ pkg/ + + +# Build +# uncomment this to debug race conditions +#RUN GOPRIVATE=github.com/RafaySystems/* CGO_ENABLED=1 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -race -a -o rafay-relay main.go +RUN GOPRIVATE=github.com/RafaySystems/* CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" -a -o rafay-relay main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:latest +WORKDIR / +COPY --from=builder /workspace/relay . +ENTRYPOINT ["/relay"] + diff --git a/components/relay/Dockerfile.agent b/components/relay/Dockerfile.agent new file mode 100644 index 0000000..c50e75c --- /dev/null +++ b/components/relay/Dockerfile.agent @@ -0,0 +1,65 @@ +# Build the manager binary +FROM golang:1.16.6 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# setup github user and token +ARG BUILD_USR="nothing" +ARG BUILD_PWD="nothing" + +# Create the user and group files that will be used in the running +# container to run the process as an unprivileged user. +RUN mkdir /user && \ + echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \ + echo 'nobody:x:65534:' > /user/group + +# Create a netrc file using the credentials specified using --build-arg +RUN printf "machine github.com\n\ + login ${BUILD_USR}\n\ + password ${BUILD_PWD}\n\ + \n\ + machine api.github.com\n\ + login ${BUILD_USR}\n\ + password ${BUILD_PWD}\n"\ + >> /root/.netrc +RUN chmod 600 /root/.netrc + +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN GOPRIVATE=github.com/RafaySystems/* go mod download + +# Copy the go source +COPY main.go main.go +COPY pkg/ pkg/ + + +# Build +# uncomment this to debug race conditions +#RUN GOPRIVATE=github.com/RafaySystems/* CGO_ENABLED=1 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -race -a -o rafay-relay main.go +RUN GOPRIVATE=github.com/RafaySystems/* CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" -a -o rafay-relay main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:latest +WORKDIR / +COPY --from=builder /workspace/relay . +ENTRYPOINT ["/relay"] + + +# Before copying the Go binary directly to the final image, +# add them to the intermdediate upx image +FROM rafaysystems/upx:latest as upx +COPY --from=builder /workspace/relay /relay.orig + +# Compress the binary and copy it to final image +RUN upx --best --lzma -o /relay /relay.orig + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=upx /relay . +ENTRYPOINT ["/relay"] diff --git a/components/relay/Dockerfile.dev b/components/relay/Dockerfile.dev new file mode 100644 index 0000000..125f600 --- /dev/null +++ b/components/relay/Dockerfile.dev @@ -0,0 +1,8 @@ +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +#FROM gcr.io/distroless/static:latest +## use base:debug for shell access +FROM gcr.io/distroless/static:latest +WORKDIR / +COPY relay.upx relay +ENTRYPOINT ["/relay"] diff --git a/components/relay/Makefile b/components/relay/Makefile new file mode 100644 index 0000000..062dcb5 --- /dev/null +++ b/components/relay/Makefile @@ -0,0 +1,33 @@ +IMG ?= relay:latest +TS := $(shell /bin/date "+%Y%m%d%H%M%S") +DEV_USER ?= dev +DEV_TAG := registry.dev.rafay-edge.net:5000/${DEV_USER}/relay:$(TS) + +.PHONY: tidy +tidy: + GOPRIVATE=github.com/RafaySystems/* go mod tidy + +.PHONY: vendor +vendor: + GOPROXY=direct GOPRIVATE=github.com/RafaySystems/* go mod vendor + +check: + go fmt ./... + go vet ./... + +build: + docker build . -t ${IMG} --build-arg BUILD_USR=${BUILD_USER} --build-arg BUILD_PWD=${BUILD_PASSWORD} + +build-agent: + docker build -t ${IMG} --build-arg BUILD_USR=${BUILD_USER} --build-arg BUILD_PWD=${BUILD_PASSWORD} -f Dockerfile.agent . + +tag-dev: + docker tag ${IMG} $(DEV_TAG) + docker push $(DEV_TAG) + +build-dev: + rm -rf relay.* + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" -o relay.dev main.go + upx -5 -o relay.upx relay.dev + docker build -f Dockerfile.dev -t ${IMG} . + $(MAKE) tag-dev diff --git a/components/relay/README.md b/components/relay/README.md new file mode 100644 index 0000000..9bf89aa --- /dev/null +++ b/components/relay/README.md @@ -0,0 +1,137 @@ +# Rafay relay-server / rafay-agent +Relay server and agent for kubectl access. + +## Development Setup +Relay depends following code-repos +1. rcloud-base/components/common : git@github.com:RafaySystems/rcloud-base.git +2. rcloud-base/components/adminsrv : git@github.com:RafaySystems/rcloud-base.git +3. rcloud-base/components/cluster-scheduler : git@github.com:RafaySystems/rcloud-base.git + +Check out the above code repos. + +Go to cluster-scheduler +``` +make teardown +make setup +make run +``` + +Go to adminsrv +``` +make teardown +make setup +GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info go run main.go + +``` + +### Rafay Relay Server +Get peer client token for relay. + +Open the admindb postgress and execute below SQL to fetch the token. +``` +admindb=# select token from sentry_bootstrap_agent_template where name='rafay-sentry-peering-client'; + token +---------------------- + brhsrbbipt349qt0qad0 + +``` + +Use above token set the environment variable for relay-server +``` +export RELAY_PEERING_TOKEN=brhsrbbipt349qt0qad0 +export RAFAY_RELAY_PEERSERVICE=http://peering.sentry.rafay.local:10001 +export POD_NAME=relay-pod1 +go run main.go --mode server --log-level 3 + +``` +### Rafay Relay Agent + +Get the config for rafay relay agent +``` +curl http://localhost:11000/infra/v3/project/:metadata.project/cluster/:metadata.name/download +``` + +Use the values of relay-agent-config and apply in the cluster. + +``` +kubectl -n rafay-system get cm/relay-agent-config -o yaml + +apiVersion: v1 +data: + clusterID: 4qkolkn + relays: '[{"token":"brht3cjipt35cb3kfjag","addr":"api.sentry.rafay.local:11000","endpoint":"*.core-connector.relay.rafay.local:443","name":"rafay-core-relay-agent","templateToken":"brhsrbbipt349qt0qae0"}]' +kind: ConfigMap +metadata: + creationTimestamp: "2020-04-28T07:10:53Z" + name: relay-agent-config + namespace: rafay-system + resourceVersion: "3127787" + selfLink: /api/v1/namespaces/rafay-system/configmaps/relay-agent-config + uid: e5fb5c16-c659-46d3-805f-3a2f3b73e9d7 +``` + +``` +kubectl apply -f +``` + +Run relay-agent +``` +export POD_NAME=relay-agent-pod1 +go run main.go --mode client --log-level 3 +``` + +Setup the local /etc/hosts file to resolve below hostname to localhost +``` +127.0.0.1 peering.sentry.rafay.local + +127.0.0.1 rx28oml.core-connector.relay.rafay.local +127.0.0.1 4qkolkn.core-connector.relay.rafay.local + +127.0.0.1 rx28oml.user.relay.rafay.local +127.0.0.1 4qkolkn.user.relay.rafay.local +``` + +Download kubeconfig +``` +curl http://localhost:11000/v2/sentry/kubeconfig/user?opts.selector=rafay.dev/clusterName=c-4 +``` + +Save the contents to a file and use for kubectl +``` +kubectl --v=6 --kubeconfig //kubeconfig-c-5 get pod -A +``` + + +### Production +Relay server need following environment variables + +POD_NAME - name of the relay instaance (auto generated) +POD_NAMESPACE - namespace of the relay instance + +RAFAY_SENTRY - URL to boostrap with sentry service +RAFAY_RELAY_PEERSERVICE - To join gRPC with sentry peer service + + +RELAY_USER_DOMAIN - domain suffix for relay user facing server +RELAY_CONNECTOR_DOMAIN - domain suffix for relay connector facing domain + +RELAY_TOKEN - Peer Service Token bootstrapping +RELAY_USER_TOKEN - Relay user facing server bootstrapping +RELAY_CONNECTOR_TOKEN - Relay connector facing server bootstrapping + +RELAY_LISTENIP - specify the listen IP if you want servers to listen other than 0.0.0.0 (usefull in testing local) + + +Examples: +``` +export RAFAY_SENTRY=http://sentry.rafay.dev:9000 +export RAFAY_RELAY_PEERSERVICE=http://peering.sentry.rafay.local:7001 + +export RELAY_USER_DOMAIN="user.relay.rafay.local" +export RELAY_CONNECTOR_DOMAIN="core-connector.relay.rafay.local" + +export RELAY_TOKEN=bqfvhabipt3a2g46986g +export RELAY_USER_TOKEN=bqfvhabipt3a2g46987g +export RELAY_CONNECTOR_TOKEN=bqfvhabipt3a2g469870 +export RELAY_LISTENIP=127.0.10.1 +``` \ No newline at end of file diff --git a/components/relay/go.mod b/components/relay/go.mod new file mode 100644 index 0000000..0bca88f --- /dev/null +++ b/components/relay/go.mod @@ -0,0 +1,114 @@ +module github.com/RafaySystems/rcloud-base/components/relay + +go 1.17 + +require ( + github.com/RafaySystems/rcloud-base/components/common v0.0.0-unpublished + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/dgraph-io/ristretto v0.1.0 + github.com/felixge/httpsnoop v1.0.1 + github.com/felixge/tcpkeepalive v0.0.0-20160804073959-5bb0b2dea91e + github.com/fsnotify/fsnotify v1.5.1 + github.com/google/uuid v1.3.0 + github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b + github.com/jinzhu/inflection v1.0.0 + github.com/julienschmidt/httprouter v1.3.0 + github.com/rs/xid v1.3.0 + github.com/segmentio/encoding v0.3.3 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.8.1 + github.com/twmb/murmur3 v1.1.6 + go.uber.org/zap v1.20.0 + golang.org/x/net v0.0.0-20211209124913-491a49abca63 + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac + google.golang.org/grpc v1.43.0 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + k8s.io/api v0.23.2 + k8s.io/apimachinery v0.23.2 + k8s.io/client-go v0.23.2 + sigs.k8s.io/controller-runtime v0.11.0 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-logr/logr v1.2.0 // indirect + github.com/go-openapi/analysis v0.20.1 // indirect + github.com/go-openapi/errors v0.20.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/loads v0.21.0 // indirect + github.com/go-openapi/runtime v0.21.1 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/strfmt v0.21.1 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/validate v0.20.3 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.7 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/processout/grpc-go-pool v1.2.1 // indirect + github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.28.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/valyala/fastjson v1.6.3 // indirect + go.mongodb.org/mongo-driver v1.7.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sys v0.0.0-20211123173158-ef496fb156ab // indirect + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/text v0.3.7 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/apiextensions-apiserver v0.23.2 // indirect + k8s.io/component-base v0.23.2 // indirect + k8s.io/klog v1.0.0 // indirect + k8s.io/klog/v2 v2.30.0 // indirect + k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 // indirect + k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect + sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect + sigs.k8s.io/kustomize/pseudo/k8s v0.1.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect +) + +replace github.com/RafaySystems/rcloud-base/components/common v0.0.0-unpublished => /home/infracloud/development/vscode-workspace/rafay/rcloud-base/components/common/ diff --git a/components/relay/go.sum b/components/relay/go.sum new file mode 100644 index 0000000..93ffcef --- /dev/null +++ b/components/relay/go.sum @@ -0,0 +1,1301 @@ +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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +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/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +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/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +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-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +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/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/tcpkeepalive v0.0.0-20160804073959-5bb0b2dea91e h1:mVIjvOd7NckIwf9J4hLB2YWXBYjhREF4vBeZXZ8mrWM= +github.com/felixge/tcpkeepalive v0.0.0-20160804073959-5bb0b2dea91e/go.mod h1:z0yk3Pix6k848RFizhkU4uY36ts5pB1t3toBwudGbBo= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= +github.com/go-openapi/analysis v0.20.1 h1:zdVbw8yoD4SWZeq+cWdGgquaB0W4VrsJvDJHJND/Ktc= +github.com/go-openapi/analysis v0.20.1/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o= +github.com/go-openapi/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68= +github.com/go-openapi/loads v0.21.0/go.mod h1:rHYve9nZrQ4CJhyeIIFJINGCg1tQpx2yJrrNo8sf1ws= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.21.1 h1:/KIG00BzA2x2HRStX2tnhbqbQdPcFlkgsYCiNY20FZs= +github.com/go-openapi/runtime v0.21.1/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1 h1:G6s2t5V5kGCHLVbSdZ/6lI8Wm4OzoPFkc3/cjAsKQrM= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw= +github.com/go-openapi/validate v0.20.3/go.mod h1:goDdqVGiigM3jChcrYJxD2joalke3ZXeftD16byIjA4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +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/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 h1:I8MsauTJQXZ8df8qJvEln0kYNc3bSapuaSsEsnFdEFU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3/go.mod h1:lZdb/YAJUSj9OqrCHs2ihjtoO3+xK3G53wTYXFWRGDo= +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/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE= +github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +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/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +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.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/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/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/processout/grpc-go-pool v1.2.1 h1:hbp1BOA02CIxEAoRLHGpUhhPFv77nwfBLBeO3Ya9P7I= +github.com/processout/grpc-go-pool v1.2.1/go.mod h1:F4hiNj96O6VQ87jv4rdz8R9tkHdelQQJ/J2B1a5VSt4= +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.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= +github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +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.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/russross/blackfriday/v2 v2.0.1/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/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.3.3 h1:VG1HceOLwHkSDdkxshlu9pD4FftWkScmHc8RDQ+w9uM= +github.com/segmentio/encoding v0.3.3/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +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.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +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/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +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/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +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/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= +github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/uptrace/bun v1.0.21/go.mod h1:u+QsgCgjGFwshy3euGAN1CLEO9RMf42lga5jQ/ezYsc= +github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= +github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc= +go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +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-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/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-20190320064053-1272bf9dcd53/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +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-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20180909124046-d0be0721c37e/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-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211123173158-ef496fb156ab h1:rfJ1bsoJQQIAoAxTxB7bme+vHrNkRw8CqfsYh9w54cw= +golang.org/x/sys v0.0.0-20211123173158-ef496fb156ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/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-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190125232054-d66bd3c5d5a6/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-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/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-20190624222133-a101b041ded4/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-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +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/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= +k8s.io/api v0.23.2 h1:62cpzreV3dCuj0hqPi8r4dyWh48ogMcyh+ga9jEGij4= +k8s.io/api v0.23.2/go.mod h1:sYuDb3flCtRPI8ghn6qFrcK5ZBu2mhbElxRE95qpwlI= +k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= +k8s.io/apiextensions-apiserver v0.23.2 h1:N6CIVAhmF0ahgFKUMDdV/AUyckhUb4nIyVPohPtdUPk= +k8s.io/apiextensions-apiserver v0.23.2/go.mod h1:9cs7avT6+GfzbB0pambTvH11wcaR85QQg4ovl9s15UU= +k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= +k8s.io/apimachinery v0.23.2 h1:dBmjCOeYBdg2ibcQxMuUq+OopZ9fjfLIR5taP/XKeTs= +k8s.io/apimachinery v0.23.2/go.mod h1:zDqeV0AK62LbCI0CI7KbWCAYdLg+E+8UXJ0rIz5gmS8= +k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= +k8s.io/apiserver v0.23.2/go.mod h1:Kdt8gafkPev9Gfh+H6lCPbmRu42f7BfhOfHKKa3dtyU= +k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= +k8s.io/client-go v0.23.2 h1:BNbOcxa99jxHH8mM1cPKGIrrKRnCSAfAtyonYGsbFtE= +k8s.io/client-go v0.23.2/go.mod h1:k3YbsWg6GWdHF1THHTQP88X9RhB1DWPo3Dq7KfU/D1c= +k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= +k8s.io/code-generator v0.23.2/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= +k8s.io/component-base v0.23.2 h1:dAYmUhWIBWO762etTjBEEKtYYHi5CoQInSLtK6LM1Zs= +k8s.io/component-base v0.23.2/go.mod h1:wS9Z03MO3oJ0RU8bB/dbXTiluGju+SC/F5i660gxB8c= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26 h1:2G24ndYyfk0l23ZrGutxb0s9TRe4m1ZjFlcu4cEU1zA= +k8s.io/kube-openapi v0.0.0-20220114203427-a0453230fd26/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/utils v0.0.0-20191030222137-2b95a09bc58d/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ= +sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/kustomize/pseudo/k8s v0.1.0 h1:otg4dLFc03c3gzl+2CV8GPGcd1kk8wjXwD+UhhcCn5I= +sigs.k8s.io/kustomize/pseudo/k8s v0.1.0/go.mod h1:bl/gVJgYYhJZCZdYU2BfnaKYAlqFkgbJEkpl302jEss= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/components/relay/main.go b/components/relay/main.go new file mode 100644 index 0000000..dd666f8 --- /dev/null +++ b/components/relay/main.go @@ -0,0 +1,148 @@ +// Copyright (C) 2020 Rafay Systems https://rafay.co/ + +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "net/http" + _ "net/http/pprof" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/agent" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relay" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/tail" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" +) + +var ( + log *relaylogger.RelayLog +) + +func exit(cancel context.CancelFunc) { + cancel() + os.Exit(0) +} + +func terminate(cancel context.CancelFunc) { + cancel() + os.Exit(1) +} + +//os signal handler +func signalHandler(sig os.Signal, cancel context.CancelFunc) { + if sig == syscall.SIGINT || sig == syscall.SIGKILL || sig == syscall.SIGTERM || sig == syscall.SIGQUIT { + log.Error( + nil, + "Received", + "signal", sig, + ) + exit(cancel) + return + } + + log.Info( + "Received", + "signal", sig, + ) +} + +func main() { + + flag.String("mode", "server", "mode of relay, accepeted values server/client") + flag.Int("log-level", 1, "log leve, accepted values 0-3") + flag.Bool("profile", false, "enable profiling") + flag.Int("profile-port", 13000, "profiler http port") + + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + viper.BindPFlags(pflag.CommandLine) + + mode := viper.GetString("mode") + logLevel := viper.GetInt("log-level") + profile := viper.GetBool("profile") + profilePort := viper.GetInt("profile-port") + + utils.GenUUID() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + log = relaylogger.NewLogger(logLevel).WithName("Relay") + + signalChan := make(chan os.Signal, 2) + signal.Notify(signalChan, + os.Interrupt, + syscall.SIGINT, + syscall.SIGHUP, + syscall.SIGKILL, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + log.Info( + "Relay", + "mode", mode, + "loglevel", logLevel, + ) + + utils.LogLevel = logLevel + + if profile { + go func() { + log.Info("profiling enabled", "profilePort", profilePort) + err := http.ListenAndServe(fmt.Sprintf(":%d", profilePort), nil) + if err != nil { + panic(err) + } + }() + } + +restart: + switch mode { + case "client": + log.Info("Starting relay agent client") + utils.Mode = utils.RELAYAGENT + go agent.RunRelayKubeCTLAgent(ctx, logLevel) + case "cdclient": + log.Info("Starting relay agent client") + utils.Mode = utils.CDRELAYAGENT + go agent.RunRelayCDAgent(ctx, logLevel) + case "tail": + log.Info("Starting relay log tail") + go tail.RunRelayTail(ctx) + case "cdrelay": + utils.Mode = utils.CDRELAY + log.Info("Starting cdrelay server") + go relay.RunCDRelayServer(ctx, logLevel) + default: + // default server + utils.Mode = utils.RELAY + log.Info("Starting relay server") + go relay.RunRelayServer(ctx, logLevel) + } + + for { + select { + case <-utils.ExitChan: + log.Info( + "got exit", + ) + time.Sleep(time.Second * 5) + goto restart + case <-utils.TerminateChan: + terminate(cancel) + case sig := <-signalChan: + signalHandler(sig, cancel) + } + } +} diff --git a/components/relay/pkg/agent/agent.go b/components/relay/pkg/agent/agent.go new file mode 100644 index 0000000..705ce21 --- /dev/null +++ b/components/relay/pkg/agent/agent.go @@ -0,0 +1,595 @@ +package agent + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "strconv" + "time" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/apply" + clientutil "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/client" + "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/register" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/cleanup" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/proxy" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/tunnel" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + podNameEnv = "POD_NAME" + podNamespaceEnv = "POD_NAMESPACE" + maxDialsEnv = "MAX_DIALS" + dialoutProxyEnv = "DIALOUT_PROXY" + dialoutProxyAuthEnv = "DIALOUT_PROXY_AUTHENTICATION" // user:password + configNameEnv = "RELAY_CONFIGMAP_NAME" + configNameCD = "cd-agent-configmap" + cdConfigPath = "/etc/config/relayConfigData" + relayRegisterTokenName = "relays" + relayClusterIDName = "clusterID" + relayAgentIDName = "agentID" +) + +var ( + podName string + podNamespace string + relayRegisterToken string + relayClusterID string + relayAgentID string + applier client.Client + lastHash string + log *relaylogger.RelayLog + configName string +) + +const ( + rafaySystemNamespace = "rafay-system" +) + +func processRelayConfigData(cfgData string) error { + if err := json.Unmarshal([]byte(cfgData), &utils.RelayNetworks); err != nil { + log.Error( + err, + "failed to unmashal", + ) + return err + } + return nil +} + +func getConfigMap(ctx context.Context, c client.Client, name string) (*corev1.ConfigMap, error) { + cm := corev1.ConfigMap{} + + err := c.Get(ctx, client.ObjectKey{ + Namespace: rafaySystemNamespace, + Name: name, + }, &cm) + if err != nil { + return nil, err + } + + cm.TypeMeta = metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + } + + return &cm, nil +} + +func getConfigHash(ctx context.Context, configMap string) (string, error) { + + cm, err := getConfigMap(ctx, applier, configMap) + if err != nil { + return "", err + } + + b, err := json.Marshal(cm.Data) + if err != nil { + return "", err + } + + h := sha256.New() + h.Write(b) + + //return cm.Annotations[hash.ObjectHash], nil + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +func getRelayAgentConfig(ctx context.Context) error { + var ok bool + + cm, err := getConfigMap(ctx, applier, configName) + if err != nil { + return err + } + + if relayClusterID, ok = cm.Data[relayClusterIDName]; !ok { + return fmt.Errorf("failed to get relayClusterID") + } + if relayRegisterToken, ok = cm.Data[relayRegisterTokenName]; !ok { + return fmt.Errorf("failed to get relayRegisterToken") + } + + if err := processRelayConfigData(relayRegisterToken); err != nil { + return fmt.Errorf("failed to process relayRegisterToken %s", relayRegisterToken) + } + if len(utils.RelayNetworks) <= 0 { + return fmt.Errorf("empty relaynetwork configmap %s", relayRegisterToken) + } + + log.Info( + "relay agent config values", + "podName", podName, + "podNamespace", podNamespace, + ) + + for _, item := range utils.RelayNetworks { + log.Info( + "relay network info", + "Token", item.Token, + "Addr", item.Addr, + "Domain", item.Domain, + "Name", item.Name, + ) + } + + utils.ClusterID = relayClusterID + return nil +} + +func setupclient(ctx context.Context, log *relaylogger.RelayLog) error { + configName = "relay-agent-config" + viper.SetDefault(podNameEnv, "relay-agent") + viper.SetDefault(podNamespaceEnv, "rafay-system") + viper.BindEnv(podNameEnv) + viper.BindEnv(podNamespaceEnv) + + podName = viper.GetString(podNameEnv) + podNamespace = viper.GetString(podNamespaceEnv) + + utils.PODNAME = podName + + viper.SetDefault(maxDialsEnv, "10") + viper.BindEnv(maxDialsEnv) + utils.MaxDials = viper.GetInt(maxDialsEnv) + + viper.SetDefault(dialoutProxyEnv, "") + viper.BindEnv(dialoutProxyEnv) + + if u, err := url.Parse(viper.GetString(dialoutProxyEnv)); err == nil { + utils.DialoutProxy = u.Host + } else { + utils.DialoutProxy = viper.GetString(dialoutProxyEnv) + } + + viper.SetDefault(dialoutProxyAuthEnv, "") + viper.BindEnv(dialoutProxyAuthEnv) + proxyAuth := viper.GetString(dialoutProxyAuthEnv) + utils.DialoutProxyAuth = proxyAuth + + if utils.MaxDials < utils.MinDials { + utils.MaxDials = utils.MinDials + } + + viper.SetDefault(configNameEnv, "") + viper.BindEnv(configNameEnv) + customeRelayCfgMap := viper.GetString(configNameEnv) + if customeRelayCfgMap != "" { + configName = customeRelayCfgMap + } + + err := getRelayAgentConfig(ctx) + if err != nil { + log.Error( + err, + "failed to get relay config", + ) + return err + } + return nil +} + +func setupCDClient(ctx context.Context, log *relaylogger.RelayLog) error { + + viper.SetDefault(podNameEnv, "relay-cdagent") + viper.SetDefault(podNamespaceEnv, "rafay-system") + viper.BindEnv(podNameEnv) + viper.BindEnv(podNamespaceEnv) + + podName = viper.GetString(podNameEnv) + podNamespace = viper.GetString(podNamespaceEnv) + + utils.PODNAME = podName + + viper.SetDefault(maxDialsEnv, "2") + viper.BindEnv(maxDialsEnv) + // utils.MaxDials = viper.GetInt(maxDialsEnv) + + viper.SetDefault(dialoutProxyEnv, "") + viper.BindEnv(dialoutProxyEnv) + // utils.DialoutProxy = viper.GetString(dialoutProxyEnv) + + viper.SetDefault(dialoutProxyAuthEnv, "") + viper.BindEnv(dialoutProxyAuthEnv) + // proxyAuth := viper.GetString(dialoutProxyAuthEnv) + // utils.DialoutProxyAuth = base64.StdEncoding.EncodeToString([]byte(proxyAuth)) + + // if utils.MaxDials < 1 { + // utils.MaxDials = 2 + // } + + err := getRelayCDAgentConfig(ctx) + if err != nil { + log.Error( + err, + "failed to get relay config", + ) + return err + } + return nil +} + +func getRelayCDAgentConfig(ctx context.Context) error { + var ok bool + + configBytes, err := ioutil.ReadFile(cdConfigPath) + if err != nil { + return err + } + + var config map[string]interface{} + err = json.Unmarshal(configBytes, &config) + if err != nil { + return err + } + + if relayAgentID, ok = config[relayAgentIDName].(string); !ok { + return fmt.Errorf("failed to get cd-relayClusterID") + } + + if maxDialsStr, ok := config["maxDials"].(string); ok { + maxDials, err := strconv.Atoi(maxDialsStr) + if err != nil { + maxDials = 2 + } + utils.MaxDials = maxDials + } + + if utils.MaxDials < utils.MinDials { + utils.MaxDials = utils.MinDials + } + + var dialoutProxy string + if dp, ok := config["dialoutProxy"]; ok { + if dialoutProxy, ok = dp.(string); ok { + } + } + + if dialoutProxy == "" { + if u, err := url.Parse(viper.GetString(dialoutProxyEnv)); err == nil { + dialoutProxy = u.Host + } else { + dialoutProxy = viper.GetString(dialoutProxyEnv) + } + } + + utils.DialoutProxy = dialoutProxy + + var dialoutProxyAuth string + if dpa, ok := config["dialoutProxyAuthentication"]; ok { + if dialoutProxyAuth, ok = dpa.(string); ok { + } + } + + if dialoutProxyAuth == "" { + dialoutProxyAuth = viper.GetString(dialoutProxyAuthEnv) + } + + utils.DialoutProxyAuth = dialoutProxyAuth + + if relays, ok := config[relayRegisterTokenName].(interface{}); ok { + relayRegisterTokenBytes, err := json.Marshal(relays) + if err != nil { + return fmt.Errorf("failed to get cd-relayRegisterToken: %s", err.Error()) + } + relayRegisterToken = string(relayRegisterTokenBytes) + } else { + return fmt.Errorf("failed to get cd-relayRegisterToken") + } + + if err := processRelayConfigData(relayRegisterToken); err != nil { + return fmt.Errorf("failed to process cd-relayRegisterToken %s", relayRegisterToken) + } + if len(utils.RelayNetworks) <= 0 { + return fmt.Errorf("empty cd-relaynetwork configmap %s", relayRegisterToken) + } + + log.Info( + "relay cd agent config values", + "podName", podName, + "podNamespace", podNamespace, + ) + + for _, item := range utils.RelayNetworks { + log.Info( + "relay network info", + "Token", item.Token, + "Addr", item.Addr, + "Domain", item.Domain, + "Name", item.Name, + ) + } + + utils.AgentID = relayAgentID + return nil +} + +// registerRelayAgent will register with rafay-core-relay-connector template token +// registration fetches client-certificate/root-ca to connect to relay server +func registerRelayAgent(ctx context.Context, rn utils.Relaynetwork) error { + cfg := ®ister.Config{ + TemplateToken: rn.TemplateToken, + Addr: rn.Addr, + ClientID: rn.Token, + ClientIP: utils.GetRelayIP(), + Name: podName, + } + + if utils.IsHTTPS(rn.Addr) { + cfg.Scheme = "https" + } + + log.Info("config:", cfg) + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register relay agent", + ) + return err + } + + log.Info( + "certificate:", + string(cfg.Certificate), + ) + + rc := utils.RelayNetworkConfig{} + rc.Network = rn + rc.RelayAgentCACert = cfg.CACertificate + rc.RelayAgentCert = cfg.Certificate + rc.RelayAgentKey = cfg.PrivateKey + + utils.RelayAgentConfig[rn.Name] = rc + + return nil +} + +func checkConfigMapChange(configMap string) bool { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + h, err := getConfigHash(ctx, configMap) + if err != nil { + log.Error( + err, + "failed to get relay configmap hash", + ) + return false + } + + if lastHash != "" { + if lastHash != h { + //hash changed + log.Info("hash of configmap data changed", "prev", lastHash, "curr", h) + lastHash = h + return true + } + } else { + log.Info("setting hash of configmap data", "hash", h) + lastHash = h + } + + return false +} + +// RunRelayCDAgent entry to the relay client +func RunRelayCDAgent(ctx context.Context, logLvl int) { + lastHash = "" + utils.RelayNetworks = nil + utils.RelayAgentConfig = make(map[string]utils.RelayNetworkConfig) + + log = relaylogger.NewLogger(logLvl).WithName("Relay Agent") + + c, err := clientutil.New() + if err != nil { + log.Error( + err, + "failed in clientutil new", + ) + utils.ExitChan <- true + return + } + + if err := proxy.InitUnixCacheRoundTripper(); err != nil { + log.Error( + err, + "failed to init unix cached round tripper", + ) + utils.ExitChan <- true + return + } + + applier = apply.NewApplier(c) + + rctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := setupCDClient(rctx, log); err != nil { + log.Error( + err, + "relay agent exiting", + ) + utils.ExitChan <- true + return + } + + for _, rn := range utils.RelayNetworks { + go handleRelayNetworks(rctx, rn) + } + + //watch config changes + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Error(err, "unable to create watcher for config file") + utils.ExitChan <- true + return + } + defer watcher.Close() + err = watcher.Add(cdConfigPath) + if err != nil { + log.Error(err, "unable to add config file to watcher") + } + for { + select { + case event, ok := <-watcher.Events: + if ok { + log.Info("event:", event, "modified file:", event.Name) + } + if event.Op != fsnotify.Chmod { + utils.ExitChan <- true + return + } + case err, ok := <-watcher.Errors: + if !ok { + utils.ExitChan <- true + return + } + log.Info("error:", err) + case <-ctx.Done(): + log.Info( + "Relay agent exiting", + ) + return + } + } + +} + +// RunRelayKubeCTLAgent entry to the relay client +func RunRelayKubeCTLAgent(ctx context.Context, logLvl int) { + lastHash = "" + utils.RelayNetworks = nil + utils.RelayAgentConfig = make(map[string]utils.RelayNetworkConfig) + + log = relaylogger.NewLogger(logLvl).WithName("Relay Agent") + + c, err := clientutil.New() + if err != nil { + log.Error( + err, + "failed in clientutil new", + ) + utils.TerminateChan <- true + return + } + + if err := proxy.InitUnixCacheRoundTripper(); err != nil { + log.Error( + err, + "failed to init unix cached round tripper", + ) + utils.ExitChan <- true + return + } + + applier = apply.NewApplier(c) + + rctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := setupclient(rctx, log); err != nil { + log.Error( + err, + "relay agent exiting", + ) + utils.ExitChan <- true + return + } + + go cleanup.StaleAuthz(rctx) + + for _, rn := range utils.RelayNetworks { + go handleRelayNetworks(rctx, rn) + } + + //ticker configmap changes + ticker := time.NewTicker(10 * time.Second) + for { + select { + case <-ticker.C: + if checkConfigMapChange(configName) { + // cancel the ctx + ticker.Stop() + utils.ExitChan <- true + return + } + case <-ctx.Done(): + log.Info( + "Relay agent exiting", + ) + return + } + } + +} + +func handleRelayNetworks(ctx context.Context, rn utils.Relaynetwork) { + rnExitChan := make(chan bool) +restartRegister: + ticker := time.NewTicker(5 * time.Second) + if err := registerRelayAgent(ctx, rn); err != nil { + //wait until config is available + for { + select { + case <-ticker.C: + goto restartRegister + case <-ctx.Done(): + ticker.Stop() + return + } + } + } + ticker.Stop() + +restartNetwork: + go tunnel.StartClient(ctx, log, "", utils.RelayAgentConfig[rn.Name], rnExitChan) + + for { + select { + case <-rnExitChan: + log.Info( + "relay network stopped, restart in 5 secs", + "name", rn.Name, + ) + time.Sleep(2 * time.Second) + goto restartNetwork + case <-ctx.Done(): + log.Info( + "Relay network exiting", + "name", rn.Name, + ) + return + } + } + +} diff --git a/components/relay/pkg/agent/lib.go b/components/relay/pkg/agent/lib.go new file mode 100644 index 0000000..65e84ad --- /dev/null +++ b/components/relay/pkg/agent/lib.go @@ -0,0 +1,36 @@ +package agent + +import ( + "context" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" +) + +// RunCDAgentRoutine starts cd agent. run as a go routine +func RunCDAgentRoutine(ctx context.Context, logLevel int) { + + _log := relaylogger.NewLogger(logLevel).WithName("Relay") + _log.Info("Starting relay agent client") + + utils.GenUUID() + utils.LogLevel = logLevel + utils.Mode = utils.CDRELAYAGENT + +restart: + go RunRelayCDAgent(ctx, logLevel) + + for { + select { + case <-utils.ExitChan: + log.Info( + "got exit", + ) + time.Sleep(time.Second * 5) + goto restart + case <-ctx.Done(): + return + } + } +} diff --git a/components/relay/pkg/audit/audit.go b/components/relay/pkg/audit/audit.go new file mode 100644 index 0000000..10de238 --- /dev/null +++ b/components/relay/pkg/audit/audit.go @@ -0,0 +1,97 @@ +package audit + +import ( + "net/http" + "strings" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/sessions" + "github.com/felixge/httpsnoop" + "github.com/rs/xid" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// options holds audit options +type options struct { + basePath string + maxSizeMB int + maxBackups int + maxAgeDays int +} + +// Option is the functional argument for audit options +type Option func(o *options) + +// WithBasePath sets base path for audit logs +func WithBasePath(path string) Option { + return func(o *options) { + o.basePath = path + } +} + +// WithMaxSizeMB sets max size of audit file before it is rotated +func WithMaxSizeMB(size int) Option { + return func(o *options) { + o.maxSizeMB = size + } +} + +// WithMaxBackups sets maximum number of backed up rotated audit logs to maintain +func WithMaxBackups(backups int) Option { + return func(o *options) { + o.maxBackups = backups + } +} + +// WithMaxAgeDays sets age after which backed up rotated audit logs will be deleted +func WithMaxAgeDays(ageDays int) Option { + return func(o *options) { + o.maxAgeDays = ageDays + } +} + +type audit struct { + logger *zap.Logger +} + +func (a *audit) handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + metrics := httpsnoop.CaptureMetrics(next, w, r) + + fields := make([]zapcore.Field, 0, 10) + fields = append(fields, zap.String("xid", xid.New().String())) + fields = append(fields, zap.String("method", r.Method)) + fields = append(fields, zap.String("url", r.URL.EscapedPath())) + fields = append(fields, zap.String("query", r.URL.RawQuery)) + fields = append(fields, zap.String("serverName", r.TLS.ServerName)) + fields = append(fields, zap.String("user", r.TLS.PeerCertificates[0].Subject.CommonName)) + fields = append(fields, zap.String("remoteAddr", strings.Split(r.RemoteAddr, ":")[0])) + fields = append(fields, zap.Int("statusCode", metrics.Code)) + fields = append(fields, zap.Int64("written", metrics.Written)) + fields = append(fields, zap.Duration("duration", metrics.Duration)) + if r.Header.Get("X-Rafay-Audit") == "" { + a.logger.Info("access", fields...) + } else { + a.logger.Info("audit", fields...) + } + + if metrics.Code == http.StatusUnauthorized || metrics.Code == http.StatusBadGateway { + sessKey := r.Header.Get("X-Rafay-Sessionkey") + sessions.SetSessionErrorFlag(sessKey) + } + + }) +} + +// WrapWithAudit wrap audit handler around next http.Handler +func WrapWithAudit(next http.Handler, opts ...Option) http.Handler { + + auditOpts := &options{} + for _, opt := range opts { + opt(auditOpts) + } + + audit := &audit{logger: getAuditLogger(auditOpts)} + return audit.handler(next) +} diff --git a/components/relay/pkg/audit/logger.go b/components/relay/pkg/audit/logger.go new file mode 100644 index 0000000..49eda82 --- /dev/null +++ b/components/relay/pkg/audit/logger.go @@ -0,0 +1,30 @@ +package audit + +import ( + "fmt" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +func getAuditLogger(opts *options) *zap.Logger { + + path := fmt.Sprintf("%s/audit.log", opts.basePath) + + encoder := zap.NewProductionEncoderConfig() + encoder.EncodeTime = zapcore.ISO8601TimeEncoder + + logger := zap.New(zapcore.NewCore( + zapcore.NewJSONEncoder(encoder), + zapcore.AddSync(&lumberjack.Logger{ + Filename: path, + MaxSize: opts.maxSizeMB, // megabytes + MaxBackups: opts.maxBackups, + MaxAge: opts.maxAgeDays, // days + }), + zap.InfoLevel, + )) + + return logger +} diff --git a/components/relay/pkg/cleanup/authz.go b/components/relay/pkg/cleanup/authz.go new file mode 100644 index 0000000..f4d7d3d --- /dev/null +++ b/components/relay/pkg/cleanup/authz.go @@ -0,0 +1,150 @@ +package cleanup + +import ( + "context" + "strconv" + "time" + + clientutil "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/client" + "github.com/RafaySystems/rcloud-base/components/common/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" +) + +var ( + _log = log.GetLogger() +) + +const ( + // how often should the cleanup routine run + sweepInterval = time.Minute * 10 + // any authz which is olderThan this should be cleaned up + olderThan = time.Hour * 8 + + rafayRelayLabel = "rafay-relay" + authzRefreshLabel = "authz-refreshed" + rafaySystemNS = "rafay-system" + + maxResults = 50 +) + +func getMatchingLabelSelector() (sel client.MatchingLabelsSelector, err error) { + var relayLabelReq, refreshLabelReq *labels.Requirement + + relayLabelReq, err = labels.NewRequirement(rafayRelayLabel, selection.Equals, []string{"true"}) + if err != nil { + _log.Infow("unable to create relay label requirment", "error", err) + return + } + + refreshLabelReq, err = labels.NewRequirement( + authzRefreshLabel, + selection.LessThan, + []string{strconv.FormatInt(time.Now().Add(-olderThan).Unix(), 10)}, + ) + if err != nil { + _log.Infow("unable to create authz refresh requirment", "error", err) + return + } + sel.Selector = labels.NewSelector().Add(*relayLabelReq, *refreshLabelReq) + + return +} + +func cleanServiceAccounts(ctx context.Context, c client.Client, sel client.MatchingLabelsSelector) { + nCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + var saList corev1.ServiceAccountList + + err := c.List(nCtx, &saList, sel, client.InNamespace(rafaySystemNS), client.Limit(maxResults)) + if err != nil { + _log.Infow("unable to list service accounts", "error", err) + return + } + + for _, item := range saList.Items { + _log.Infow("found stale service account", "name", item.Name) + err = c.Delete(ctx, &item) + if err != nil { + _log.Infow("unable to delete stale service account", "name", item.Name, "error", err) + } + } + +} + +func cleanClusterRoleBindings(ctx context.Context, c client.Client, sel client.MatchingLabelsSelector) { + nCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + var crbList rbacv1.ClusterRoleBindingList + + err := c.List(nCtx, &crbList, sel, client.Limit(maxResults)) + if err != nil { + _log.Infow("unable to list cluster role bindings", "error", err) + return + } + + for _, item := range crbList.Items { + _log.Infow("found stale cluster role binding", "name", item.Name) + err = c.Delete(ctx, &item) + if err != nil { + _log.Infow("unable to delete cluster role binding", "name", item.Name, "error", err) + } + } +} + +func cleanRoleBindings(ctx context.Context, c client.Client, sel client.MatchingLabelsSelector) { + nCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + var rbList rbacv1.RoleBindingList + + err := c.List(nCtx, &rbList, sel, client.Limit(maxResults)) + if err != nil { + _log.Infow("unable to list role bindings", "error", err) + return + } + + for _, item := range rbList.Items { + _log.Infow("found stale role binding", "name", item.Name) + err = c.Delete(ctx, &item) + if err != nil { + _log.Infow("unable to delete role binding", "name", item.Name, "error", err) + } + } +} + +// StaleAuthz cleans up state authorizations provisioned in the cluster +func StaleAuthz(ctx context.Context) { + c, err := clientutil.New() + if err != nil { + _log.Fatalw("unable to create client for cleaning stale authz entries", "error", err) + } + + ticker := time.NewTicker(sweepInterval) +sweepLoop: + for ; true; <-ticker.C { + select { + case <-ctx.Done(): + break sweepLoop + default: + } + + sel, err := getMatchingLabelSelector() + if err != nil { + _log.Infow("unable to build matching label selector", "error", err) + continue + } + + _log.Infow("finding authz matching", "selector", sel) + + cleanClusterRoleBindings(ctx, c, sel) + cleanRoleBindings(ctx, c, sel) + cleanServiceAccounts(ctx, c, sel) + } +} diff --git a/components/relay/pkg/profile/profile.go b/components/relay/pkg/profile/profile.go new file mode 100644 index 0000000..b9b3d71 --- /dev/null +++ b/components/relay/pkg/profile/profile.go @@ -0,0 +1,31 @@ +package profile + +import ( + "context" + "fmt" + "net/http" + _ "net/http/pprof" + "time" +) + +// RunProfiler runs pprof on the given port +func RunProfiler(port int, stop <-chan struct{}) error { + + s := http.Server{ + Addr: fmt.Sprintf(":%d", port), + } + + defer func() { + <-stop + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + s.Shutdown(ctx) + }() + + err := s.ListenAndServe() + if err != nil { + return err + } + + return nil +} diff --git a/components/relay/pkg/proxy/kube_proxy.go b/components/relay/pkg/proxy/kube_proxy.go new file mode 100644 index 0000000..0e6b786 --- /dev/null +++ b/components/relay/pkg/proxy/kube_proxy.go @@ -0,0 +1,299 @@ +package proxy + +import ( + "context" + "crypto/tls" + "encoding/json" + "hash" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/dgraph-io/ristretto" + utilnet "k8s.io/apimachinery/pkg/util/net" + k8proxy "k8s.io/apimachinery/pkg/util/proxy" + "k8s.io/client-go/rest" + "k8s.io/client-go/transport" +) + +// TODO convert to JSON +type responder struct{} + +func (r *responder) Error(w http.ResponseWriter, req *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +type unixCachedRoundTrip struct { + proxy http.Handler + t *http.Transport +} + +type relayCachedPeerRoundTrip struct { + proxy http.Handler + t *http.Transport +} + +var ( + cacheTTL = time.Minute * 15 + // cache map of unix round tripper + unixCachedRoundTripper *ristretto.Cache + // cache map for peer upstream round tripper + relayPeerRoundTripper *ristretto.Cache +) + +// InitUnixCacheRoundTripper initialize the cache +func InitUnixCacheRoundTripper() error { + var err error + unixCachedRoundTripper, err = ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 25, // Maximum cost of cache (32MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + + return err +} + +// InitPeerCacheRoundTripper initialize the cache +func InitPeerCacheRoundTripper() error { + var err error + relayPeerRoundTripper, err = ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 25, // Maximum cost of cache (32MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + + return err +} + +// makeUpgradeTransport creates a transport that explicitly bypasses HTTP2 support +// for proxy connections that must upgrade. +func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (k8proxy.UpgradeRequestRoundTripper, error) { + + transportConfig, err := config.TransportConfig() + if err != nil { + return nil, err + } + tlsConfig, err := transport.TLSConfigFor(transportConfig) + if err != nil { + return nil, err + } + tlsConfig.NextProtos = []string{"http/1.1"} + rt := utilnet.SetOldTransportDefaults(&http.Transport{ + TLSClientConfig: tlsConfig, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: keepalive, + }).DialContext, + TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), + MaxIdleConns: idleConns, + MaxConnsPerHost: idleConnsPerHost, + }) + + upgrader, err := transport.HTTPWrappersForConfig(transportConfig, k8proxy.MirrorRequest) + if err != nil { + return nil, err + } + + return k8proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil +} + +// NewCachedKubeHandler is cached kube handler +func NewCachedKubeHandler(cfg *rest.Config, keepalive time.Duration) (*k8proxy.UpgradeAwareHandler, error) { + host := cfg.Host + if !strings.HasSuffix(host, "/") { + host = host + "/" + } + target, err := url.Parse(host) + if err != nil { + return nil, err + } + + responder := &responder{} + + transport, err := rest.TransportFor(cfg) + if err != nil { + return nil, err + } + + rt, err := NewCachedRoundTripper(transport) + if err != nil { + return nil, err + } + + upgradeTransport, err := makeUpgradeTransport(cfg, keepalive) + if err != nil { + return nil, err + } + + kproxy := k8proxy.NewUpgradeAwareHandler(target, rt, false, false, responder) + kproxy.UpgradeTransport = upgradeTransport + kproxy.UseRequestLocation = true + + return kproxy, nil +} + +// UnixDialContext unix dial +func UnixDialContext(sockPath, key, username, sni string) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + + conn, err := net.DialTimeout("unix", sockPath, 60*time.Second) + if err != nil { + return nil, err + } + + hdr := &utils.ProxyProtocolMessage{ + DialinKey: key, + UserName: username, + SNI: sni, + } + + b, err := json.Marshal(hdr) + if err != nil { + return nil, err + } + + buf := make([]byte, utils.ProxyProtocolSize) + copy(buf, b) + + _, err = conn.Write(buf) + if err != nil { + return nil, err + } + return conn, nil + } +} + +func makeUnixUpgradeTransport(sockPath, key, username, sni string) (k8proxy.UpgradeRequestRoundTripper, error) { + + rt := utilnet.SetOldTransportDefaults(&http.Transport{ + DialContext: UnixDialContext(sockPath, key, username, sni), + }) + + upgrader, err := transport.HTTPWrappersForConfig(new(transport.Config), k8proxy.MirrorRequest) + if err != nil { + return nil, err + } + return k8proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil +} + +func getRTCacheKey(skey string) (key uint64) { + hasher := _hashPool.Get().(hash.Hash64) + hasher.Reset() + hasher.Write([]byte(skey)) + key = hasher.Sum64() + _hashPool.Put(hasher) + return +} + +type unixHandlerOptions struct { + sockPath string + key string + username string + sni string +} + +//UnixKubeHandler unix handler +func UnixKubeHandler(sockPath, key, username, sni string) (http.Handler, error) { + + hkey := getRTCacheKey(username + key) + if val, ok := unixCachedRoundTripper.Get(hkey); ok { + rt := val.(unixCachedRoundTrip) + return rt.proxy, nil + } + + target, err := url.Parse("http://unixpath-relay/") + if err != nil { + return nil, err + } + + responder := &responder{} + + upgradeTransport, err := makeUnixUpgradeTransport(sockPath, key, username, sni) + if err != nil { + return nil, err + } + + t := getUnixTransport(sockPath, key, username, sni) + proxy := k8proxy.NewUpgradeAwareHandler(target, t, false, false, responder) + proxy.UpgradeTransport = upgradeTransport + proxy.UseRequestLocation = true + + rt := unixCachedRoundTrip{ + proxy: proxy, + t: t, + } + + unixCachedRoundTripper.SetWithTTL(hkey, rt, 100, cacheTTL) + + return proxy, nil +} + +// Peer Relay Proxy Helpers +func peerDialContext(relayIP string) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := net.DialTimeout("tcp", relayIP, 60*time.Second) + if err != nil { + return nil, err + } + return conn, nil + } +} + +//makePeerUpgradeTransport ... +func makePeerUpgradeTransport(relayIP string, tlscfg *tls.Config) (k8proxy.UpgradeRequestRoundTripper, error) { + rt := utilnet.SetOldTransportDefaults(&http.Transport{ + DialContext: peerDialContext(relayIP), + TLSClientConfig: tlscfg, + MaxIdleConns: 10, + IdleConnTimeout: utils.IdleTimeout, + TLSHandshakeTimeout: 30 * time.Second, + // Keep the peer upstream non http2 + // Found issues in upgradeaware http2 + // missing http2 preface when doing proxy. + ForceAttemptHTTP2: false, + }) + + upgrader, err := transport.HTTPWrappersForConfig(new(transport.Config), k8proxy.MirrorRequest) + if err != nil { + return nil, err + } + return k8proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil +} + +//PeerKubeHandler peer proxying handler +func PeerKubeHandler(tlscfg *tls.Config, relayIP string) (http.Handler, error) { + hkey := getRTCacheKey(tlscfg.ServerName) + if val, ok := relayPeerRoundTripper.Get(hkey); ok { + rt := val.(relayCachedPeerRoundTrip) + return rt.proxy, nil + } + + target, err := url.Parse("https://" + tlscfg.ServerName + "/") + if err != nil { + return nil, err + } + + responder := &responder{} + + upgradeTransport, err := makePeerUpgradeTransport(relayIP, tlscfg) + if err != nil { + return nil, err + } + + t := getPeerTransport(tlscfg, relayIP) + proxy := k8proxy.NewUpgradeAwareHandler(target, t, false, false, responder) + proxy.UpgradeTransport = upgradeTransport + proxy.UseRequestLocation = true + + rt := relayCachedPeerRoundTrip{ + proxy: proxy, + t: t, + } + + relayPeerRoundTripper.SetWithTTL(hkey, rt, 100, cacheTTL) + + return proxy, nil +} diff --git a/components/relay/pkg/proxy/proxy.go b/components/relay/pkg/proxy/proxy.go new file mode 100644 index 0000000..c760431 --- /dev/null +++ b/components/relay/pkg/proxy/proxy.go @@ -0,0 +1,12 @@ +package proxy + +import ( + "io" + "net/http" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" +) + +// Func is responsible for forwarding a remote connection to local server +// and writing the response. +type Func func(w io.Writer, r io.ReadCloser, msg *utils.ControlMessage, req *http.Request) diff --git a/components/relay/pkg/proxy/round_tripper.go b/components/relay/pkg/proxy/round_tripper.go new file mode 100644 index 0000000..2bf5205 --- /dev/null +++ b/components/relay/pkg/proxy/round_tripper.go @@ -0,0 +1,214 @@ +package proxy + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "hash" + "net/http" + "sync" + "time" + + "github.com/twmb/murmur3" + + clientutil "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/client" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/dgraph-io/ristretto" + utilnet "k8s.io/apimachinery/pkg/util/net" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + logger = relaylogger.NewLogger(utils.LogLevel).WithName("Proxy") + // ErrInvalidUser is returned for invalid user + ErrInvalidUser = errors.New("invalid user") +) + +var _hashPool = sync.Pool{ + New: func() interface{} { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return murmur3.New64() + }, +} + +const ( + defaulTLSHandshakeTimeout = 10 * time.Second + idleConnsPerHost = 5 + idleConns = 25 + defaultTimeout = 30 * time.Second + defaultKeepAlive = 30 * time.Second + disableCompression = false +) + +func getUnixTransport(sockPath, key, username, sni string) *http.Transport { + return utilnet.SetTransportDefaults(&http.Transport{ + TLSHandshakeTimeout: defaulTLSHandshakeTimeout, + MaxIdleConnsPerHost: idleConnsPerHost, + DialContext: UnixDialContext(sockPath, key, username, sni), + DisableCompression: disableCompression, + MaxIdleConns: idleConns, + }) +} + +func getPeerTransport(tlscfg *tls.Config, relayIP string) *http.Transport { + if tlscfg != nil { + tlscfg.NextProtos = []string{"http/1.1"} + } + return &http.Transport{ + TLSHandshakeTimeout: defaulTLSHandshakeTimeout, + TLSClientConfig: tlscfg, + MaxIdleConns: idleConns, + MaxIdleConnsPerHost: idleConnsPerHost, + DialContext: peerDialContext(relayIP), + DisableCompression: disableCompression, + TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), + //Keep the peer upstream non http2 + ForceAttemptHTTP2: false, + } +} + +// NewCachedRoundTripper returns new cached round tripper +func NewCachedRoundTripper(rt http.RoundTripper) (http.RoundTripper, error) { + logger = relaylogger.NewLogger(utils.LogLevel).WithName("CachedRoundTripper") + c, err := clientutil.New() + if err != nil { + logger.Error( + err, + "failed in clientutil.New", + ) + return nil, err + } + + cache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 25, // Maximum cost of cache (32MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + if err != nil { + return nil, err + } + + crt := &cachedRoundTripper{client: c, cache: cache, rt: rt} + + return crt, nil +} + +type cachedRoundTripper struct { + client client.Client + rt http.RoundTripper + cache *ristretto.Cache +} + +func getCacheKey(userName, namespace string) (key uint64) { + hasher := _hashPool.Get().(hash.Hash64) + hasher.Reset() + hasher.Write([]byte(namespace)) + hasher.Write([]byte(userName)) + key = hasher.Sum64() + _hashPool.Put(hasher) + return +} + +func (rt *cachedRoundTripper) getToken(req *http.Request) (token string, err error) { + userName := req.Header.Get(utils.HeaderRafayUserName) + namespace := req.Header.Get(utils.HeaderRafayNamespace) + clearCache := req.Header.Get(utils.HeaderClearSecret) + if userName == "" || namespace == "" { + logger.Error( + nil, + "getToken failed", + "userName", userName, + "namespace", namespace, + "req", req, + ) + return "", ErrInvalidUser + } + + logger.Debug( + "getToken for ", + "user", userName, + "namespace", namespace, + "clearCache", clearCache, + ) + + key := getCacheKey(userName, namespace) + + if clearCache == "" { + if val, ok := rt.cache.Get(key); ok { + if strVal, ok := val.(string); ok { + logger.Debug( + "token found in cache ", + "user", userName, + "namespace", namespace, + ) + token = strVal + return + } + } + } else { + // Clear the header + req.Header.Del(utils.HeaderClearSecret) + } + + ctx, cancel := context.WithTimeout(req.Context(), time.Second*10) + defer cancel() + + secret, err := getServiceAccountSecret(ctx, rt.client, userName, namespace) + if err != nil { + return "", err + } + + token = string(secret.Data[tokenKey]) + rt.cache.SetWithTTL(key, token, 100, time.Minute*5) + return + +} + +func (rt *cachedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + logger.Debug("RoundTrip", "request", req) + // cache here + token, err := rt.getToken(req) + if err != nil { + logger.Error( + err, + "failed getToken in RoundTrip", + "req", req, + ) + return nil, err + } + + req = utilnet.CloneRequest(req) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + // remove x-rafay heders before sending to kube-apiserver + req.Header.Del("X-Rafay-Key") + req.Header.Del("X-Rafay-Namespace") + req.Header.Del("X-Rafay-User") + + return rt.rt.RoundTrip(req) +} + +func (rt *cachedRoundTripper) CancelRequest(req *http.Request) { + tryCancelRequest(rt.WrappedRoundTripper(), req) +} + +func (rt *cachedRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } + +func tryCancelRequest(rt http.RoundTripper, req *http.Request) { + type canceler interface { + CancelRequest(*http.Request) + } + switch rt := rt.(type) { + case canceler: + rt.CancelRequest(req) + case utilnet.RoundTripperWrapper: + tryCancelRequest(rt.WrappedRoundTripper(), req) + default: + logger.Error(nil, "Unable to cancel request") + } +} diff --git a/components/relay/pkg/proxy/service_account.go b/components/relay/pkg/proxy/service_account.go new file mode 100644 index 0000000..e1a7336 --- /dev/null +++ b/components/relay/pkg/proxy/service_account.go @@ -0,0 +1,157 @@ +package proxy + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/apply" + clientutil "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/client" + cruntime "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/runtime" + clusterv2 "github.com/RafaySystems/rcloud-base/components/common/proto/types/controller" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + corev1 "k8s.io/api/core/v1" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +var ( + svclogger = relaylogger.NewLogger(utils.LogLevel).WithName("ServiceAccount") + // ErrNotSAToken is returned when secret refered by service account is not of type service account token + ErrNotSAToken = errors.New("secert is not of type ServiceAccountToken") +) + +const ( + caCertKey = "ca.crt" + tokenKey = "token" +) + +// getServiceAccountSecret returns secret for the service account +func getServiceAccountSecret(ctx context.Context, c k8sclient.Client, name, namespace string) (*corev1.Secret, error) { + var serviceAccount corev1.ServiceAccount + var secret corev1.Secret + + err := c.Get(ctx, k8sclient.ObjectKey{ + Name: name, + Namespace: namespace, + }, &serviceAccount) + + if err != nil { + return nil, err + } + + for _, saSecret := range serviceAccount.Secrets { + err = c.Get(ctx, k8sclient.ObjectKey{ + Name: saSecret.Name, + Namespace: namespace, + }, &secret) + if err != nil { + continue + } + + if secret.Type == corev1.SecretTypeServiceAccountToken { + return &secret, nil + } + } + + return nil, fmt.Errorf("service account %s/%s does not have secrets of type ServiceAccountToken", namespace, name) +} + +//DeleteServiceAccount from cluster and cache +func DeleteServiceAccount(key, rafayAuthzSA, rafayAuthzRole, rafayAuthzRoleBind string, delCache bool) { + svclogger.Debug( + "DeleteServiceAccount", + key, + "not evicted", delCache, + ) + dstrSA, err1 := base64.StdEncoding.DecodeString(rafayAuthzSA) + dstrRole, err2 := base64.StdEncoding.DecodeString(rafayAuthzRole) + dstrRB, err3 := base64.StdEncoding.DecodeString(rafayAuthzRoleBind) + + if err1 == nil && err2 == nil && err3 == nil { + // delete sa,role,rolebinding + svclogger.Debug( + "delete service account", + "yaml SA:", + string(dstrSA), + "yaml Role:", + string(dstrRole), + "yaml RoleBind:", + string(dstrRB), + ) + + c, err := clientutil.New() + if err != nil { + svclogger.Error( + err, + "failed in clientutil new", + ) + return + } + applier := apply.NewApplier(c) + + sa, err1 := getObject(dstrSA) + role, err2 := getObject(dstrRole) + rb, err3 := getObject(dstrRB) + + if err1 == nil && err2 == nil && err3 == nil { + deleteAuthz(applier, rb) + deleteAuthz(applier, role) + deleteAuthz(applier, sa) + if delCache { + //delete from service cache + utils.DeleteCache(utils.ServiceAccountCache, key) + } + + return + } + + svclogger.Error( + nil, + "failed to cleanup service account", + "serviceaccount", string(dstrSA), + "role", string(dstrRole), + "rolebinding", string(dstrRB), + ) + } +} + +func getObject(yamlData []byte) (k8sclient.Object, error) { + jb, err := yaml.YAMLToJSON(yamlData) + if err != nil { + return nil, err + } + + var so clusterv2.StepObject + err = json.Unmarshal(jb, &so) + if err != nil { + return nil, err + } + + o, _, err := cruntime.ToUnstructuredObject(&so) + if err != nil { + return nil, err + } + + return o, nil +} + +func deleteAuthz(applier apply.Applier, obj k8sclient.Object) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + err := applier.Delete(ctx, obj) + if err != nil { + svclogger.Error( + err, + "failed to delete sa/role/rolebinding", + ) + return err + } + + return nil +} diff --git a/components/relay/pkg/proxy/tcpproxy.go b/components/relay/pkg/proxy/tcpproxy.go new file mode 100644 index 0000000..70de9ce --- /dev/null +++ b/components/relay/pkg/proxy/tcpproxy.go @@ -0,0 +1,244 @@ +package proxy + +import ( + "crypto/tls" + "io" + "net" + "net/http" + "net/url" + "os" + "syscall" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + k8proxy "k8s.io/apimachinery/pkg/util/proxy" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +// TCPProxy forwards TCP streams. +type TCPProxy struct { + // logger is the proxy logger. + logger *relaylogger.RelayLog + config *utils.ProxyConfig + handler *k8proxy.UpgradeAwareHandler + //kube-apiserver address + apiHost string +} + +func (tp *TCPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + u, err := url.Parse(tp.apiHost) + if err == nil { + r.URL.Host = u.Host + r.URL.Scheme = u.Scheme + r.Host = u.Host + } + r.Header.Set("Host", tp.apiHost) + utils.UnSetXForwardedRafay(r.Header) + tp.handler.ServeHTTP(w, r) +} + +func (tp *TCPProxy) backendProxy() error { + socketPath := utils.UNIXAGENTSOCKET + tp.config.ServiceSNI + syscall.Unlink(socketPath) + + l, err := net.Listen("unix", socketPath) + if err != nil { + tp.logger.Error( + err, + "failed to listen on unix", + "socketPath", socketPath, + ) + return err + } + + tp.logger.Info( + "started listening on unix", + "socketPath", socketPath, + ) + + cfg, err := config.GetConfig() + if err != nil { + tp.logger.Error( + err, + "failed in NewKubeHandler", + ) + return nil + } + + hndlr, err := NewCachedKubeHandler(cfg, utils.DefaultKeepAliveInterval) + if err != nil { + tp.logger.Error( + err, + "unable to create kube handler for config", + ) + return err + } + + tp.handler = hndlr + + go func() { + + s := &http.Server{ + Handler: http.Handler(tp), + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + + if err := s.Serve(l); err != nil { + tp.logger.Error( + err, + "unable to start backend server", + ) + } + }() + + tp.handler = hndlr + + return nil +} + +// NewKubeCtlTCPProxy creates new direct TCPProxy, everything will be proxied to +// unix socket. +func NewKubeCtlTCPProxy(lg *relaylogger.RelayLog, cfg *utils.ProxyConfig) *TCPProxy { + tp := &TCPProxy{ + logger: lg, + config: cfg, + } + + err := tp.backendProxy() + if err != nil { + return nil + } + + host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") + if len(host) == 0 || len(port) == 0 { + ec, err := config.GetConfig() + if err != nil { + tp.logger.Error( + err, + "failed to get kubeapi host:port", + ) + return nil + } + tp.apiHost = ec.Host + } else { + tp.apiHost = "https://" + net.JoinHostPort(host, port) + } + + return tp +} + +// NewTCPProxy creates new direct TCPProxy, everything will be proxied to +// unix socket. +func NewTCPProxy(lg *relaylogger.RelayLog, cfg *utils.ProxyConfig) *TCPProxy { + tp := &TCPProxy{ + logger: lg, + config: cfg, + } + + host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") + if len(host) == 0 || len(port) == 0 { + ec, err := config.GetConfig() + if err != nil { + tp.logger.Error( + err, + "failed to get kubeapi host:port", + ) + return nil + } + tp.apiHost = ec.Host + } else { + tp.apiHost = "https://" + net.JoinHostPort(host, port) + } + + return tp +} + +// Proxy is a ProxyFunc. +func (tp *TCPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *utils.ControlMessage, req *http.Request) { + var target string + var network string + var directionTO, directionFrom string + + switch tp.config.Protocol { + case utils.CDAGENTCORE: + if tp.config.Upstream != "" { + target = tp.config.Upstream + } else { + target = utils.DefaultTCPUpstream + } + network = "tcp" + directionTO = "tunnel to TCP" + directionFrom = "TCP to tunnel" + default: + target = utils.UNIXAGENTSOCKET + tp.config.ServiceSNI + network = "unix" + directionTO = "tunnel to unix" + directionFrom = "unix to tunnel" + } + + if target == "" { + tp.logger.Info( + "no unix target", + "ctrlMsg", msg, + ) + return + } + + local, err := net.DialTimeout(network, target, utils.DefaultMuxTimeout) + if err != nil { + tp.logger.Error( + err, + "msg dial failed", + "target", target, + "ctrlMsg", msg, + "err", err, + ) + return + } + defer local.Close() + + done := make(chan struct{}) + go func() { + utils.Transfer(flushWriter{w, tp.logger, local}, local, tp.logger, directionFrom) + close(done) + }() + + utils.Transfer(local, r, tp.logger, directionTO) + +proxyDone: + for { + select { + case <-req.Context().Done(): + break proxyDone + case <-done: + break proxyDone + } + } + // wait fr 2 sec before closing + time.Sleep(2 * time.Second) +} + +type flushWriter struct { + w io.Writer + lg *relaylogger.RelayLog + c net.Conn +} + +func (fw flushWriter) Write(p []byte) (n int, err error) { + n, err = fw.w.Write(p) + fw.lg.Debug( + "data wrote", + "data", string(p), + ) + if f, ok := fw.w.(http.Flusher); ok { + f.Flush() + } else { + fw.lg.Info( + "flushWriter http.Flusher not ok", + ) + } + // tolerate up to utils.IdleTimeout idletime + fw.c.SetDeadline(time.Now().Add(utils.IdleTimeout)) + return +} diff --git a/components/relay/pkg/proxy/testdata/role.yaml b/components/relay/pkg/proxy/testdata/role.yaml new file mode 100644 index 0000000..090e7f4 --- /dev/null +++ b/components/relay/pkg/proxy/testdata/role.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rafay-core-debug + namespace: default + labels: + creator: rafay-relay + expire: "yes" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: rafay-core-debug-role + namespace: default +rules: +- apiGroups: + - "" + resources: + - "*" + verbs: + - "*" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: rafay-core-debug-binding + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: rafay-core-debug-role +subjects: +- kind: ServiceAccount + name: rafay-core-debug diff --git a/components/relay/pkg/relay/relay.go b/components/relay/pkg/relay/relay.go new file mode 100644 index 0000000..d7c394e --- /dev/null +++ b/components/relay/pkg/relay/relay.go @@ -0,0 +1,888 @@ +package relay + +import ( + "context" + "crypto/x509/pkix" + "fmt" + "net" + "net/http" + "strconv" + "time" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/cryptoutil" + "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/register" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/tunnel" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/spf13/viper" +) + +const ( + podNameEnv = "POD_NAME" + podNamespaceEnv = "POD_NAMESPACE" + peerServiceURIEnv = "RAFAY_RELAY_PEERSERVICE" + relayListenIPEnv = "RELAY_LISTENIP" + sentryAddrEnv = "SENTRY_ADDR" // used by relay running inside core + auditPathEnv = "AUDIT_PATH" + metricListenEnv = "METRIC_LISTEN" + + // for relays running outside of core + // supply bootstrapAddr and token as environment variables + bootstrapAddrEnv = "BOOTSTRAP_ADDR" + relayPeeringTokenEnv = "RELAY_PEERING_TOKEN" + relayUserTokenEnv = "RELAY_USER_TOKEN" + relayConnectorTokenEnv = "RELAY_CONNECTOR_TOKEN" + relayUserHostPortEnv = "RELAY_USER_HOST_PORT" + relayConnectorHostPortEnv = "RELAY_CONNECTOR_HOST_PORT" + relayNetworkIDEnv = "RELAY_NETWORK_ID" + + // cd relay + cdRelayUserTokenEnv = "CD_RELAY_USER_TOKEN" + cdRelayConnectorTokenEnv = "CD_RELAY_CONNECTOR_TOKEN" +) + +var ( + podName string + podNamespace string + relayPeeringURI string + relayListenIP string + log *relaylogger.RelayLog + sentryAddr string // used by relay running inside core + auditPath string + metricListen string + + // for relays running outside of core + relayPeeringToken string + relayConnectorToken string + relayUserToken string + bootstrapAddr string + relayUserHostPort string + relayConnectorHostPort string + relayNetworkID string + + // cd relay + cdRelayConnectorToken string + cdRelayUserToken string +) + +func setupserver(log *relaylogger.RelayLog) error { + // Enable VIPER to read Environment Variables + viper.AutomaticEnv() + + viper.SetDefault(podNameEnv, "relay-pod") + viper.SetDefault(podNamespace, "rafay-system") + viper.SetDefault(peerServiceURIEnv, "https://peering.sentry.rafay.local:10001") + viper.SetDefault(sentryAddrEnv, "localhost:10000") + + // for relays running outside of core + viper.SetDefault(bootstrapAddrEnv, "") + viper.SetDefault(relayPeeringTokenEnv, "-") + viper.SetDefault(relayUserToken, "-") + viper.SetDefault(relayConnectorToken, "-") + viper.SetDefault(relayUserHostPortEnv, "") + viper.SetDefault(relayConnectorHostPortEnv, "") + viper.SetDefault(auditPathEnv, "/tmp") + viper.SetDefault(metricListenEnv, ":8003") + viper.SetDefault(relayNetworkIDEnv, "rafay-core-64a66h0asw") + + viper.BindEnv(podNameEnv) + viper.BindEnv(podNamespaceEnv) + viper.BindEnv(relayPeeringTokenEnv) + viper.BindEnv(sentryAddrEnv) + + // outside core configs + viper.BindEnv(bootstrapAddrEnv) + viper.BindEnv(relayUserHostPortEnv) + viper.BindEnv(relayConnectorHostPortEnv) + viper.BindEnv(relayUserTokenEnv) + viper.BindEnv(relayConnectorTokenEnv) + viper.BindEnv(relayNetworkIDEnv) + + // for testing + viper.BindEnv(relayListenIPEnv) + + podName = viper.GetString(podNameEnv) + podNamespace = viper.GetString(podNamespaceEnv) + sentryAddr = viper.GetString(sentryAddrEnv) + auditPath = viper.GetString(auditPathEnv) + + utils.PODNAME = podName + + relayPeeringURI = viper.GetString(peerServiceURIEnv) + + // outside core relay configurations + bootstrapAddr = viper.GetString(bootstrapAddrEnv) + relayUserHostPort = viper.GetString(relayUserHostPortEnv) + relayConnectorHostPort = viper.GetString(relayConnectorHostPortEnv) + relayNetworkID = viper.GetString(relayNetworkIDEnv) + + if sentryAddr == "" && bootstrapAddr == "" { + log.Error( + nil, + "missing sentry & bootstrap uri, please set one of the environment variable RAFAY_SENTRY[relay deployed inside core] (or) BOOTSTRAP_ADDR[outside core]", + ) + return fmt.Errorf("relay server failed in setupserver") + } + + if bootstrapAddr != "" { + // outside config found, unset inside boostrap endpoint. + // both cannot be set, only one mode is possible + sentryAddr = "" + } + + if relayPeeringURI == "" { + log.Error( + nil, + "missing relay peer service uri, please set RAFAY_RELAY_PEERSERVICE environment variable", + ) + return fmt.Errorf("relay server failed in setupserver") + } + + relayPeeringToken = viper.GetString(relayPeeringTokenEnv) + relayUserToken = viper.GetString(relayUserTokenEnv) + relayConnectorToken = viper.GetString(relayConnectorTokenEnv) + relayListenIP = viper.GetString(relayListenIPEnv) // used for running multiple relays in local test hosts + metricListen = viper.GetString(metricListenEnv) + + log.Info( + "relay server setup values", + "podName", podName, + "podNamespace", podNamespace, + "relayPeeringURI", relayPeeringURI, + "relayListenIP", relayListenIP, + ) + + if sentryAddr != "" { + log.Info("relay inside core: sentryAddr", sentryAddr) + } else { + log.Info( + "outside core relay: bootStrapURI", bootstrapAddr, + "relayPeeringToken", relayPeeringToken, + "relayUserToken", relayUserToken, + "relayConnectorToken", relayConnectorToken, + ) + if relayPeeringToken == "-" || relayUserToken == "-" || relayConnectorToken == "-" || relayUserHostPort == "" || relayConnectorHostPort == "" { + log.Error( + fmt.Errorf("missing env variable"), + "outside core relay: one of the required token is missing, please set missing environment variables", + "RELAY_PEERING_TOKEN", relayPeeringToken, + "RELAY_USER_TOKEN", relayUserToken, + "RELAY_CONNECTOR_TOKEN", relayConnectorToken, + "RELAY_USER_HOST_PORT", relayUserHostPort, + "RELAY_CONNECTOR_HOST_PORT", relayConnectorHostPort, + ) + return fmt.Errorf("relay server failed in setupserver") + } + + // process the user facing host:port + host, port, err := net.SplitHostPort(relayUserHostPort) + if err != nil { + log.Error( + err, + "failed to process RELAY_USER_HOST_PORT env variable", relayUserHostPort, + ) + return fmt.Errorf("relay server failed in setupserver") + } + utils.RelayUserPort, err = strconv.Atoi(port) + if err != nil { + log.Error( + err, + "invalid port in RELAY_USER_HOST_PORT env variable", relayUserHostPort, + ) + return fmt.Errorf("relay server failed in setupserver") + } + utils.RelayUserHost = host + + // process the connector facing host:port + host, port, err = net.SplitHostPort(relayConnectorHostPort) + if err != nil { + log.Error( + err, + "failed to process RELAY_CONNECTOR_HOST_PORT env variable", relayConnectorHostPort, + ) + return fmt.Errorf("relay server failed in setupserver") + } + utils.RelayConnectorPort, err = strconv.Atoi(port) + if err != nil { + log.Error( + err, + "invalid port in RELAY_CONNECTOR_HOST_PORT env variable", relayConnectorHostPort, + ) + return fmt.Errorf("relay server failed in setupserver") + } + utils.RelayConnectorHost = host + } + + utils.RelayIPFromConfig = relayListenIP + return nil +} + +func setupCDServer(log *relaylogger.RelayLog) error { + // Enable VIPER to read Environment Variables + viper.AutomaticEnv() + + viper.SetDefault(podNameEnv, "cdrelay-pod") + viper.SetDefault(podNamespace, "rafay-system") + viper.SetDefault(peerServiceURIEnv, "https://peering.sentry.rafay.local:10001") + viper.SetDefault(sentryAddrEnv, "localhost:10000") + + // for relays running outside of core + viper.SetDefault(bootstrapAddrEnv, "") + viper.SetDefault(relayPeeringTokenEnv, "-") + viper.SetDefault(cdRelayUserTokenEnv, "-") + viper.SetDefault(cdRelayConnectorTokenEnv, "-") + viper.SetDefault(auditPathEnv, "/tmp") + viper.SetDefault(metricListenEnv, ":8003") + + viper.BindEnv(podNameEnv) + viper.BindEnv(podNamespaceEnv) + viper.BindEnv(relayPeeringTokenEnv) + viper.BindEnv(sentryAddrEnv) + + // outside core configs + viper.BindEnv(bootstrapAddrEnv) + viper.BindEnv(cdRelayUserTokenEnv) + viper.BindEnv(cdRelayConnectorTokenEnv) + + // for testing + viper.BindEnv(relayListenIPEnv) + + podName = viper.GetString(podNameEnv) + podNamespace = viper.GetString(podNamespaceEnv) + sentryAddr = viper.GetString(sentryAddrEnv) + auditPath = viper.GetString(auditPathEnv) + + utils.PODNAME = podName + + relayPeeringURI = viper.GetString(peerServiceURIEnv) + + if sentryAddr == "" { + log.Error( + nil, + "missing sentry uri, please set one of the environment variable RAFAY_SENTRY[relay deployed inside core] (or) BOOTSTRAP_ADDR[outside core]", + ) + return fmt.Errorf("relay server failed in setupserver") + } + + if relayPeeringURI == "" { + log.Error( + nil, + "missing relay peer service uri, please set RAFAY_RELAY_PEERSERVICE environment variable", + ) + return fmt.Errorf("relay server failed in setupserver") + } + + relayPeeringToken = viper.GetString(relayPeeringTokenEnv) + cdRelayUserToken = viper.GetString(cdRelayUserTokenEnv) + cdRelayConnectorToken = viper.GetString(cdRelayConnectorTokenEnv) + relayListenIP = viper.GetString(relayListenIPEnv) // used for running multiple relays in local test hosts + metricListen = viper.GetString(metricListenEnv) + + log.Debug( + "relay server setup values", + "podName", podName, + "podNamespace", podNamespace, + "relayPeeringURI", relayPeeringURI, + "relayListenIP", relayListenIP, + ) + + utils.RelayIPFromConfig = relayListenIP + return nil +} + +// prepare config for outside relay boot strapping +func prepareConfigCSRForBootStrapOutSideCore(config *register.Config, CN string, log *relaylogger.RelayLog) error { + privKey, err := cryptoutil.GenerateECDSAPrivateKey() + if err != nil { + log.Error( + err, + "failed generate ecd private key with CN", CN, + ) + return err + } + + key, err := cryptoutil.EncodePrivateKey(privKey, cryptoutil.NoPassword) + if err != nil { + log.Error( + err, + "failed encode ecd private key with CN", CN, + ) + return err + } + + config.PrivateKey = key + + csr, err := cryptoutil.CreateCSR(pkix.Name{ + CommonName: CN, + Country: []string{"USA"}, + Organization: []string{"Rafay Systems Inc"}, + Province: []string{"California"}, + Locality: []string{"Sunnyvale"}, + OrganizationalUnit: []string{relayNetworkID}, + }, privKey) + if err != nil { + log.Error( + err, + "failed create CSR with CN", CN, + ) + return err + } + + config.CSR = csr + return nil +} + +// registerRelayPeerService will register with rafay-sentry-peering-client template token +// registration fetches client-certificate/root-ca to connect to sentry peer service +func registerRelayPeerService(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "peering-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "peering-" + podName, + Mode: "client", + } + + if bootstrapAddr != "" { + // outside core bootstrap + cfg.Addr = bootstrapAddr + if relayPeeringToken == "" { + return fmt.Errorf("empty peering token for bootstrap") + } + cfg.TemplateToken = relayPeeringToken + cfg.Scheme = "http" + // check port is 443 then set scheme as https + if utils.IsHTTPS(bootstrapAddr) { + cfg.Scheme = "https" + } + // this is a client certificate CN is same as ClientID + err := prepareConfigCSRForBootStrapOutSideCore(cfg, cfg.ClientID, log) + if err != nil { + return fmt.Errorf("failed in config csr for relay peering bootstrap") + } + } else { + cfg.TemplateToken = "template/-" + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-sentry-peering-client" + } + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register peering relay", + ) + return err + } + + log.Info( + "peer certificate:", + string(cfg.Certificate), + ) + + utils.PeerCertificate = cfg.Certificate + utils.PeerPrivateKey = cfg.PrivateKey + utils.PeerCACertificate = cfg.CACertificate + utils.PeerServiceURI = relayPeeringURI + + return nil +} + +// registerCDRelayPeerService will register with rafay-sentry-cd-peering-client template token +// registration fetches client-certificate/root-ca to connect to sentry peer service +func registerCDRelayPeerService(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "peering-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "peering-" + podName, + Mode: "client", + } + + if bootstrapAddr != "" { + // outside core bootstrap + cfg.Addr = bootstrapAddr + if relayPeeringToken == "" { + return fmt.Errorf("empty peering token for bootstrap") + } + cfg.TemplateToken = relayPeeringToken + cfg.Scheme = "http" + // check port is 443 then set scheme as https + if utils.IsHTTPS(bootstrapAddr) { + cfg.Scheme = "https" + } + // this is a client certificate CN is same as ClientID + err := prepareConfigCSRForBootStrapOutSideCore(cfg, cfg.ClientID, log) + if err != nil { + return fmt.Errorf("failed in config csr for relay peering bootstrap") + } + } else { + cfg.TemplateToken = "template/cd-relay" + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-sentry-cd-peering-client" + } + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register cd peering relay", + ) + return err + } + + log.Info( + "peer certificate:", + string(cfg.Certificate), + ) + + utils.PeerCertificate = cfg.Certificate + utils.PeerPrivateKey = cfg.PrivateKey + utils.PeerCACertificate = cfg.CACertificate + utils.PeerServiceURI = relayPeeringURI + + return nil +} + +// registerRelayUserServer will register with rafay-core-relay-user template token +// registration fetches client-certificate/root-ca to terminate user connections +// the same certificate will be used as the client cert for peer-upstreams +func registerRelayUserServer(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "user-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "user-" + podName, + Mode: "server", + } + + if bootstrapAddr != "" { + // outside core bootstrap + cfg.Addr = bootstrapAddr + if relayUserToken == "" { + return fmt.Errorf("empty user token for bootstrap") + } + cfg.TemplateToken = relayUserToken + cfg.Scheme = "http" + // check port is 443 then set scheme as https + if utils.IsHTTPS(bootstrapAddr) { + cfg.Scheme = "https" + } + // this is a server certificate CN is same as ServerHost + cfg.ServerHost = utils.RelayUserHost + cfg.ServerPort = utils.RelayUserPort + err := prepareConfigCSRForBootStrapOutSideCore(cfg, cfg.ServerHost, log) + if err != nil { + return fmt.Errorf("failed in config csr for relay user server bootstrap") + } + } else { + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-core-relay-user" + } + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register user relay", + ) + return err + } + + log.Info( + "user certificate:", + string(cfg.Certificate), + ) + + utils.RelayUserCert = cfg.Certificate + utils.RelayUserKey = cfg.PrivateKey + utils.RelayUserCACert = cfg.CACertificate + utils.RelayUserPort = cfg.ServerPort + utils.RelayUserHost = cfg.ServerHost + + return nil +} + +// registerCDRelayUserServer will register with rafay-core-cd-relay-user template token +// registration fetches client-certificate/root-ca to terminate user connections +// the same certificate will be used as the client cert for peer-upstreams +func registerCDRelayUserServer(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "cd-user-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "cd-user-" + podName, + Mode: "server", + } + + //inside core bootstrap + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-core-cd-relay-user" + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register user cd relay", + ) + return err + } + + log.Info( + "user certificate:", + string(cfg.Certificate), + ) + + utils.CDRelayUserCert = cfg.Certificate + utils.CDRelayUserKey = cfg.PrivateKey + utils.CDRelayUserCACert = cfg.CACertificate + utils.CDRelayUserPort = cfg.ServerPort + utils.CDRelayUserHost = cfg.ServerHost + + return nil +} + +// registerRelayConnectorServer will register with rafay-core-relay-connector template token +// registration fetches client-certificate/root-ca to terminate connector connections +// the same certificate will be used as the client cert for peer-upstreams +func registerRelayConnectorServer(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "relay-server-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "relay-server-" + podName, + Mode: "server", + } + + if bootstrapAddr != "" { + // outside core bootstrap + cfg.Addr = bootstrapAddr + if relayConnectorToken == "" { + return fmt.Errorf("empty user token for bootstrap") + } + cfg.TemplateToken = relayConnectorToken + cfg.Scheme = "http" + // check port is 443 then set scheme as https + if utils.IsHTTPS(bootstrapAddr) { + cfg.Scheme = "https" + } + // this is a server certificate CN is same as ServerHost + cfg.ServerHost = utils.RelayConnectorHost + cfg.ServerPort = utils.RelayConnectorPort + err := prepareConfigCSRForBootStrapOutSideCore(cfg, cfg.ServerHost, log) + if err != nil { + return fmt.Errorf("failed in config csr for relay connector server bootstrap") + } + } else { + //inside core bootstrap + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-core-relay-server" + } + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register connector relay", + ) + return err + } + + log.Info( + "connector certificate:", + string(cfg.Certificate), + ) + + utils.RelayConnectorCert = cfg.Certificate + utils.RelayConnectorKey = cfg.PrivateKey + utils.RelayConnectorCACert = cfg.CACertificate + utils.RelayConnectorHost = cfg.ServerHost + utils.RelayConnectorPort = cfg.ServerPort + + return nil +} + +// registerCDRelayConnectorServer will register with rafay-core-cd-relay-connector template token +// registration fetches client-certificate/root-ca to terminate connector connections +// the same certificate will be used as the client cert for peer-upstreams +func registerCDRelayConnectorServer(ctx context.Context, log *relaylogger.RelayLog) error { + cfg := ®ister.Config{ + ClientID: "cd-relay-server-" + podName, + ClientIP: utils.GetRelayIP(), + Name: "cd-relay-server-" + podName, + Mode: "server", + } + + //inside core bootstrap + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-core-cd-relay-server" + + if err := register.Register(ctx, cfg); err != nil { + log.Error( + err, + "failed to register connector cd relay", + ) + return err + } + + log.Info( + "connector certificate:", + string(cfg.Certificate), + ) + + utils.CDRelayConnectorCert = cfg.Certificate + utils.CDRelayConnectorKey = cfg.PrivateKey + utils.CDRelayConnectorCACert = cfg.CACertificate + utils.CDRelayConnectorHost = cfg.ServerHost + utils.CDRelayConnectorPort = cfg.ServerPort + + return nil +} + +func relayServerBootStrap(ctx context.Context, log *relaylogger.RelayLog) { + //peer service bootstrap + ticker := time.NewTicker(5 * time.Second) + for { + err := registerRelayPeerService(ctx, log) + if err != nil { + log.Error( + err, + "failed to register relay with peer-service-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() + + ticker = time.NewTicker(5 * time.Second) + // relay user server bootstrap + for { + err := registerRelayUserServer(ctx, log) + if err != nil { + log.Error( + err, + "failed to register relay with relay-user-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() + + ticker = time.NewTicker(5 * time.Second) + // relay connector server bootstrap + for { + err := registerRelayConnectorServer(ctx, log) + if err != nil { + log.Error( + err, + "failed to register relay with relay-connector-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() +} + +func cdRelayServerBootStrap(ctx context.Context, log *relaylogger.RelayLog) { + //peer service bootstrap + ticker := time.NewTicker(5 * time.Second) + for { + err := registerCDRelayPeerService(ctx, log) + if err != nil { + log.Error( + err, + "failed to register cd relay with peer-service-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() + + ticker = time.NewTicker(5 * time.Second) + // relay user server bootstrap + for { + err := registerCDRelayUserServer(ctx, log) + if err != nil { + log.Error( + err, + "failed to register relay with cd-relay-user-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() + + ticker = time.NewTicker(5 * time.Second) + // relay connector server bootstrap + for { + err := registerCDRelayConnectorServer(ctx, log) + if err != nil { + log.Error( + err, + "failed to register relay with cd-relay-connector-bootstrap service, will retry", + ) + select { + case <-ticker.C: + continue + case <-ctx.Done(): + ticker.Stop() + return + } + } + break + } + ticker.Stop() +} + +//RunRelayServer entry to the relay server +func RunRelayServer(ctx context.Context, logLvl int) { + rctx, cancel := context.WithCancel(ctx) + defer cancel() + + log = relaylogger.NewLogger(logLvl).WithName("Relay Server") + + if err := setupserver(log); err != nil { + //config is not available + log.Error( + err, + "relay server exiting", + ) + utils.ExitChan <- true + return + } + +restartServer: + relayServerBootStrap(rctx, log) + go tunnel.StartServer(rctx, log, fmt.Sprintf("%s/%s", auditPath, podName), utils.ExitChan) + go runMetricServer(rctx, log) + + for { + select { + case <-utils.ExitChan: + log.Info( + "Relay server stopped, restart in 2 secs", + ) + time.Sleep(2 * time.Second) + goto restartServer + case <-ctx.Done(): + log.Info( + "Relay server exiting", + ) + return + } + } +} + +//RunCDRelayServer entry to the relay server +func RunCDRelayServer(ctx context.Context, logLvl int) { + rctx, cancel := context.WithCancel(ctx) + defer cancel() + + log = relaylogger.NewLogger(logLvl).WithName("Relay Server") + + if err := setupCDServer(log); err != nil { + //config is not available + log.Error( + err, + "relay server exiting", + ) + utils.ExitChan <- true + return + } + +restartServer: + cdRelayServerBootStrap(rctx, log) + go tunnel.StartCDServer(rctx, log, fmt.Sprintf("%s/%s", auditPath, podName), utils.ExitChan) + go runMetricServer(rctx, log) + + for { + select { + case <-utils.ExitChan: + log.Info( + "Relay server stopped, restart in 2 secs", + ) + time.Sleep(2 * time.Second) + goto restartServer + case <-ctx.Done(): + log.Info( + "Relay server exiting", + ) + return + } + } +} + +func runMetricServer(ctx context.Context, log *relaylogger.RelayLog) { + + m := http.NewServeMux() + s := http.Server{Addr: metricListen, Handler: m} + m.HandleFunc("/dialins", dialinMetrics) + m.HandleFunc("/loglevel", setLoglevel) + m.HandleFunc("/health", healthCheck) + + go func() { + <-ctx.Done() + s.Shutdown(context.Background()) + }() + log.Info("Start Relay Metric Server at", metricListen) + err := s.ListenAndServe() + log.Info("Stopping Relay Metric Server at", metricListen, err) +} + +func dialinMetrics(w http.ResponseWriter, r *http.Request) { + tunnel.DialinMetric(w) +} + +func setLoglevel(w http.ResponseWriter, r *http.Request) { + keys, ok := r.URL.Query()["level"] + if !ok || len(keys[0]) < 1 { + fmt.Fprintf(w, "missing level query string") + return + } + + level := keys[0] + lvl, err := strconv.Atoi(level) + if err != nil { + fmt.Fprintf(w, "invalid level query string") + return + } + + relaylogger.SetRunTimeLogLevel(lvl) + + fmt.Fprintf(w, "Success: set loglevel to "+level) +} + +func healthCheck(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "up and feels good") +} diff --git a/components/relay/pkg/relaylogger/relaylogger.go b/components/relay/pkg/relaylogger/relaylogger.go new file mode 100644 index 0000000..5a82884 --- /dev/null +++ b/components/relay/pkg/relaylogger/relaylogger.go @@ -0,0 +1,145 @@ +package relaylogger + +import ( + "bytes" + "fmt" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/log" + "go.uber.org/zap" +) + +var ( + logtmp = log.GetLogger() + plain = logtmp.SugaredLogger.Desugar().WithOptions(zap.AddCallerSkip(2)) + rlog = plain.Sugar() + runLevel = 0 +) + +// RelayLog is a sample logr.Logger that logs to stderr. +// It's terribly inefficient, and is *only* a basic example. +type RelayLog struct { + level int + name string + keyValues map[string]interface{} +} + +func (l *RelayLog) relaylog(level int, msg string, kvs ...interface{}) { + var buf bytes.Buffer + + if level > runLevel { + return + } + + fmt.Fprintf(&buf, "%s::%s ", l.name, msg) + for k, v := range l.keyValues { + fmt.Fprintf(&buf, "%s: %+v ", k, v) + } + + if len(kvs)%2 == 0 { + for i := 0; i < len(kvs); i += 2 { + fmt.Fprintf(&buf, "%s: %+v ", kvs[i], kvs[i+1]) + + } + } else { + for i := 0; i < len(kvs); i++ { + fmt.Fprintf(&buf, "%+v ", kvs[i]) + } + } + + fmt.Fprintf(&buf, "\n") + switch level { + case 0: + rlog.Panicw(buf.String()) + case 1: + rlog.Errorw(buf.String()) + case 2: + rlog.Warnw(buf.String()) + case 3: + rlog.Infow(buf.String()) + case 4: + rlog.Debugw(buf.String()) + } + +} + +//Enabled log enabled +func (_ *RelayLog) Enabled() bool { + return true +} + +//Critical log critical +func (l *RelayLog) Critical(msg string, kvs ...interface{}) { + l.relaylog(0, msg, kvs...) +} + +//Error log error level +func (l *RelayLog) Error(err error, msg string, kvs ...interface{}) { + kvs = append(kvs, "error", err) + l.relaylog(1, msg, kvs...) +} + +//Warn log warning level +func (l *RelayLog) Warn(msg string, kvs ...interface{}) { + l.relaylog(2, msg, kvs...) +} + +//Info log info level +func (l *RelayLog) Info(msg string, kvs ...interface{}) { + l.relaylog(3, msg, kvs...) +} + +//Debug log debug level +func (l *RelayLog) Debug(msg string, kvs ...interface{}) { + l.relaylog(4, msg, kvs...) +} + +//V .. +func (l *RelayLog) V(_ int) *RelayLog { + return l +} + +//WithName set log name +func (l *RelayLog) WithName(name string) *RelayLog { + var objName string + if l.name == "" { + objName = name + } else { + objName = l.name + "." + name + } + + return &RelayLog{ + level: l.level, + name: objName, + keyValues: l.keyValues, + } +} + +//WithValues log key values +func (l *RelayLog) WithValues(kvs ...interface{}) *RelayLog { + newMap := make(map[string]interface{}, len(l.keyValues)+len(kvs)/2) + for k, v := range l.keyValues { + newMap[k] = v + } + for i := 0; i < len(kvs); i += 2 { + newMap[kvs[i].(string)] = kvs[i+1] + } + return &RelayLog{ + level: l.level, + name: l.name, + keyValues: newMap, + } +} + +//NewLogger create new logger +func NewLogger(level int) *RelayLog { + if runLevel < level { + runLevel = level + } + return &RelayLog{level: level} +} + +//SetRunTimeLogLevel set a run time log level +func SetRunTimeLogLevel(level int) { + fmt.Println("log level changed from ", runLevel, " to ", level) + runLevel = level +} diff --git a/components/relay/pkg/sessions/usersession.go b/components/relay/pkg/sessions/usersession.go new file mode 100644 index 0000000..cc6fc45 --- /dev/null +++ b/components/relay/pkg/sessions/usersession.go @@ -0,0 +1,161 @@ +package sessions + +import ( + "hash" + "net/http" + "sync" + "time" + + "github.com/dgraph-io/ristretto" + "github.com/julienschmidt/httprouter" + "github.com/twmb/murmur3" +) + +//UserSession session cache +type UserSession struct { + // Type of the server. Relay means user-facing + // Dialin means cluster-facing + Type string + + // ServerName of the server which accepted the connection + ServerName string + + // CertSNI derived from client certificate + CertSNI string + + // server block of this connection + //server *Server + + // DialinCachedKey already stiched to a dialin + DialinCachedKey string + + // ErrorFlag indicate the session got 401/502 + ErrorFlag bool + + // UserName used in login + UserName string + + // RoleName Role + RoleName string + + // IsReadrole read/write + IsReadrole bool + + // IsReadrole OrgAdmin + IsOrgAdmin bool + + // EnforceOrgAdminOnlySecret access + EnforceOrgAdminOnlySecret bool +} + +var ( + // userSessions cache + userSessions *ristretto.Cache + + // for URL matching + roleCheck *httprouter.Router + + // for URL matching + roleCheckSecret *httprouter.Router + + _hashPool = sync.Pool{ + New: func() interface{} { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return murmur3.New64() + }, + } +) + +var _dummyHandler = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {} + +//InitUserSessionCache init user cache +func InitUserSessionCache() error { + var err error + userSessions, err = ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 25, // Maximum cost of cache (32MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + + roleCheck = httprouter.New() + roleCheck.Handle("POST", "/api/:version/namespaces/:namespace/pods/:pod/exec", _dummyHandler) + roleCheck.Handle("POST", "/api/:version/namespaces/:namespace/secrets", _dummyHandler) + roleCheck.Handle("GET", "/api/:version/namespaces/:namespace/secrets", _dummyHandler) + roleCheck.Handle("GET", "/api/:version/secrets", _dummyHandler) + + roleCheckSecret = httprouter.New() + roleCheckSecret.Handle("POST", "/api/:version/namespaces/:namespace/secrets", _dummyHandler) + roleCheckSecret.Handle("GET", "/api/:version/namespaces/:namespace/secrets", _dummyHandler) + roleCheckSecret.Handle("GET", "/api/:version/secrets", _dummyHandler) + + return err +} + +// GetRoleCheck check the given method and URL matches +func GetRoleCheck(method, url string) bool { + h, _, _ := roleCheck.Lookup(method, url) + return h != nil +} + +// GetRoleCheck check the given method and URL matches +func GetSecretRoleCheck(method, url string) bool { + h, _, _ := roleCheckSecret.Lookup(method, url) + return h != nil +} + +//GetUserSession get the user session +func GetUserSession(skey string) (*UserSession, bool) { + hkey := getUserCacheKey(skey) + if val, ok := userSessions.Get(hkey); ok { + return val.(*UserSession), true + } + return nil, false +} + +//AddUserSession add user session +func AddUserSession(s *UserSession, skey string) { + hkey := getUserCacheKey(skey) + userSessions.SetWithTTL(hkey, s, 100, time.Minute*15) +} + +//DeleteUserSession add user session +func DeleteUserSession(skey string) { + hkey := getUserCacheKey(skey) + userSessions.Del(hkey) +} + +//UpdateUserSessionExpiry set a short TTL when error happens +func UpdateUserSessionExpiry(skey string, secs int) { + hkey := getUserCacheKey(skey) + if val, ok := userSessions.Get(hkey); ok { + s := val.(*UserSession) + if !s.ErrorFlag { + s.ErrorFlag = true + userSessions.Del(hkey) + userSessions.SetWithTTL(hkey, s, 100, time.Second*time.Duration(secs)) + } + } +} + +//SetSessionErrorFlag ser session errflg +func SetSessionErrorFlag(skey string) { + hkey := getUserCacheKey(skey) + if val, ok := userSessions.Get(hkey); ok { + s := val.(*UserSession) + if !s.ErrorFlag { + s.ErrorFlag = true + } + } +} + +//getUserCacheKey get cache key +func getUserCacheKey(skey string) (key uint64) { + hasher := _hashPool.Get().(hash.Hash64) + hasher.Reset() + hasher.Write([]byte(skey)) + key = hasher.Sum64() + _hashPool.Put(hasher) + return +} diff --git a/components/relay/pkg/tail/follower/follower.go b/components/relay/pkg/tail/follower/follower.go new file mode 100644 index 0000000..a03f033 --- /dev/null +++ b/components/relay/pkg/tail/follower/follower.go @@ -0,0 +1,322 @@ +package follower + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "sync" + "time" + + "github.com/fsnotify/fsnotify" +) + +const ( + bufSize = 4 * 1024 + peekSize = 1024 +) + +var ( + _ = fmt.Print +) + +type Line struct { + bytes []byte + discarded int +} + +func (l *Line) Bytes() []byte { + return l.bytes +} + +func (l *Line) String() string { + return string(l.bytes) +} + +func (l *Line) Discarded() int { + return l.discarded +} + +type Config struct { + Offset int64 + Whence int + Reopen bool +} + +type Follower struct { + once sync.Once + file *os.File + filename string + lines chan Line + err error + config Config + reader *bufio.Reader + watcher *fsnotify.Watcher + offset int64 + closeCh chan struct{} +} + +func New(filename string, config Config) (*Follower, error) { + t := &Follower{ + filename: filename, + lines: make(chan Line), + config: config, + closeCh: make(chan struct{}), + } + + err := t.reopen() + if err != nil { + return nil, err + } + + go t.once.Do(t.run) + + return t, nil +} + +func (t *Follower) Lines() chan Line { + return t.lines +} + +func (t *Follower) Err() error { + return t.err +} + +func (t *Follower) Close() { + t.closeCh <- struct{}{} +} + +func (t *Follower) run() { + t.close(t.follow()) +} + +func (t *Follower) follow() error { + _, err := t.file.Seek(t.config.Offset, t.config.Whence) + if err != nil { + return err + } + + var ( + eventChan = make(chan fsnotify.Event) + errChan = make(chan error, 1) + ) + + t.watcher, err = fsnotify.NewWatcher() + if err != nil { + return err + } + + defer t.watcher.Close() + go t.watchFileEvents(eventChan, errChan) + + t.watcher.Add(t.filename) + + for { + for { + // discard leading NUL bytes + var discarded int + + for { + b, _ := t.reader.Peek(peekSize) + i := bytes.LastIndexByte(b, '\x00') + + if i > 0 { + n, _ := t.reader.Discard(i + 1) + discarded += n + } + + if i+1 < peekSize { + break + } + } + + s, err := t.reader.ReadBytes('\n') + if err != nil && err != io.EOF { + return err + } + + // if we encounter EOF before a line delimiter, + // ReadBytes() will return the remaining bytes, + // so push them back onto the buffer, rewind + // our seek position, and wait for further file changes. + // we also have to save our dangling byte count in the event + // that we want to re-open the file and seek to the end + if err == io.EOF { + l := len(s) + + t.offset, err = t.file.Seek(-int64(l), io.SeekCurrent) + if err != nil { + return err + } + + t.reader.Reset(t.file) + break + } + + t.sendLine(s, discarded) + } + + // we're now at EOF, so wait for changes + select { + case evt := <-eventChan: + switch evt.Op { + + // as soon as something is written, go back and read until EOF. + case fsnotify.Chmod: + fallthrough + + case fsnotify.Write: + fi, err := t.file.Stat() + if err != nil { + if !os.IsNotExist(err) { + return err + } + + // it's possible that an unlink can cause fsnotify.Chmod, + // so attempt to rewatch if the file is missing + if err := t.rewatch(); err != nil { + return err + } + + continue + } + + // file was truncated, seek to the beginning + if t.offset > fi.Size() { + t.offset, err = t.file.Seek(0, io.SeekStart) + if err != nil { + return err + } + + t.reader.Reset(t.file) + } + + continue + + // if a file is removed or renamed + // and re-opening is desired, see if it appears + // again within a 1 second deadline. this should be enough time + // to see the file again for log rotation programs with this behavior + default: + if !t.config.Reopen { + return nil + } + + if err := t.rewatch(); err != nil { + return err + } + + continue + } + + // any errors that come from fsnotify + case err := <-errChan: + return err + + // a request to stop + case <-t.closeCh: + t.watcher.Remove(t.filename) + return nil + + // fall back to 10 second polling if we haven't received any fsevents + // stat the file, if it's still there, just continue and try to read bytes + // if not, go through our re-opening routine + case <-time.After(10 * time.Second): + fi1, err := t.file.Stat() + if err != nil && !os.IsNotExist(err) { + return err + } + + fi2, err := os.Stat(t.filename) + if err != nil && !os.IsNotExist(err) { + return err + } + + if os.SameFile(fi1, fi2) { + continue + } + + if err := t.rewatch(); err != nil { + return err + } + + continue + } + } + +} + +func (t *Follower) rewatch() error { + t.watcher.Remove(t.filename) + if err := t.reopen(); err != nil { + return err + } + + t.watcher.Add(t.filename) + return nil +} + +func (t *Follower) reopen() error { + if t.file != nil { + t.file.Close() + t.file = nil + } + + file, err := os.Open(t.filename) + if err != nil { + return err + } + + t.file = file + t.reader = bufio.NewReaderSize(t.file, bufSize) + + return nil +} + +func (t *Follower) close(err error) { + t.err = err + + if t.file != nil { + t.file.Close() + } + + close(t.lines) +} + +func (t *Follower) sendLine(l []byte, d int) { + t.lines <- Line{l[:len(l)-1], d} +} + +func (t *Follower) watchFileEvents(eventChan chan fsnotify.Event, errChan chan error) { + for { + select { + case evt, ok := <-t.watcher.Events: + if !ok { + return + } + + // debounce write events, but send all others + switch evt.Op { + case fsnotify.Write: + select { + case eventChan <- evt: + default: + } + + default: + select { + case eventChan <- evt: + case err := <-t.watcher.Errors: + errChan <- err + return + } + } + + // die on a file watching error + case err := <-t.watcher.Errors: + errChan <- err + return + } + } +} diff --git a/components/relay/pkg/tail/register.go b/components/relay/pkg/tail/register.go new file mode 100644 index 0000000..8fbd4a6 --- /dev/null +++ b/components/relay/pkg/tail/register.go @@ -0,0 +1,109 @@ +package tail + +import ( + "context" + "crypto/x509/pkix" + "fmt" + "net/url" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/cryptoutil" + "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/register" + sentryrpc "github.com/RafaySystems/rcloud-base/components/common/proto/rpc/sentry" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" +) + +// prepare config for outside relay boot strapping +func prepareConfigCSRForBootStrapOutSideCore(config *register.Config, CN string) error { + privKey, err := cryptoutil.GenerateECDSAPrivateKey() + if err != nil { + _log.Infow("failed generate private key", "CN", CN) + return err + } + + key, err := cryptoutil.EncodePrivateKey(privKey, cryptoutil.NoPassword) + if err != nil { + _log.Infow("failed encode ecd private key", "cn", CN, "error", err) + return err + } + + config.PrivateKey = key + + csr, err := cryptoutil.CreateCSR(pkix.Name{ + CommonName: CN, + Country: []string{"USA"}, + Organization: []string{"Rafay Systems Inc"}, + OrganizationalUnit: []string{config.Name}, + Province: []string{"California"}, + Locality: []string{"Sunnyvale"}, + }, privKey) + if err != nil { + _log.Infow("failed to create CSR", "CN", CN, "error", err) + return err + } + + config.CSR = csr + return nil +} + +// registerRelayPeerService will register with rafay-sentry-peering-client template token +// registration fetches client-certificate/root-ca to connect to sentry peer service +func registerRelayPeerService(ctx context.Context) (*register.Config, error) { + cfg := ®ister.Config{ + ClientID: "tail-" + podName, + Name: "tail-" + podName, + Mode: "client", + } + + if bootstrapAddr != "" { + // outside core bootstrap + cfg.Addr = bootstrapAddr + if relayPeeringToken == "" { + return nil, fmt.Errorf("empty peering token for bootstrap") + } + cfg.TemplateToken = relayPeeringToken + cfg.Scheme = "http" + // check port is 443 then set scheme as https + if utils.IsHTTPS(bootstrapAddr) { + cfg.Scheme = "https" + } + // this is a client certificate CN is same as ClientID + err := prepareConfigCSRForBootStrapOutSideCore(cfg, cfg.ClientID) + if err != nil { + return nil, fmt.Errorf("failed in config csr for relay peering bootstrap") + } + } else { + cfg.TemplateToken = "template/-" + cfg.Addr = sentryAddr + cfg.Scheme = "grpc" + cfg.TemplateName = "rafay-sentry-peering-client" + } + + if err := register.Register(ctx, cfg); err != nil { + _log.Infow("unable to register", "error", err) + return nil, err + } + + _log.Infow("successfully registered", "cert", string(cfg.Certificate)) + + return cfg, nil +} + +func newSentryAuthorizationPool(ctx context.Context) (sentryrpc.SentryAuthorizationPool, error) { + + cfg, err := registerRelayPeerService(ctx) + if err != nil { + return nil, err + } + + url, err := url.Parse(relayPeeringURI) + if err != nil { + return nil, err + } + + return sentryrpc.NewSentryAuthorizationPool( + sentryrpc.WithAddr(url.Host), + sentryrpc.WithClientCertPEM(cfg.Certificate), + sentryrpc.WithClientKeyPEM(cfg.PrivateKey), + sentryrpc.WithCaCertPEM(cfg.CACertificate), + ) +} diff --git a/components/relay/pkg/tail/run.go b/components/relay/pkg/tail/run.go new file mode 100644 index 0000000..aefc4f7 --- /dev/null +++ b/components/relay/pkg/tail/run.go @@ -0,0 +1,213 @@ +package tail + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + "github.com/segmentio/encoding/json" + "github.com/spf13/viper" +) + +const ( + auditPathEnv = "AUDIT_PATH" + podNameEnv = "POD_NAME" + podNamespaceEnv = "POD_NAMESPACE" + peerServiceURIEnv = "RAFAY_RELAY_PEERSERVICE" + sentryAddrEnv = "SENTRY_ADDR" // used by relay running inside core + bootstrapAddrEnv = "BOOTSTRAP_ADDR" + relayPeeringTokenEnv = "RELAY_PEERING_TOKEN" +) + +var ( + podName string + podNamespace string + relayPeeringURI string + sentryAddr string // used by relay running inside core + auditPath string + + // for relays running outside of core + relayPeeringToken string + bootstrapAddr string +) + +func setupTail() error { + // Enable VIPER to read Environment Variables + viper.AutomaticEnv() + + viper.SetDefault(podNameEnv, "relay-pod") + viper.SetDefault(podNamespace, "rafay-system") + viper.SetDefault(peerServiceURIEnv, "https://peering.sentry.rafay.local:10001") + viper.SetDefault(sentryAddrEnv, "localhost:10000") + + // for relays running outside of core + viper.SetDefault(bootstrapAddrEnv, "") + viper.SetDefault(relayPeeringTokenEnv, "-") + viper.SetDefault(auditPathEnv, "/tmp") + + viper.BindEnv(podNameEnv) + viper.BindEnv(podNamespaceEnv) + viper.BindEnv(relayPeeringTokenEnv) + viper.BindEnv(sentryAddrEnv) + + // outside core configs + viper.BindEnv(bootstrapAddrEnv) + + podName = viper.GetString(podNameEnv) + podNamespace = viper.GetString(podNamespaceEnv) + sentryAddr = viper.GetString(sentryAddrEnv) + auditPath = viper.GetString(auditPathEnv) + + relayPeeringURI = viper.GetString(peerServiceURIEnv) + + // outside core relay configurations + bootstrapAddr = viper.GetString(bootstrapAddrEnv) + + if sentryAddr == "" && bootstrapAddr == "" { + _log.Infow("missing sentry & bootstrap uri, please set one of the environment variable RAFAY_SENTRY[relay deployed inside core] (or) BOOTSTRAP_ADDR[outside core]") + return fmt.Errorf("relay server failed in setupserver") + } + + if bootstrapAddr != "" { + // outside config found, unset inside boostrap endpoint. + // both cannot be set, only one mode is possible + sentryAddr = "" + } + + if relayPeeringURI == "" { + _log.Infow("missing relay peer service uri, please set RAFAY_RELAY_PEERSERVICE environment variable") + return fmt.Errorf("relay server failed in setupserver") + } + + relayPeeringToken = viper.GetString(relayPeeringTokenEnv) + + _log.Infow( + "relay server setup values", + "podName", podName, + "podNamespace", podNamespace, + "relayPeeringURI", relayPeeringURI, + "auditPath", auditPath, + ) + + if sentryAddr != "" { + _log.Infow("relay inside core", "sentryAddr", sentryAddr) + } else { + _log.Infow("outside core relay", "bootStrapURI", bootstrapAddr, + "relayPeeringToken", relayPeeringToken, + ) + if relayPeeringToken == "-" { + _log.Infow("missing env variable", "RELAY_PEERING_TOKEN", relayPeeringToken) + return fmt.Errorf("relay server failed in setupserver") + } + + } + return nil +} + +func runTail(ctx context.Context) { + sap, err := newSentryAuthorizationPool(ctx) + if err != nil { + _log.Panicw("unable to create sentry authorization pool", "error", err) + } + + //_log.Infow("created sentry authorization pool") + + transformer, err := NewTransformer(sap) + if err != nil { + _log.Panicw("unable to create transformer", "error", err) + } + + //_log.Infow("created audit transformer") + + logChan := make(chan LogMsg) + + go func() { + ticker := time.NewTicker(time.Second * 30) + defer ticker.Stop() + + watchedDir := make(map[string]context.CancelFunc) + var m sync.Mutex + + // find new dirs in audit path + for ; true; <-ticker.C { + //_log.Infow("ticker started") + // check if context is done + select { + case <-ctx.Done(): + _log.Infow("context done") + for _, c := range watchedDir { + c() + } + break + default: + } + + staleDirs, err := findStaleDirs(auditPath) + if err != nil { + _log.Infow("unable to find state directories", "error", err) + continue + } + + remStaleLoop: + for _, staleDir := range staleDirs { + err := os.RemoveAll(staleDir) + if err != nil { + _log.Infow("unable to remove stale dir", "error", err) + continue remStaleLoop + } + } + + dirs, err := findLogDirs(auditPath) + if err != nil { + _log.Infow("unable to find log dirs", "error", err) + continue + } + + //_log.Infow("found log dirs", "dirs", dirs) + + for _, dir := range dirs { + m.Lock() + + if _, ok := watchedDir[dir]; !ok { + wCtx, cancel := context.WithCancel(ctx) + watchedDir[dir] = cancel + + go tailDir(wCtx, dir, logChan) + } + m.Unlock() + } + } + + }() + +transformLoop: + for { + select { + case lm := <-logChan: + var am AuditMsg + err := transformer.Transform(&lm, &am) + if err != nil { + _log.Infow("unable to transform message", "error", err) + continue + } + + err = json.NewEncoder(os.Stdout).Encode(&am) + if err != nil { + _log.Infow("unable to encode audit msg", "error", err) + continue + } + + case <-ctx.Done(): + break transformLoop + } + } + +} + +// RunRelayTail runs relay tail +func RunRelayTail(ctx context.Context) { + setupTail() + runTail(ctx) +} diff --git a/components/relay/pkg/tail/tail.go b/components/relay/pkg/tail/tail.go new file mode 100644 index 0000000..3266ddb --- /dev/null +++ b/components/relay/pkg/tail/tail.go @@ -0,0 +1,241 @@ +package tail + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/time/rate" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/log" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/tail/follower" +) + +const ( + filterName = "lines.cuckoo" +) + +var ( + _log = log.GetLogger() +) + +func isStaleDir(path string) bool { + autitLogPath := fmt.Sprintf("%s/audit.log", path) + info, err := os.Stat(autitLogPath) + if err != nil { + _log.Infow("unable to stat file", "path", autitLogPath, "error", err) + return false + } + + // delete directory that has no modified files in last 7 days. + if info.ModTime().Before(time.Now().Add(-time.Hour * 24 * 7)) { + _log.Infow("audit log not updated for 7 days", "path", autitLogPath) + return true + } + + return false +} + +func hasLogs(path string) bool { + + matches, _ := filepath.Glob(fmt.Sprintf("%s/audit*.log", path)) + + return len(matches) > 0 +} + +func findStaleDirs(root string) ([]string, error) { + if !strings.HasSuffix(root, "/") { + root = root + "/" + } + + var staleDirs []string + var nonStaleDirs []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + _log.Infow("prevent panic by handling failure in accessing the path", path, "error", err) + return err + } + if info.IsDir() && hasLogs(path) { + if isStaleDir(path) { + staleDirs = append(staleDirs, path) + } else { + nonStaleDirs = append(nonStaleDirs, path) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + if len(nonStaleDirs) <= 0 { + // looks like all are stale directories, there are no logs for + // 7 days hence can't identify which one to delete. + // skip stale directory cleanup for now. + return nil, fmt.Errorf("can not identify stale vs non-stale directory") + } + + return staleDirs, nil +} + +func findLogDirs(root string) ([]string, error) { + + if !strings.HasSuffix(root, "/") { + root = root + "/" + } + + var auditDirs []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + _log.Infow("prevent panic by handling failure in accessing the path", path, "error", err) + return err + } + if info.IsDir() && hasLogs(path) && !isStaleDir(path) { + auditDirs = append(auditDirs, path) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return auditDirs, nil +} + +func tailRotatedFile(ctx context.Context, filePath string, tailChan chan<- LogMsg) error { + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + limiter := rate.NewLimiter(rate.Every(time.Minute/200), 200) + + scanner := bufio.NewScanner(f) + + for scanner.Scan() { + + err := limiter.Wait(ctx) + if err != nil { + return err + } + + var lm LogMsg + err = json.Unmarshal(scanner.Bytes(), &lm) + if err != nil { + continue + } + + tailChan <- lm + } + + if err := scanner.Err(); err != nil { + return err + } + + // remove the file after done reading + os.Remove(filePath) + + return nil +} + +func tailCurrentFile(ctx context.Context, filePath string, tailChan chan<- LogMsg) error { + + t, err := follower.New(filePath, follower.Config{ + Whence: io.SeekStart, + Offset: 0, + Reopen: true, + }) + + if err != nil { + return err + } + + limiter := rate.NewLimiter(rate.Every(time.Minute/200), 200) + + for line := range t.Lines() { + + err := limiter.Wait(ctx) + if err != nil { + return err + } + + var lm LogMsg + err = json.Unmarshal(line.Bytes(), &lm) + if err != nil { + continue + } + + tailChan <- lm + + } + + if t.Err() != nil { + return err + } + + return nil +} + +// tailDir tails audit log directory for a relay pod +func tailDir(ctx context.Context, path string, logChan chan<- LogMsg) error { + + // filter, err := getFilter(path) + // if err != nil { + // return err + // } + + // defer saveFilter(path, filter) + + files := func() []string { + matches, _ := filepath.Glob(fmt.Sprintf("%s/audit*.log", path)) + + sort.Strings(matches) + return matches + }() + + //_log.Infow("found files", "files", files) + + tailChan := make(chan LogMsg) + for _, file := range files { + if strings.HasSuffix(file, "audit.log") { + go tailCurrentFile(ctx, file, tailChan) + } else { + go tailRotatedFile(ctx, file, tailChan) + } + } + +tailLoop: + for { + select { + case <-ctx.Done(): + break tailLoop + case log := <-tailChan: + + // // if the log xid is seen already ignore + // if filter.Lookup(lm.XID.Bytes()) { + // filter.Delete(lm.XID.Bytes()) + // continue + // } + + // filter.InsertUnique(lm.XID.Bytes()) + logChan <- log + + } + } + + return nil +} diff --git a/components/relay/pkg/tail/transform.go b/components/relay/pkg/tail/transform.go new file mode 100644 index 0000000..85db113 --- /dev/null +++ b/components/relay/pkg/tail/transform.go @@ -0,0 +1,217 @@ +package tail + +import ( + "bytes" + "context" + "fmt" + "hash" + "net/http" + "strings" + "sync" + "time" + + "github.com/dgraph-io/ristretto" + "github.com/jinzhu/inflection" + "github.com/twmb/murmur3" + + sentryrpc "github.com/RafaySystems/rcloud-base/components/common/proto/rpc/sentry" + "github.com/julienschmidt/httprouter" +) + +var _dummyHandler = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {} + +var _bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var _hashPool = sync.Pool{ + New: func() interface{} { + return murmur3.New64() + }, +} + +func getCacheKey(s string) uint64 { + hasher := _hashPool.Get().(hash.Hash64) + defer _hashPool.Put(hasher) + + buf := _bufPool.Get().(*bytes.Buffer) + defer _bufPool.Put(buf) + + buf.Reset() + buf.WriteString(s) + + hasher.Reset() + hasher.Write(buf.Bytes()) + return hasher.Sum64() +} + +// Transformer is the interface for transforming relay log message into audit message +type Transformer interface { + Transform(lm *LogMsg, am *AuditMsg) error +} + +// NewTransformer returns new relay log transformer +func NewTransformer(authzPool sentryrpc.SentryAuthorizationPool) (Transformer, error) { + + r := httprouter.New() + + r.Handle("GET", "/api", _dummyHandler) + r.Handle("GET", "/api/:version", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace/:kind1", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace/:kind1/:name", _dummyHandler) + r.Handle("GET", "/apis", _dummyHandler) + r.Handle("GET", "/apis/:group/:version", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace/:kind1", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace/:kind1/:name", _dummyHandler) + + cache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 27, // Maximum cost of cache (128MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + if err != nil { + return nil, err + } + + t := &transformer{ + authzPool: authzPool, + r: r, + cache: cache, + } + return t, nil +} + +type transformer struct { + authzPool sentryrpc.SentryAuthorizationPool + cache *ristretto.Cache + r *httprouter.Router +} + +func (t *transformer) getUser(cn string) (*sentryrpc.LookupUserResponse, error) { + key := getCacheKey(cn) + if val, ok := t.cache.Get(key); ok { + if usr, ok := val.(*sentryrpc.LookupUserResponse); ok { + return usr, nil + } + } + + // cache miss + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + authzClient, err := t.authzPool.NewClient(ctx) + if err != nil { + return nil, err + } + defer authzClient.Close() + + resp, err := authzClient.LookupUser(ctx, &sentryrpc.LookupUserRequest{ + UserCN: cn, + }) + + if err != nil { + return nil, err + } + + t.cache.Set(key, resp, 100) + return resp, nil +} + +func (t *transformer) getCluster(serverName string) (*sentryrpc.LookupClusterResponse, error) { + idx := strings.Index(serverName, ".") + + if idx > 0 { + key := getCacheKey(serverName[0:idx]) + if val, ok := t.cache.Get(key); ok { + if cluster, ok := val.(*sentryrpc.LookupClusterResponse); ok { + return cluster, nil + } + } + + // cache miss + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + authzClient, err := t.authzPool.NewClient(ctx) + if err != nil { + return nil, err + } + defer authzClient.Close() + + resp, err := authzClient.LookupCluster(ctx, &sentryrpc.LookupClusterRequest{ + ClusterSNI: serverName, + }) + + if err != nil { + return nil, err + } + + t.cache.Set(key, resp, 110) + return resp, nil + } + + return nil, fmt.Errorf("invalid serverName %s", serverName) +} + +func (t *transformer) getParams(url string) httprouter.Params { + + if h, params, _ := t.r.Lookup("GET", url); h != nil { + + return params + } + + return nil +} + +func (t *transformer) Transform(lm *LogMsg, am *AuditMsg) error { + + //_log.Infow("transforming", "logMessage", *lm) + cluster, err := t.getCluster(lm.ServerName) + if err != nil { + _log.Infow("unable to lookup cluster", "error", err) + return err + } + + user, err := t.getUser(lm.User) + if err != nil { + _log.Infow("unable to lookup user", "error", err) + return err + } + + params := t.getParams(lm.URL) + + am.ClusterName = cluster.Name + am.OrganizationID = user.OrganizationID + am.PartnerID = user.PartnerID + am.UserName = user.UserName + am.Duration = lm.Duration + am.Written = lm.Written + am.Timestamp = lm.Timestamp + am.Method = lm.Method + am.URL = lm.URL + am.Query = lm.Query + am.XID = lm.XID + am.StatusCode = lm.StatusCode + am.RemoteAddr = lm.RemoteAddr + am.SessionType = user.SessionType + + if params != nil { + am.APIVersion = fmt.Sprintf("%s/%s", params.ByName("group"), params.ByName("version")) + if params.ByName("kind1") != "" { + am.Kind = params.ByName("kind1") + } else { + am.Kind = params.ByName("kind0") + } + am.Kind = inflection.Singular(am.Kind) + am.Namespace = params.ByName("namespace") + am.Name = params.ByName("name") + } + + return nil +} diff --git a/components/relay/pkg/tail/types.go b/components/relay/pkg/tail/types.go new file mode 100644 index 0000000..6dff2d2 --- /dev/null +++ b/components/relay/pkg/tail/types.go @@ -0,0 +1,80 @@ +package tail + +import ( + "github.com/rs/xid" +) + +// { +// "level": "info", +// "ts": 1591154299.638035, +// "msg": "audit", +// "xid": "brbhcuug10lf2qfjj1hg", +// "method": "GET", +// "url": "/apis/metrics.k8s.io/v1beta1?timeout=32s", +// "serverName": "x3mxvkr.user.relay.rafay.local", +// "user": "rafay-core-debug", +// "remoteAddr": "127.0.0.1:63513", +// "statusCode": 503, +// "written": 20, +// "duration": 0.002513407 +// } + +// LogMsg represents log message +type LogMsg struct { + //Level string `json:"level"` + //Message string `json:"msg"` + Timestamp string `json:"ts"` + XID xid.ID `json:"xid"` + Method string `json:"method"` + URL string `json:"url"` + Query string `json:"query"` + ServerName string `json:"serverName"` + User string `json:"user"` + RemoteAddr string `json:"remoteAddr"` + StatusCode int `json:"statusCode"` + Written int64 `json:"written"` + Duration float64 `json:"duration"` +} + +// AuditMsg represents audit message +type AuditMsg struct { + Timestamp string `json:"ts"` + XID xid.ID `json:"id"` + StatusCode int `json:"sc"` + UserName string `json:"un"` + OrganizationID string `json:"o"` + PartnerID string `json:"p"` + RemoteAddr string `json:"ra"` + Duration float64 `json:"d"` + ClusterName string `json:"cn"` + APIVersion string `json:"av"` + Kind string `json:"k"` + Namespace string `json:"ns"` + Name string `json:"n"` + Method string `json:"m"` + URL string `json:"url"` + Query string `json:"q"` + Written int64 `json:"w"` + SessionType string `json:"st"` +} + +// Reset resets audit message +func (m *AuditMsg) Reset() { + m.Timestamp = "" + m.XID = xid.NilID() + m.StatusCode = 0 + m.UserName = "" + m.OrganizationID = "" + m.PartnerID = "" + m.RemoteAddr = "" + m.Duration = 0 + m.ClusterName = "" + m.APIVersion = "" + m.Kind = "" + m.Namespace = "" + m.Name = "" + m.Method = "" + m.URL = "" + m.Query = "" + m.Written = 0 +} diff --git a/components/relay/pkg/tail/url_test.go b/components/relay/pkg/tail/url_test.go new file mode 100644 index 0000000..c3a1602 --- /dev/null +++ b/components/relay/pkg/tail/url_test.go @@ -0,0 +1,76 @@ +package tail + +import ( + "testing" + + "github.com/julienschmidt/httprouter" +) + +// CASE 1 /api +// CASE 2 /apis +// CASE 3 /apis/storage.k8s.io/v1beta1 +// CASE 4 /api/v1/namespaces +// CASE 5 /api/v1/pods +// CASE 6 /api/v1/namespaces/rafay-system +// CASE 7 /api/v1/namespaces/rafay-system/configmaps/relay-agent-config +// CASE 8 /apis/k3s.cattle.io/v1/namespaces/kube-system/addons + +func TestURL(t *testing.T) { + r := httprouter.New() + + r.Handle("GET", "/api", _dummyHandler) + r.Handle("GET", "/api/:version", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace/:kind1", _dummyHandler) + r.Handle("GET", "/api/:version/:kind0/:namespace/:kind1/:name", _dummyHandler) + r.Handle("GET", "/apis", _dummyHandler) + r.Handle("GET", "/apis/:group/:version", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace/:kind1", _dummyHandler) + r.Handle("GET", "/apis/:group/:version/:kind0/:namespace/:kind1/:name", _dummyHandler) + + if h, params, _ := r.Lookup("GET", "/api/v1/pods"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + + if h, params, _ := r.Lookup("GET", "/apis/storage.k8s.io/v1beta1"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + + if h, params, _ := r.Lookup("GET", "/api/v1/namespaces/rafay-system/configmaps/relay-agent-config"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + + if h, params, _ := r.Lookup("GET", "/apis/k3s.cattle.io/v1/namespaces/kube-system/addons"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + + if h, params, _ := r.Lookup("GET", "/apis/metrics.k8s.io/v1beta1"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + + if h, params, _ := r.Lookup("GET", "/api/v1/namespaces/test-ns"); h != nil { + t.Log(params) + } else { + t.Error("expected match") + return + } + +} diff --git a/components/relay/pkg/tunnel/authz.go b/components/relay/pkg/tunnel/authz.go new file mode 100644 index 0000000..95b9640 --- /dev/null +++ b/components/relay/pkg/tunnel/authz.go @@ -0,0 +1,397 @@ +package tunnel + +import ( + "context" + "fmt" + "hash" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/log" + + "github.com/RafaySystems/rcloud-base/components/common/pkg/controller/apply" + "github.com/RafaySystems/rcloud-base/components/common/pkg/grpc" + sentryrpc "github.com/RafaySystems/rcloud-base/components/common/proto/rpc/sentry" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/proxy" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/dgraph-io/ristretto" + "github.com/twmb/murmur3" + "google.golang.org/grpc/credentials" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/transport" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +var ( + _log = log.GetLogger() +) + +const ( + bypassNamespace = "rafay-system" +) + +var bypassUserNames = []string{"system-sa", "default"} + +func getClusterID(sni string) (string, error) { + idx := strings.Index(sni, ".") + if idx < 0 { + return "", fmt.Errorf("invalid user sni format %s", sni) + } + return sni[0:idx], nil +} + +type bypassWrapper struct { + sni, key, bypassUserName string + rt http.RoundTripper +} + +func (w *bypassWrapper) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Set("X-Rafay-User", w.bypassUserName) + r.Header.Set("X-Rafay-Key", w.key) + r.Header.Set("X-Rafay-Namespace", bypassNamespace) + + if !strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path = r.URL.Path + "/" + } + + return w.rt.RoundTrip(r) +} + +func newBypassWrapper(sni, key, bypassUserName string) transport.WrapperFunc { + return func(rt http.RoundTripper) http.RoundTripper { + return &bypassWrapper{sni: sni, key: key, rt: rt, bypassUserName: bypassUserName} + } +} + +func getKubeRESTConfig(socketPath, sni, dialinLookupKey, username string) *rest.Config { + + cfg := &rest.Config{ + Host: fmt.Sprintf("%s_%s", sni, dialinLookupKey), + // Set transport expicitly to prevent client for caching transport + Transport: &http.Transport{ + DialContext: proxy.UnixDialContext(socketPath, dialinLookupKey, username, sni), + }, + Timeout: time.Second * 5, + } + + cfg.Wrap(newBypassWrapper(sni, dialinLookupKey, username)) + + return cfg + +} + +var _hashPool = sync.Pool{ + New: func() interface{} { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return murmur3.New64() + }, +} + +type authzProvisioner struct { + clientCache *ristretto.Cache + authzCache *ristretto.Cache +} + +func newAuthzProvisioner() (*authzProvisioner, error) { + clientCache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 27, // Maximum cost of cache (256MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + if err != nil { + return nil, err + } + + authzCache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e4, // Num keys to track frequency of (10k). + MaxCost: 1 << 25, // Maximum cost of cache (32MB). + BufferItems: 64, // Number of keys per Get buffer. + }) + if err != nil { + return nil, err + } + + return &authzProvisioner{clientCache: clientCache, authzCache: authzCache}, nil +} + +func (p *authzProvisioner) getClient(socketPath, sni, dialinLookupKey string) (client.Client, error) { + + key := func() uint64 { + hasher := _hashPool.Get().(hash.Hash64) + hasher.Reset() + hasher.Write([]byte(sni)) + hasher.Write([]byte(dialinLookupKey)) + k := hasher.Sum64() + _hashPool.Put(hasher) + return k + }() + + if i, ok := p.clientCache.Get(key); ok { + if c, ok := i.(client.Client); ok { + return c, nil + } + } + + getK8sClient := func(bypassUserName string) (client.Client, error) { + cfg := getKubeRESTConfig(socketPath, sni, dialinLookupKey, bypassUserName) + mapper, err := apiutil.NewDynamicRESTMapper(cfg) + if err != nil { + return nil, err + } + + c, err := client.New(cfg, client.Options{Scheme: scheme.Scheme, Mapper: mapper}) + return c, err + } + + var c client.Client + var err error + for _, username := range bypassUserNames { + c, err = getK8sClient(username) + if err != nil { + _log.Infow("error getting k8s client for username", "username", username) + } else { + break + } + } + if err != nil { + return nil, err + } + p.clientCache.SetWithTTL(key, c, 100, time.Minute*30) + return c, nil +} + +func (p *authzProvisioner) getAuthz(ctx context.Context, userCN, sni string, certIssue int64) (*sentryrpc.GetUserAuthorizationResponse, error) { + clusterID, err := getClusterID(sni) + if err != nil { + _log.Infow("unable to get clusterID", "error", err) + return nil, err + } + + u, err := url.Parse(utils.PeerServiceURI) + if err != nil { + _log.Infow("unable to parse peer service url", "error", err) + return nil, err + } + //Load certificates + tlsConfig, err := ClientTLSConfigFromBytes(utils.PeerCertificate, utils.PeerPrivateKey, utils.PeerCACertificate, u.Host) + if err != nil { + _log.Infow("unable to build tls config for peer service", "error", err) + return nil, err + } + transportCreds := credentials.NewTLS(tlsConfig) + peerSeviceHost := u.Host + + conn, err := grpc.NewSecureClientConn(ctx, peerSeviceHost, transportCreds) + if err != nil { + _log.Infow("unable to connect to sentry", "error", err) + return nil, err + } + defer conn.Close() + + client := sentryrpc.NewClusterAuthorizationClient(conn) + + resp, err := client.GetUserAuthorization(ctx, &sentryrpc.GetUserAuthorizationRequest{ + UserCN: userCN, + ClusterID: clusterID, + CertIssueSeconds: certIssue, + }) + + if err != nil { + _log.Infow("unable to get authorization for user", "error", err) + return nil, err + } + return resp, nil +} + +func (p *authzProvisioner) provisionAuthzHandleRoleChange(auth *sentryrpc.GetUserAuthorizationResponse, socketPath, sni, dialinLookupKey string) { + k8sDelClient, err := p.getClient(socketPath, sni, dialinLookupKey) + if err != nil { + _log.Infow("unable to get del client", "sni", sni, "dialinLookupKey", dialinLookupKey, "error", err) + return + } + nctx, cancel := context.WithTimeout(context.Background(), time.Second*180) + defer cancel() + // Delete cluster role binding to clean if exist to handle change in user role + for _, crb := range auth.DeleteClusterRoleBindings { + //crbo, _, err := runtime.ToObject(crb) + if err == nil { + err := k8sDelClient.Delete(nctx, crb) + if err != nil { + _log.Debugw("unable to delete ClusterRoleBindings", "name", crb.TypeMeta.GetObjectKind(), "obj", crb, "err", err) + } else { + _log.Infow("successfully to deleted ClusterRoleBindings", "obj", crb) + } + } + } + + // To handle change in namespace changes delete rolebindings to clean if exist. + for _, rb := range auth.DeleteRoleBindings { + //rbo, _, err := runtime.ToObject(rb) + if err == nil { + err := k8sDelClient.Delete(nctx, rb) + if err != nil { + _log.Debugw("unable to delete RoleBindings", "name", rb.TypeMeta.GetObjectKind(), "obj", rb, "err", err) + } else { + _log.Infow("successfully to deleted RoleBindings", "obj", rb) + } + } + } +} + +func (p *authzProvisioner) provisionAuthz(ctx context.Context, auth *sentryrpc.GetUserAuthorizationResponse, socketPath, sni, dialinLookupKey string) error { + + k8sClient, err := p.getClient(socketPath, sni, dialinLookupKey) + if err != nil { + _log.Infow("unable to get client", "sni", sni, "dialinLookupKey", dialinLookupKey, "error", err) + return err + } + + // Handle deletion of excluded clusterrolebindings and rolebindings + // in the background. + go p.provisionAuthzHandleRoleChange(auth, socketPath, sni, dialinLookupKey) + if len(auth.ClusterRoles) > 0 { + _log.Infow("cluster scope authz for", "user", auth.ServiceAccount, "ClusterRoles", auth.ClusterRoles, "ClusterRoleBindings", auth.ClusterRoleBindings) + } + if len(auth.Roles) > 0 { + _log.Infow("namespace scope authz for", "user", auth.ServiceAccount, "Roles", auth.Roles, "RoleBindings", auth.RoleBindings, "Namespaces", auth.Namespaces) + } + + applier := apply.NewApplier(k8sClient) + + /*sao, _, err := runtime.ToObject(auth.ServiceAccount) + if err != nil { + _log.Infow("unable to make service account runtime object", "error", err) + return err + }*/ + + err = applier.Apply(ctx, auth.ServiceAccount) + // Ignore already exist error caused due to + // multiple inflight request trying to provision + // ZTKA JIT service account + if err != nil && !strings.Contains(err.Error(), "already exists") { + _log.Infow("unable to apply service account", "error", err) + return err + } + + for _, clusterRole := range auth.ClusterRoles { + /*cro, _, err := runtime.ToObject(clusterRole) + if err != nil { + _log.Infow("unable to make clusterRole runtime object", "error", err) + return err + }*/ + + err = applier.Apply(ctx, clusterRole) + if err != nil && !strings.Contains(err.Error(), "already exists") { + _log.Infow("unable to apply clusterRole", "error", err) + return err + } + } + + for _, clusterRoleBinding := range auth.ClusterRoleBindings { + /*crbo, _, err := runtime.ToObject(clusterRoleBinding) + if err != nil { + _log.Infow("unable to make clusterRoleBinding runtime object", "error", err) + return err + }*/ + + err = applier.Apply(ctx, clusterRoleBinding) + if err != nil && !strings.Contains(err.Error(), "already exists") { + _log.Infow("unable to apply clusterRoleBinding", "error", err) + return err + } + } + + for _, role := range auth.Roles { + /*cro, _, err := runtime.ToObject(role) + if err != nil { + _log.Infow("unable to make roles runtime object", "error", err) + return err + }*/ + + err = applier.Apply(ctx, role) + if err != nil && !strings.Contains(err.Error(), "already exists") { + _log.Infow("unable to apply Role", "error", err) + //Ignore any error + } + } + + for _, roleBinding := range auth.RoleBindings { + /*crbo, _, err := runtime.ToObject(roleBinding) + if err != nil { + _log.Infow("unable to make RoleBinding runtime object", "error", err) + return err + }*/ + + err = applier.Apply(ctx, roleBinding) + if err != nil && !strings.Contains(err.Error(), "already exists") { + _log.Infow("unable to apply RoleBinding", "error", err) + //Ignore any error + } + } + + _log.Infow("applied authz", "sni", sni, "user", auth.UserName) + + return nil +} + +type provisionedUser struct { + UserName string + Role string + IsReadRole bool + IsOrgAdmin bool + EnforceOrgAdminOnlySecretAccess bool +} + +func (p *authzProvisioner) ProvisionAuthzForUser(socketPath, userCN, sni, dialinLookupKey string, forceProvision, refreshSession bool, certIssue int64) (string, string, bool, bool, bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + + key := func() uint64 { + hasher := _hashPool.Get().(hash.Hash64) + hasher.Reset() + hasher.Write([]byte(userCN)) + hasher.Write([]byte(sni)) + k := hasher.Sum64() + _hashPool.Put(hasher) + return k + }() + + if !forceProvision { + if i, ok := p.authzCache.Get(key); ok { + if pu, ok := i.(*provisionedUser); ok { + return pu.UserName, pu.Role, pu.IsReadRole, pu.IsOrgAdmin, pu.EnforceOrgAdminOnlySecretAccess, nil + } + } + } + + resp, err := p.getAuthz(ctx, userCN, sni, certIssue) + if err != nil { + return "", "", false, false, false, err + } + + applyctx, applycancel := context.WithTimeout(context.Background(), time.Second*60) + defer applycancel() + err = p.provisionAuthz(applyctx, resp, socketPath, sni, dialinLookupKey) + // ignore cluster provision failures during refresh. + // continue with existing RBAC. + if !refreshSession && err != nil { + return "", "", false, false, false, err + } + + p.authzCache.SetWithTTL(key, &provisionedUser{ + UserName: resp.UserName, + Role: resp.RoleName, + IsReadRole: resp.IsRead, + IsOrgAdmin: resp.IsOrgAdmin, + EnforceOrgAdminOnlySecretAccess: resp.EnforceOrgAdminOnlySecretAccess, + }, 100, time.Minute*5) + + return resp.UserName, resp.RoleName, resp.IsRead, resp.IsOrgAdmin, resp.EnforceOrgAdminOnlySecretAccess, nil +} diff --git a/components/relay/pkg/tunnel/backoff.go b/components/relay/pkg/tunnel/backoff.go new file mode 100644 index 0000000..d4fcccb --- /dev/null +++ b/components/relay/pkg/tunnel/backoff.go @@ -0,0 +1,15 @@ +package tunnel + +import "time" + +// Backoff defines behavior of staggering reconnection retries. +type Backoff interface { + // Next returns the duration to sleep before retrying to reconnect. + // If the returned value is negative, the retry is aborted. + NextBackOff() time.Duration + + // Reset is used to signal a reconnection was successful and next + // call to Next should return desired time duration for 1st reconnection + // attempt. + Reset() +} diff --git a/components/relay/pkg/tunnel/client.go b/components/relay/pkg/tunnel/client.go new file mode 100644 index 0000000..522e45e --- /dev/null +++ b/components/relay/pkg/tunnel/client.go @@ -0,0 +1,651 @@ +package tunnel + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/proxy" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/cenkalti/backoff" + "golang.org/x/net/http2" +) + +//ClientConfig .. +type ClientConfig struct { + //ServiceName name of the service + ServiceName string + // ServerAddr specifies address of the tunnel server. + ServerAddr string + //Upstream upstream address + Upstream string + //Protocol .. + Protocol string + // TLSClientConfig specifies the tls configuration to use with + // tls.Client. + TLSClientConfig *tls.Config + // Backoff specifies backoff policy on server connection retry. If nil + // when dial fails it will not be retried. + Backoff Backoff + + //ServiceProxy is Func responsible for transferring data between server and local services. + ServiceProxy proxy.Func + // Logger is optional logger. If nil logging is disabled. + Logger *relaylogger.RelayLog +} + +//Client struct +type Client struct { + sync.Mutex + conn net.Conn + httpServer *http2.Server + serverErr error + lastDisconnect time.Time + config *ClientConfig + logger *relaylogger.RelayLog + closeChnl chan bool + streams int64 + lastRequest time.Time + lastRequestStreams int64 + isScaledConnection bool +} + +var ( + clog *relaylogger.RelayLog + //Clients map, key by ServerName + //Clients = make(map[string]*Client) + ScaleClients = make(chan bool, 5) +) + +func expBackoff(c BackoffConfig) *backoff.ExponentialBackOff { + b := backoff.NewExponentialBackOff() + b.InitialInterval = c.Interval + b.Multiplier = c.Multiplier + b.MaxInterval = c.MaxInterval + b.MaxElapsedTime = c.MaxTime + + return b +} + +//loadNewRelayNetwork start the relay agent for a given network +func loadNewRelayNetwork(ctx context.Context, rnc utils.RelayNetworkConfig) error { + var spxy proxy.Func + var tlsconf *tls.Config + var dialout *Dialout + var err error + + backoff := BackoffConfig{ + Interval: DefaultBackoffInterval, + Multiplier: DefaultBackoffMultiplier, + MaxInterval: DefaultBackoffMaxInterval, + MaxTime: DefaultBackoffMaxTime, + } + + //proxy handler + switch rnc.Network.Name { + case utils.CDAGENTCORE: + tlsconf, err = ClientTLSConfigFromBytes(rnc.RelayAgentCert, rnc.RelayAgentKey, rnc.RelayAgentCACert, strings.ReplaceAll(rnc.Network.Domain, "*", utils.AgentID)) + if err != nil { + clog.Error( + err, + "failed to create tlsconfig", + "service name", rnc.Network.Name, + "addr", rnc.Network.Domain, + ) + return fmt.Errorf("failed to load client certs") + } + dialout = &Dialout{ + Protocol: rnc.Network.Name, + Upstream: rnc.Network.Upstream, + Addr: strings.ReplaceAll(rnc.Network.Domain, "*", utils.AgentID), + ServiceSNI: utils.AgentID, + } + spxy = clientProxy(utils.GENTCP, dialout, clog.WithName(rnc.Network.Name)) + default: + tlsconf, err = ClientTLSConfigFromBytes(rnc.RelayAgentCert, rnc.RelayAgentKey, rnc.RelayAgentCACert, strings.ReplaceAll(rnc.Network.Domain, "*", utils.ClusterID)) + if err != nil { + clog.Error( + err, + "failed to create tlsconfig", + "service name", rnc.Network.Name, + "addr", rnc.Network.Domain, + ) + return fmt.Errorf("failed to load client certs") + } + dialout = &Dialout{ + Protocol: rnc.Network.Name, + Upstream: rnc.Network.Upstream, + Addr: strings.ReplaceAll(rnc.Network.Domain, "*", utils.ClusterID), + ServiceSNI: utils.ClusterID, + } + spxy = clientProxy(utils.KUBECTL, dialout, clog.WithName(rnc.Network.Name)) + } + + if spxy == nil { + return fmt.Errorf("failed to load client proxy") + } + + ccfg := &ClientConfig{ + ServiceName: rnc.Network.Name, + ServerAddr: dialout.Addr, + Protocol: dialout.Protocol, + TLSClientConfig: tlsconf, + Upstream: dialout.Upstream, + ServiceProxy: spxy, + Backoff: expBackoff(backoff), + Logger: clog.WithName(rnc.Network.Name), + } + + for i := 0; i < utils.MaxDials; i++ { + c := &Client{ + config: ccfg, + httpServer: &http2.Server{}, + logger: clog.WithName(rnc.Network.Name), + isScaledConnection: false, + } + + c.closeChnl = make(chan bool) + go c.runClient(ctx) + } + + // scale clients + // based on rate of new streams or + // based on total concurrent streams + go runClientScaling(ctx, ccfg, rnc) + + return nil +} + +func runClientScaling(ctx context.Context, ccfg *ClientConfig, rnc utils.RelayNetworkConfig) { + var scaledClients []*Client + totalScaledClient := 0 + + for { + select { + case <-ScaleClients: + // got signal to scale + if totalScaledClient < utils.MaxDials*utils.MaxScaleMultiplier { + c := &Client{ + config: ccfg, + httpServer: &http2.Server{}, + logger: clog.WithName(rnc.Network.Name), + isScaledConnection: true, + } + c.closeChnl = make(chan bool) + scaledClients = append(scaledClients, c) + totalScaledClient++ + go c.runClient(ctx) + clog.Info( + "scale client signal", + " totalScaledClient ", totalScaledClient, + ) + } + case <-time.After(5 * time.Minute): + tempClients := scaledClients[:0] + for _, c := range scaledClients { + now := time.Now() + c.Lock() + curStreams := atomic.LoadInt64(&c.streams) + d := now.Sub(c.lastRequest) + s := int64(d / time.Hour) + if curStreams <= 0 && s >= int64(utils.HealingInterval) { + // no request for last 24Hr, close client + c.closeChnl <- true + totalScaledClient-- + } else { + tempClients = append(tempClients, c) + } + c.Unlock() + } + scaledClients = tempClients + clog.Info( + "scaled client timer", + " totalScaledClient ", totalScaledClient, + ) + case <-ctx.Done(): + return + } + } +} + +func (c *Client) runClient(ctx context.Context) { + if err := c.Start(ctx); err != nil { + if c.isScaledConnection { + c.logger.Info( + "closed scaled dialouts after idle time", + "service name", c.config.ServiceName, + "addr", c.config.ServerAddr, + ) + } else { + c.logger.Error( + err, + "failed to start client for dialouts", + "service name", c.config.ServiceName, + "addr", c.config.ServerAddr, + ) + } + } +} + +//Start relay client +func (c *Client) Start(ctx context.Context) error { + cw := make(chan bool) + + for { + conn, err := c.connect() + if err != nil { + c.logger.Info( + "connect failed will retry after 2 sec", + ) + time.Sleep(2 * time.Second) + continue + } + + go func() { + //handles the http/2 tunnel + c.httpServer.ServeConn(conn, &http2.ServeConnOpts{ + Handler: http.HandlerFunc(c.serveHTTP), + }) + + cw <- true + }() + + select { + case <-cw: + // dialout got closed + time.Sleep(100 * time.Millisecond) + case <-c.closeChnl: + c.conn.Close() + c.logger.Info( + "close connection due to idle streams", + ) + return fmt.Errorf("idle streams") + case <-ctx.Done(): + c.conn.Close() + return fmt.Errorf("ctx done") + } + + c.logger.Info( + "client disconnected", + ) + c.Lock() + now := time.Now() + err = c.serverErr + + // detect disconnect hiccup + if err == nil && now.Sub(c.lastDisconnect).Seconds() < 5 { + err = fmt.Errorf("connection is being cut") + } + + c.conn = nil + c.serverErr = nil + c.lastDisconnect = now + c.Unlock() + + if err != nil { + c.logger.Error( + err, + "disconnected, will retry after 2 sec", + "name", c.config.ServiceName, + ) + time.Sleep(2 * time.Second) + continue + } + } +} + +func (c *Client) processDialoutProxy(conn net.Conn, network, addr string) error { + // send CONNECT addr HTTP/1.1 header + connHeader := "CONNECT " + addr + " HTTP/1.1\r\nHost: " + addr + "\r\n" + if utils.DialoutProxyAuth != "" { + connHeader = connHeader + "Proxy-Authorization:" + utils.DialoutProxyAuth + "\r\n" + } + connHeader = connHeader + "Connection: Keep-Alive\r\n\r\n" + + conn.Write([]byte(connHeader)) + tmp := make([]byte, 1) + checkHeader := "" + startAppend := false + respData := "" + for { + n, err := conn.Read(tmp) + if n != 1 || err != nil { + c.logger.Info( + "dial failed ", + "proxy", utils.DialoutProxy, + "proxy header", connHeader, + "network", network, + "checkHeader", checkHeader, + "err", err, + ) + return fmt.Errorf("proxy read error") + } + respData += string(tmp[0]) + if tmp[0] == '\r' { + if startAppend { + checkHeader += string(tmp[0]) + } else { + checkHeader = "\r" + startAppend = true + } + } else if tmp[0] == '\n' { + if startAppend { + if checkHeader == "\r\n\r" { + // prased the proxy response header + break + } + checkHeader += string(tmp) + } + } else { + startAppend = false + checkHeader = "" + } + } + c.logger.Info( + "proxy dialout success", + "proxy resp", respData, + ) + return nil +} + +//dialout connect +func (c *Client) connect() (net.Conn, error) { + c.Lock() + defer c.Unlock() + + if c.conn != nil { + return nil, fmt.Errorf("already connected") + } + + conn, err := c.dial() + if err != nil { + return nil, fmt.Errorf("failed to connect to server %s error %s", c.config.ServerAddr, err) + } + c.conn = conn + + return conn, nil +} + +//dial +func (c *Client) dial() (net.Conn, error) { + var ( + network = "tcp" + addr = c.config.ServerAddr + tlsConfig = c.config.TLSClientConfig + ) + + doDial := func() (conn net.Conn, err error) { + + d := &net.Dialer{ + Timeout: 60 * time.Second, + } + if utils.DialoutProxy == "" { + conn, err = d.Dial(network, addr) + } else { + conn, err = d.Dial(network, utils.DialoutProxy) + if err == nil { + err = c.processDialoutProxy(conn, network, addr) + } + } + + if err == nil { + err = utils.KeepAlive(conn) + } + if err == nil { + conn = tls.Client(conn, tlsConfig) + } + + if err == nil { + err = conn.(*tls.Conn).Handshake() + } + + if err != nil { + if conn != nil { + conn.Close() + conn = nil + } + + c.logger.Info( + "dial failed", + "network", network, + "addr", addr, + "err", err, + ) + } + + c.logger.Info( + "action dial out", + "network", network, + "addr", addr, + ) + + return + } + + b := c.config.Backoff + if b == nil { + // try once and return + return doDial() + } + + for { + + conn, err := doDial() + + // success + if err == nil { + b.Reset() + c.logger.Info( + "dial success", + ) + return conn, err + } + + // failure + d := b.NextBackOff() + if d < 0 { + b.Reset() + return conn, fmt.Errorf("backoff limit exeded: %s", err) + } + + // backoff + c.logger.Info( + "action backoff", + "sleep", d, + "address", c.config.ServerAddr, + ) + time.Sleep(d) + } +} + +// tunnel serveHTTP handler. Requests form relay lands here. +// request handled based on the action in the ctrl message (header) +// Connect method is used as handshake +// PUT method is used for tunneled requests that carry +// stream data as req.Body +func (c *Client) serveHTTP(w http.ResponseWriter, r *http.Request) { + c.logger.Debug( + "invoked handler serveHTTP", + "method", r.Method, + "currentStreams", atomic.LoadInt64(&c.streams), + ) + + if r.Method == http.MethodConnect { + if r.Header.Get(utils.HeaderError) != "" { + c.handleHandshakeError(w, r) + } else { + c.handleHandshake(w, r) + } + return + } + + atomic.AddInt64(&c.streams, 1) + defer atomic.AddInt64(&c.streams, ^int64(0)) + + curStreams := atomic.LoadInt64(&c.streams) + now := time.Now() + + c.Lock() + d := now.Sub(c.lastRequest) + s := int64(d / time.Second) + if s >= 4 { // check in minimum 4 sec + if !c.isScaledConnection { + if curStreams > int64(utils.ScalingStreamsThreshold) { + // concurrent streams count is high + ScaleClients <- true + } else if curStreams > c.lastRequestStreams { + rate := (curStreams - c.lastRequestStreams) / s + if rate > int64(utils.ScalingStreamsRateThreshold) { + // rate of new stream is high + ScaleClients <- true + } + } + } + c.lastRequest = now + c.lastRequestStreams = curStreams + } + c.Unlock() + + msg, err := utils.ReadControlMessage(r) + if err != nil { + c.logger.Error( + err, + "Read Control Message failed", + ) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + c.logger.Debug( + "handle proxy action", + "ctrlMsg", msg, + "req", r, + "currentStreams", atomic.LoadInt64(&c.streams), + ) + + switch msg.Action { + case utils.ActionProxy: + c.config.ServiceProxy(w, r.Body, msg, r) + default: + c.logger.Info( + "unknown action", + "ctrlMsg", msg, + ) + http.Error(w, err.Error(), http.StatusBadRequest) + } +} + +func (c *Client) handleHandshakeError(w http.ResponseWriter, r *http.Request) { + err := fmt.Errorf(r.Header.Get(utils.HeaderError)) + + c.logger.Info( + "action handshake error", + "addr", r.RemoteAddr, + "err", err, + ) + + c.Lock() + c.serverErr = fmt.Errorf("server error: %s", err) + c.Unlock() +} + +func (c *Client) handleHandshake(w http.ResponseWriter, r *http.Request) { + + c.logger.Info( + "action", "handshake", + "addr", r.RemoteAddr, + ) + + w.WriteHeader(http.StatusOK) + host, _, _ := net.SplitHostPort(c.config.ServerAddr) + msg := handShakeMsg{ + ServiceName: c.config.ServiceName, + Protocol: c.config.Protocol, + Host: host, + } + b, err := json.Marshal(msg) + if err != nil { + c.logger.Info( + "msg", "handshake failed", + "err", err, + ) + return + } + w.Write(b) +} + +//setups the proxy func handler +func clientProxy(svcName string, d *Dialout, logger *relaylogger.RelayLog) proxy.Func { + proxyCfg := &utils.ProxyConfig{ + Protocol: d.Protocol, + Addr: d.Addr, + ServiceSNI: d.ServiceSNI, + RootCA: d.RootCA, + ClientCRT: d.ClientCRT, + ClientKEY: d.ClientKEY, + Upstream: d.Upstream, + UpstreamClientCRT: d.UpstreamClientCRT, + UpstreamClientKEY: d.UpstreamClientKEY, + UpstreamRootCA: d.UpstreamRootCA, + UpstreamSkipVerify: d.UpstreamSkipVerify, + UpstreamKubeConfig: d.UpstreamKubeConfig, + Version: d.Version, + } + + switch svcName { + case utils.KUBECTL: + if p := proxy.NewKubeCtlTCPProxy(logger.WithName("TCPProxy"), proxyCfg); p != nil { + return p.Proxy + } + logger.Error( + nil, + "proxy.NewKubeCtlTCPProxy returned nil", + ) + return nil + case utils.GENTCP: + if p := proxy.NewTCPProxy(logger.WithName("TCPProxy"), proxyCfg); p != nil { + return p.Proxy + } + logger.Error( + nil, + "proxy.NewTCPProxy returned nil", + ) + return nil + default: + logger.Error( + nil, + "unknown service name", + ) + return nil + } +} + +//StartClient starts relay clients +func StartClient(ctx context.Context, log *relaylogger.RelayLog, file string, rnc utils.RelayNetworkConfig, exitChan chan<- bool) { + clog = log.WithName("Client") + + if err := loadNewRelayNetwork(ctx, rnc); err != nil { + clog.Error(err, "failed to load relay network") + exitChan <- true + return + } + + for { + select { + case <-ctx.Done(): + clog.Error( + ctx.Err(), + "Stopping client", + ) + return + } + } +} diff --git a/components/relay/pkg/tunnel/client_config.go b/components/relay/pkg/tunnel/client_config.go new file mode 100644 index 0000000..02107cf --- /dev/null +++ b/components/relay/pkg/tunnel/client_config.go @@ -0,0 +1,38 @@ +package tunnel + +import ( + "time" +) + +// Default backoff configuration. +const ( + DefaultBackoffInterval = 500 * time.Millisecond + DefaultBackoffMultiplier = 1.5 + DefaultBackoffMaxInterval = 20 * time.Second + DefaultBackoffMaxTime = 2 * time.Minute +) + +// BackoffConfig defines behavior of staggering reconnection retries. +type BackoffConfig struct { + Interval time.Duration + Multiplier float64 + MaxInterval time.Duration + MaxTime time.Duration +} + +// Dialout defines the dialout. +type Dialout struct { + Protocol string + Addr string + ServiceSNI string + RootCA string + ClientCRT string + ClientKEY string + Upstream string + UpstreamClientCRT string + UpstreamClientKEY string + UpstreamRootCA string + UpstreamSkipVerify bool + UpstreamKubeConfig string + Version string +} diff --git a/components/relay/pkg/tunnel/client_test.go b/components/relay/pkg/tunnel/client_test.go new file mode 100644 index 0000000..a5cc9f9 --- /dev/null +++ b/components/relay/pkg/tunnel/client_test.go @@ -0,0 +1,70 @@ +package tunnel + +import ( + "fmt" + "net" + "sync" + "testing" + "time" +) + +const ( + //UNIXSOCKET .. + UNIXSOCKET = "/tmp/relay-unix-" +) + +func startClient(wg *sync.WaitGroup, buf []byte, t *testing.T) { + defer wg.Done() + + fmt.Print("starting client") + buf3 := make([]byte, 4096) + // connect to this socket + socketPath := UNIXSOCKET + "kubectldialin.relay.rafay.dev" + conn, err := net.DialTimeout("unix", socketPath, 60*time.Second) + if err != nil { + t.Error("failed to connect ", err) + return + } + + fmt.Println("Connected to ", socketPath) + + for { + conn.Write(buf) + str := "GET /apis/rbac.authorization.k8s.io/v1?timeout=32s HTTP/1.1\r\nHost: 192.168.56.103:6443\r\nUser-Agent: curl/7.54.0\r\nAccept: */*\r\nX-Rafay-User:namespace-admin-sa\r\nX-Rafay-Namespace: default\r\n\r\n" + conn.Write([]byte(str)) + + conn.SetReadDeadline(time.Now().Add(10 * (time.Second))) + + for { + nr, err := conn.Read(buf3) + if err != nil { + fmt.Println("done reading ", err) + return + } + data := buf3[0:nr] + println("Client got:", string(data), nr) + } + } +} + +func TestRelayClient(t *testing.T) { + var wg sync.WaitGroup + + buf := make([]byte, 1024) + num := 1 + key := "cluster1.kubectldialin.relay.rafay.dev-ABCD" + + message := fmt.Sprintf("{\"DialinKey\": \"%s\", \"UserName\": \"namespace-admin-sa\", \"SNI\": \"cluster1.kubectldialin.relay.rafay.dev\"}", key[:len(key)-1]) + copy(buf, message) + + for i := 0; i < num; i++ { + wg.Add(1) + fmt.Println("starting client") + go startClient(&wg, buf, t) + } + + fmt.Print("Waiting for workers") + wg.Wait() + + fmt.Print("Completed") +} diff --git a/components/relay/pkg/tunnel/common.go b/components/relay/pkg/tunnel/common.go new file mode 100644 index 0000000..472fcd2 --- /dev/null +++ b/components/relay/pkg/tunnel/common.go @@ -0,0 +1,189 @@ +package tunnel + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" +) + +type handShakeMsg struct { + ServiceName string + Protocol string + Host string +} + +var ( + //PeerProbeChanel to push cluster connection probe. + //The RPC picks the sni and sends to core + PeerProbeChanel = make(chan string, 256) + emptyID [32]byte +) + +//ServerTLSConfigFromBytes prepare a tls config from cert,key,rootCA +func ServerTLSConfigFromBytes(certList []utils.SNICertificate, rootCAs []string, nextprotos ...string) (*tls.Config, error) { + var err error + + // load certs + config := &tls.Config{} + config.Certificates = make([]tls.Certificate, len(certList)) + + for i, v := range certList { + config.Certificates[i], err = tls.X509KeyPair(v.CertFile, v.KeyFile) + if err != nil { + return nil, err + } + } + + config.BuildNameToCertificate() + + // load rootCAs for client authentication + clientAuth := tls.RequireAndVerifyClientCert + var roots *x509.CertPool + if len(rootCAs) > 0 { + roots = x509.NewCertPool() + + for _, rootCA := range rootCAs { + rootPEM := []byte(rootCA) + if len(rootPEM) > 0 { + if ok := roots.AppendCertsFromPEM(rootPEM); !ok { + return nil, err + } + } + } + + clientAuth = tls.RequireAndVerifyClientCert + } + + config.ClientAuth = clientAuth + config.ClientCAs = roots + config.SessionTicketsDisabled = true + config.MinVersion = tls.VersionTLS12 + config.CipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256} + config.PreferServerCipherSuites = true + if len(nextprotos) > 0 { + config.NextProtos = nextprotos + } else { + config.NextProtos = []string{"h1", "h2"} + } + + return config, nil +} + +//ClientTLSConfigFromBytes sets tls config +func ClientTLSConfigFromBytes(tlsCrt []byte, tlsKey []byte, rootPEM []byte, addr string) (*tls.Config, error) { + cert, err := tls.X509KeyPair(tlsCrt, tlsKey) + if err != nil { + return nil, err + } + + var roots *x509.CertPool + if len(rootPEM) > 0 { + roots = x509.NewCertPool() + if ok := roots.AppendCertsFromPEM(rootPEM); !ok { + return nil, err + } + } + + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return &tls.Config{ + ServerName: host, + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: roots == nil, + RootCAs: roots, + SessionTicketsDisabled: true, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + PreferServerCipherSuites: true, + }, nil +} + +//ClientTLSConfig sets tls config +func ClientTLSConfig(tlsCrt string, tlsKey string, rootCA string, addr string) (*tls.Config, error) { + + cert, err := tls.LoadX509KeyPair(tlsCrt, tlsKey) + if err != nil { + return nil, err + } + + var roots *x509.CertPool + if rootCA != "" { + roots = x509.NewCertPool() + rootPEM, err := ioutil.ReadFile(rootCA) + if err != nil { + return nil, err + } + if ok := roots.AppendCertsFromPEM(rootPEM); !ok { + return nil, err + } + } + + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return &tls.Config{ + ServerName: host, + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: roots == nil, + RootCAs: roots, + SessionTicketsDisabled: true, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + PreferServerCipherSuites: true, + }, nil +} + +//GetRemoteCertID extract peer ID +func GetRemoteCertID(conn *tls.Conn) (string, error) { + // Try a TLS connection over the given connection. We explicitly perform + // the handshake, since we want to maintain the invariant that, if this + // function returns successfully, then the connection should be valid + // and verified. + if err := conn.Handshake(); err != nil { + return "", err + } + + cs := conn.ConnectionState() + + // We should have exactly one peer certificate. + certs := cs.PeerCertificates + if cl := len(certs); cl != 1 { + return "", fmt.Errorf("expecting 1 peer certificate got %d", cl) + } + + // Get remote cert's ID. + remoteCert := certs[0] + //remoteID := New(remoteCert.Raw) + + return remoteCert.Subject.CommonName, nil +} + +func SendPeerProbe(chnl chan<- string, clustersni string) { + attempt := 0 + select { + case chnl <- clustersni: + return + default: + attempt++ + if attempt > 3 { + return + } + time.Sleep(5 * time.Second) + } +} diff --git a/components/relay/pkg/tunnel/dialin_pool.go b/components/relay/pkg/tunnel/dialin_pool.go new file mode 100644 index 0000000..993fd27 --- /dev/null +++ b/components/relay/pkg/tunnel/dialin_pool.go @@ -0,0 +1,381 @@ +package tunnel + +import ( + "context" + "fmt" + "net" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "golang.org/x/net/http2" +) + +var _dplog *relaylogger.RelayLog + +type connPair struct { + conn net.Conn + clientConn *http2.ClientConn +} + +type dialinConnector struct { + // this is list of dialin peer ids + connKeys []string + lbIndex int +} +type dialinPool struct { + t *http2.Transport + conns map[string]connPair // key is host:port + free func(string) + mu sync.RWMutex + //map of connector IDs with dialin SNI key + dialinConnectors map[string]*dialinConnector +} + +func newDialinPool(t *http2.Transport, f func(string), log *relaylogger.RelayLog) *dialinPool { + return &dialinPool{ + t: t, + free: f, + conns: make(map[string]connPair), + dialinConnectors: make(map[string]*dialinConnector), + } +} + +func (p *dialinPool) URL(key string) string { + return fmt.Sprint("https://", key) +} + +//GetClientConn get connector +func (p *dialinPool) GetClientConn(req *http.Request, addr string) (*http2.ClientConn, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + if cp, ok := p.conns[addr]; ok && cp.clientConn.CanTakeNewRequest() { + return cp.clientConn, nil + } + + return nil, errClientNotConnected +} + +// CheckDialinKeyExist check cached key's still exist in pool +func (p *dialinPool) CheckDialinKeyExist(key string) bool { + p.mu.RLock() + defer p.mu.RUnlock() + _, ok := p.conns[key] + return ok +} + +func (p *dialinPool) MarkDead(c *http2.ClientConn) { + p.mu.Lock() + defer p.mu.Unlock() + + for addr, cp := range p.conns { + if cp.clientConn == c { + _dplog.Debug("MarkDead", addr, p) + p.close(cp, addr) + s := strings.SplitAfter(addr, utils.JoinString) + if len(s) == 3 { + sni := strings.Trim(s[0], utils.JoinString) + addr := s[1] + s[2] + if !p.deleteDialinConnectorKey(sni, addr) { + _dplog.Error(nil, "error in dialin MarkDead connector key delete did not find key ", addr) + } + } + return + } + } +} + +func (p *dialinPool) AddConn(conn net.Conn, identifier string, sni string, remoteAddr string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + + // append peer-cert-id with remoteAddr + addr := p.addr(identifier) + utils.JoinString + remoteAddr + + // prepend dialin sni to addr to construct the key + key := sni + utils.JoinString + addr + + if cp, ok := p.conns[key]; ok { + // check conns has the key + if err := p.ping(cp); err != nil { + p.close(cp, key) + } else { + return "", fmt.Errorf("connector key %s already in dialin pool ", key) + } + } + + // create transport new connection + c, err := p.t.NewClientConn(conn) + if err != nil { + return "", err + } + + // add conn to conns + p.conns[key] = connPair{ + conn: conn, + clientConn: c, + } + + go sendHeartBeats(p.conns[key]) + + // set dialinConnectors map using key as sni + if item, ok := p.dialinConnectors[sni]; ok { + itemCount := len(item.connKeys) + if itemCount <= 0 { + p.dialinConnectors[sni].connKeys = append(p.dialinConnectors[sni].connKeys, addr) + sort.Strings(p.dialinConnectors[sni].connKeys) + } else { + index := sort.SearchStrings(p.dialinConnectors[sni].connKeys, addr) + if index < itemCount && p.dialinConnectors[sni].connKeys[index] == addr { + _dplog.Info("address already exist") + } else { + //item not found + p.dialinConnectors[sni].connKeys = append(p.dialinConnectors[sni].connKeys, addr) + sort.Strings(p.dialinConnectors[sni].connKeys) + } + } + } else { + p.dialinConnectors[sni] = &dialinConnector{ + lbIndex: 0, + } + p.dialinConnectors[sni].connKeys = append(p.dialinConnectors[sni].connKeys, addr) + sort.Strings(p.dialinConnectors[sni].connKeys) + } + + _dplog.Info( + "Added dialin connection", + "addr", addr, + "key", key, + ) + return key, nil +} + +//GetDialinConnectorKey get connector key +func (p *dialinPool) GetDialinConnectorCount(sni string) (int, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + _dplog.Debug( + "GetDialinConnectorCount", + "sni", sni, + "pool", p, + ) + + _dplog.Info("GetDialinConnectorCount", p.dialinConnectors) + if item, ok := p.dialinConnectors[sni]; ok { + itemCount := len(item.connKeys) + if itemCount > 0 { + return itemCount, nil + } + } + + return 0, fmt.Errorf("Empty dialin pool.dialinConnectors for sni %s ", sni) +} + +func (p *dialinPool) getConnKey(sni string, item *dialinConnector, count int) (string, error) { + for i := 0; i < count; i++ { + key := sni + utils.JoinString + item.connKeys[i] + if _, ok := p.conns[key]; ok { + return key, nil + } + } + + return "", fmt.Errorf("Empty dialin pool.dialinConnectors for sni %s ", sni) +} + +//GetDialinConnectorKey get connector key +func (p *dialinPool) GetDialinConnectorKey(sni string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + + _dplog.Info( + "GetDialinConnectorKey", + "sni", sni, + ) + + if item, ok := p.dialinConnectors[sni]; ok { + itemCount := len(item.connKeys) + + if itemCount <= 0 { + return "", fmt.Errorf("Empty dialin pool.dialinConnectors for sni %s ", sni) + } + + //simple round robin selection + if item.lbIndex >= itemCount { + item.lbIndex = 0 + } + + key := sni + utils.JoinString + item.connKeys[item.lbIndex] + item.lbIndex++ + + // check key + if _, ok := p.conns[key]; !ok { + // get any key + return p.getConnKey(sni, item, itemCount) + } + + // key is valid + return key, nil + } + + return "", fmt.Errorf("Empty dialin pool.dialinConnectors for sni %s ", sni) +} + +func (p *dialinPool) deleteDialinConnectorKey(sni, addr string) bool { + if item, ok := p.dialinConnectors[sni]; ok { + itemCount := len(item.connKeys) + index := sort.SearchStrings(p.dialinConnectors[sni].connKeys, addr) + if index < itemCount { + // Not expecting lots of connectors. So using slower delete opeartion + // by shifting elements to preserve the sorted orders + p.dialinConnectors[sni].connKeys = append(item.connKeys[:index], item.connKeys[index+1:]...) + _dplog.Info( + "Deleted Connnection from dialin pool", + "addr", addr, + ) + if len(p.dialinConnectors[sni].connKeys) <= 0 { + _dplog.Info( + "Deleted last connection from dialin pool", + "sni", sni, + "addr", addr, + ) + delete(p.dialinConnectors, sni) + } + return true + } + } + return false +} + +func (p *dialinPool) DeleteConn(identifier string, sni string, remoteAddr string) { + p.mu.Lock() + defer p.mu.Unlock() + + // append peer-cert-id with remoteAddr + addr := identifier + utils.JoinString + remoteAddr + + // prepend dialin sni to addr to construct the key + key := sni + utils.JoinString + addr + if cp, ok := p.conns[key]; ok { + p.close(cp, key) + } else { + _dplog.Error( + nil, + "did not find key in pool.Conns", + "key", key, + "pool", p, + ) + } + + if p.deleteDialinConnectorKey(sni, addr) { + return + } + + _dplog.Info( + "DeleteConn did not find the coonection", + "sni", sni, + "addr", addr, + "key", key, + "pool", p, + ) + +} + +func (p *dialinPool) Ping(identifier string) (time.Duration, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if cp, ok := p.conns[identifier]; ok { + start := time.Now() + err := p.ping(cp) + return time.Since(start), err + } + + return 0, errClientNotConnected +} + +// heart beats to keep dialins alive to avoid idletimeout +func sendHeartBeats(cp connPair) { + for { + ctx, cancel := context.WithTimeout(context.Background(), utils.DefaultPingTimeout) + if err := cp.clientConn.Ping(ctx); err != nil { + cancel() + _dplog.Debug("dialin keep-alive ping err", err) + return + } + _dplog.Debug("dialin keep-alive ping success") + cancel() + time.Sleep(30 * time.Second) + } +} + +func (p *dialinPool) ping(cp connPair) error { + ctx, cancel := context.WithTimeout(context.Background(), utils.DefaultPingTimeout) + defer cancel() + + return cp.clientConn.Ping(ctx) +} + +func (p *dialinPool) close(cp connPair, addr string) { + cp.conn.Close() + _dplog.Info("dialin close", addr) + delete(p.conns, addr) + if p.free != nil { + p.free(addr) + } +} + +func (p *dialinPool) addr(identifier string) string { + return identifier +} + +//StartDialinPoolMgr starting dialin connection manager +func StartDialinPoolMgr(ctx context.Context, log *relaylogger.RelayLog, exitChan chan<- bool) { + _dplog = log.WithName("DialinPool") + + for { + select { + case <-ctx.Done(): + slog.Error( + ctx.Err(), + "Stopping dialin pool manager", + ) + return + } + } +} + +//GetDialinMetrics get connector key +func (p *dialinPool) GetDialinMetrics(w http.ResponseWriter) { + var clusterCnt, connCnt int + clusterCnt, connCnt = 0, 0 + + p.mu.RLock() + defer p.mu.RUnlock() + + _dplog.Info( + "GetDialinMetrics", + ) + + fmt.Fprintf(w, "{\"dialinmetrics\": [") + for sni, item := range p.dialinConnectors { + clusterCnt++ + connCnt += len(item.connKeys) + itemCount := strconv.Itoa(len(item.connKeys)) + + fmt.Fprintf(w, "{\"cluster\": \""+sni+"\", \"connections\": \""+itemCount+"\"},") + + } + + fmt.Fprintf(w, "], \"totalclusters\": "+strconv.Itoa(clusterCnt)) + fmt.Fprintf(w, ", \"totalconnections\": "+strconv.Itoa(connCnt)) + fmt.Fprintf(w, ", \"podname\": "+utils.PODNAME) + + fmt.Fprintf(w, " }") +} diff --git a/components/relay/pkg/tunnel/errors.go b/components/relay/pkg/tunnel/errors.go new file mode 100644 index 0000000..73eff17 --- /dev/null +++ b/components/relay/pkg/tunnel/errors.go @@ -0,0 +1,11 @@ +package tunnel + +import "errors" + +var ( + errClientNotSubscribed = errors.New("client not subscribed") + errClientNotConnected = errors.New("client not connected") + errClientAlreadyConnected = errors.New("client already connected") + + errUnauthorised = errors.New("unauthorised") +) diff --git a/components/relay/pkg/tunnel/peer.go b/components/relay/pkg/tunnel/peer.go new file mode 100644 index 0000000..18d2e17 --- /dev/null +++ b/components/relay/pkg/tunnel/peer.go @@ -0,0 +1,168 @@ +package tunnel + +import ( + "context" + "crypto/tls" + "net/url" + + "time" + + comgrpc "github.com/RafaySystems/rcloud-base/components/common/pkg/grpc" + peerclient "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/peering" + relayrpc "github.com/RafaySystems/rcloud-base/components/common/proto/rpc/sentry" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "google.golang.org/grpc/credentials" +) + +var peerlog *relaylogger.RelayLog + +//StartPeeringMgr will start the peering RPCs +func StartPeeringMgr(ctx context.Context, log *relaylogger.RelayLog, exitChan chan<- bool, config *ServerConfig) { + var tlsConfig *tls.Config + var err error + var peerSeviceHost string + + peerlog = log.WithName("PeeringMgr") + + if utils.PeerCache == nil { + peerlog.Error( + nil, + "peer client cache is nil", + ) + exitChan <- true + return + } + + //Peer Service Clint certs is popultaed from boot strap use it + if len(utils.PeerCertificate) > 0 && len(utils.PeerPrivateKey) > 0 && len(utils.PeerCACertificate) > 0 { + u, err := url.Parse(utils.PeerServiceURI) + if err != nil { + peerlog.Error( + nil, + "peer servic uri parse failed", + ) + exitChan <- true + return + } + //Load certificates + tlsConfig, err = ClientTLSConfigFromBytes(utils.PeerCertificate, utils.PeerPrivateKey, utils.PeerCACertificate, u.Host) + peerSeviceHost = u.Host + } else { + //Load certificates + tlsConfig, err = ClientTLSConfig(config.Controller.ClientCRT, config.Controller.ClientKEY, config.Controller.RootCA, config.Controller.PeerProbeSNI) + peerSeviceHost = config.Controller.PeerProbeSNI + } + + if err != nil { + peerlog.Error( + err, + "Error loading peer TLC config", + ) + exitChan <- true + return + } + + //enforce TLS mutual authN + transportCreds := credentials.NewTLS(tlsConfig) + +connectRetry: + log.Debug("Start Peer grpc dial") + conn, err := comgrpc.NewSecureClientConn(ctx, peerSeviceHost, transportCreds) + if err != nil { + peerlog.Error( + err, + "failed to connect to peer server, retry in 10 seconds", + peerSeviceHost, + ) + time.Sleep(10 * time.Second) + goto connectRetry + } + + log.Info("grpc connected to peer service", peerSeviceHost) + client := relayrpc.NewRelayPeerServiceClient(conn) + + // create RPC streams + helloStream, err := client.RelayPeerHelloRPC(context.Background()) + if err != nil { + peerlog.Error( + err, + "failed to create HelloRPC stream with peer server, retry in 10 seconds", + peerSeviceHost, + ) + conn.Close() + time.Sleep(10 * time.Second) + goto connectRetry + } + + probeStream, err := client.RelayPeerProbeRPC(context.Background()) + if err != nil { + peerlog.Error( + err, + "failed to create ProbeRPC stream with peer server, retry in 10 seconds", + peerSeviceHost, + ) + conn.Close() + time.Sleep(10 * time.Second) + goto connectRetry + } + + surveyStream, err := client.RelayPeerSurveyRPC(context.Background()) + if err != nil { + peerlog.Error( + err, + "failed to create SurveyRPC stream with peer server, retry in 10 seconds", + peerSeviceHost, + ) + conn.Close() + time.Sleep(10 * time.Second) + goto connectRetry + } + + peerlog.Debug( + "created RPC streams with peer server", + peerSeviceHost, + ) + + rpcctx, rpccancel := context.WithCancel(context.Background()) + go peerclient.ClientHelloRPC(rpcctx, helloStream, utils.PeerHelloInterval, utils.RelayUUID, utils.GetRelayIP) + //Add a init time wait for hello stream to finish + time.Sleep(2 * time.Second) + + go peerclient.ClientProbeRPC(rpcctx, probeStream, utils.PeerCache, utils.RelayUUID, utils.PeerCacheDefaultExpiry, PeerProbeChanel, utils.GetRelayIPPort) + + go peerclient.ClientSurveyRPC(rpcctx, surveyStream, utils.RelayUUID, utils.GetRelayIPPort, dialinCountLookup) + + for { + //Watch for errors in rpc streams. + //On error cancel the streams and reconnect. + //HelloRPC send heartbeats in every 60Sec. + //If there is underlying connectivity issues + //HelloRPC will detect it with in 60Sec, + select { + case <-helloStream.Context().Done(): + rpccancel() + conn.Close() + time.Sleep(5 * time.Second) + goto connectRetry + case <-probeStream.Context().Done(): + rpccancel() + conn.Close() + time.Sleep(5 * time.Second) + goto connectRetry + case <-surveyStream.Context().Done(): + rpccancel() + conn.Close() + time.Sleep(5 * time.Second) + goto connectRetry + case <-ctx.Done(): + rpccancel() + slog.Error( + ctx.Err(), + "Stopping PeeringMgr", + ) + return + } + } + +} diff --git a/components/relay/pkg/tunnel/server.go b/components/relay/pkg/tunnel/server.go new file mode 100644 index 0000000..86334b6 --- /dev/null +++ b/components/relay/pkg/tunnel/server.go @@ -0,0 +1,2052 @@ +// Copyright (C) 2020 Rafay Systems https://rafay.co/ + +package tunnel + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/proxy" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/sessions" + + peerclient "github.com/RafaySystems/rcloud-base/components/common/pkg/sentry/peering" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/audit" + "github.com/RafaySystems/rcloud-base/components/relay/pkg/utils" + "github.com/inconshreveable/go-vhost" + "golang.org/x/net/http2" +) + +// Server server definition +type Server struct { + // Type of the server. Relay means user-facing + // Dialin means cluster-facing + Type string + + // Name specifies the service names example kubectl, kubeweb, etc. + Name string + + // ServerName of the listening server. + ServerName string + + // Protocol specifies protocol used http(s) + Protocol string + + // RootCA used to verify TLS client connections + RootCA []byte + + // ServerCRT used for the server + ServerCRT []byte + + // ServerKEY used for the server + ServerKEY []byte + + // DialinServerName specify the dialin server name + // valid only for relay server types. + DialinServerName string + + // ProbeSNI specifies sni used to find peers + //ProbeSNI string + + // DialinPool where dialin connections are parked + // valid only for dialin server types + DialinPool *dialinPool + + // httpClient used to forward users connections to dialin + httpClient *http.Client + + // httpServer used to server user connections + httpServer *http.Server + + Provisioner *authzProvisioner + + auditPath string +} + +//RelayConn connection info +type RelayConn struct { + // Conn is the network connection + Conn net.Conn + + // Type of the server. Relay means user-facing + // Dialin means cluster-facing + Type string + + // ServerName of the server which accepted the connection + ServerName string + + // CertSNI derived from client certificate + CertSNI string + + // PeerID derived from client certificate + PeerID string + + // server block of this connection + server *Server + + // dialinCachedKey already stiched to a dialin + dialinCachedKey string +} + +//ServerListen defines a listen object +type ServerListen struct { + // Addr specifies the listen address + Addr string + // Protocol of all servers listening in above address + Protocol string + // RootCAs used to verify TLS client connections + RootCAs []string + // List of certs used to terminate listening *.format supported + Certs []utils.SNICertificate + // ServerList are the servers using above listen address + ServerList []*Server + + //Mux tls sni muxer + Mux *vhost.TLSMuxer +} + +type kubeError struct { + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + Metadata []string `json:"metadata"` + Status string `json:"status"` + Message string `json:"message"` + Reason string `json:"reason"` + Code int `json:"code"` +} + +var ( + //Config server config + Config *ServerConfig + //Servers map, key by ServerName + Servers = make(map[string]*Server) + //ServerAddrs Servers grouped with listen address + ServerAddrs = make(map[string][]*Server) + //ServerListeners list of listen objects + ServerListeners []*ServerListen + + // connections list of relay net.Conn/tls.Conn ojects + connections = make(map[string]*RelayConn) + // connectionsRWMu read write lock for the Connections map + connectionsRWMu sync.RWMutex + + slog *relaylogger.RelayLog +) + +func (srv *Server) disconnected(addr string) { + s := strings.Split(addr, utils.JoinString) + if len(s) < 3 { + slog.Error( + fmt.Errorf("error from disconnected callback invalid key"), + addr, + ) + return + } + remoteAddr := s[2] + if remoteAddr != "" { + deleteConnections(remoteAddr) + } +} + +// loadRelayServers from configuration +// This creates a server objects and add to +// Servers map with key as server name (*.DNS) +func loadRelayServers(config *ServerConfig) { + + slog.Info("Loading Servers form config") + + // Process config to prepare kubectl relay servers + for name, relay := range config.Relays { + provisioner, err := newAuthzProvisioner() + if err != nil { + slog.Error(err, "Unable to create provisioned") + return + } + Servers[relay.ServerName] = &Server{ + Type: utils.RELAY, + Name: name, + ServerName: relay.ServerName, + RootCA: relay.RootCA, + ServerCRT: relay.ServerCRT, + ServerKEY: relay.ServerKEY, + Protocol: relay.Protocol, + DialinServerName: relay.DialinSfx, + httpServer: &http.Server{}, + Provisioner: provisioner, + auditPath: config.AuditPath, + } + + if len(ServerAddrs[relay.Addr]) > 0 { + // check protocol + if ServerAddrs[relay.Addr][0].Protocol == relay.Protocol { + ServerAddrs[relay.Addr] = append(ServerAddrs[relay.Addr], Servers[relay.ServerName]) + } else { + // all servers listening on same address should have same + // protocol + slog.Info( + "Protocol missmatch for same addresses in relay", + "name", name, + "address", relay.Addr, + "protocol", relay.Protocol, + "expected", ServerAddrs[relay.Addr][0].Protocol, + ) + } + } else { + ServerAddrs[relay.Addr] = append(ServerAddrs[relay.Addr], Servers[relay.ServerName]) + } + } + + // Process config to prepare cd relay servers + for name, relay := range config.CDRelays { + Servers[relay.ServerName] = &Server{ + Type: utils.CDRELAY, + Name: name, + ServerName: relay.ServerName, + RootCA: relay.RootCA, + ServerCRT: relay.ServerCRT, + ServerKEY: relay.ServerKEY, + Protocol: relay.Protocol, + DialinServerName: relay.DialinSfx, + auditPath: config.AuditPath, + } + + if len(ServerAddrs[relay.Addr]) > 0 { + // check protocol + if ServerAddrs[relay.Addr][0].Protocol == relay.Protocol { + ServerAddrs[relay.Addr] = append(ServerAddrs[relay.Addr], Servers[relay.ServerName]) + } else { + // all servers listening on same address should have same + // protocol + slog.Info( + "Protocol missmatch for same addresses in relay", + "name", name, + "address", relay.Addr, + "protocol", relay.Protocol, + "expected", ServerAddrs[relay.Addr][0].Protocol, + ) + } + } else { + ServerAddrs[relay.Addr] = append(ServerAddrs[relay.Addr], Servers[relay.ServerName]) + } + } + + // Process config to prepare servers + for name, dialin := range config.Dialins { + s := &Server{ + Type: utils.DIALIN, + Name: name, + ServerName: dialin.ServerName, + RootCA: dialin.RootCA, + ServerCRT: dialin.ServerCRT, + ServerKEY: dialin.ServerKEY, + Protocol: dialin.Protocol, + } + + t := &http2.Transport{} + pool := newDialinPool(t, s.disconnected, slog) + t.ConnPool = pool + s.DialinPool = pool + s.httpClient = &http.Client{ + Transport: t, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + Servers[dialin.ServerName] = s + + if len(ServerAddrs[dialin.Addr]) > 0 { + // check protocol + if ServerAddrs[dialin.Addr][0].Protocol == dialin.Protocol { + ServerAddrs[dialin.Addr] = append(ServerAddrs[dialin.Addr], Servers[dialin.ServerName]) + } else { + slog.Info( + "Protocol missmatch for same addresses in dilain", + "name", name, + "address", dialin.Addr, + "protocol", dialin.Protocol, + "expected", ServerAddrs[dialin.Addr][0].Protocol, + ) + } + } else { + ServerAddrs[dialin.Addr] = append(ServerAddrs[dialin.Addr], Servers[dialin.ServerName]) + } + } + + slog.Debug( + "Loaded Servers", + "Servers", Servers, + ) + + slog.Debug( + "Loaded ServerAddrs", + "ServerAddrs", ServerAddrs, + ) + + // Prepare listener list + for name, srvlst := range ServerAddrs { + var listenAddr string + + // override server listern address with relayIP if present. + // This is for running mutliple instance of relay in the same + // development host. + if utils.RelayIPFromConfig == "" { + listenAddr = name + } else { + listenAddr = utils.RelayIPFromConfig + } + + srvlistener := &ServerListen{ + Addr: listenAddr, + Protocol: srvlst[0].Protocol, + ServerList: srvlst, + } + + // set the list cert/key paires for this listener + for _, srv := range srvlst { + snicert := utils.SNICertificate{ + CertFile: srv.ServerCRT, + KeyFile: srv.ServerKEY, + } + srvlistener.Certs = append(srvlistener.Certs, snicert) + // set the rootCA list for this listener + srvlistener.RootCAs = append(srvlistener.RootCAs, string(srv.RootCA)) + } + + ServerListeners = append(ServerListeners, srvlistener) + } + + slog.Info( + "Loaded server ", + "num-listeners", len(ServerListeners), + ) +} + +func (srv *Server) connectRequest(key string, msg *utils.ControlMessage, r io.Reader) (*http.Request, error) { + req, err := http.NewRequest(http.MethodPut, srv.DialinPool.URL(key), r) + if err != nil { + return nil, fmt.Errorf("could not create request: %s", err) + } + // write message to header + utils.WriteToHeader(req.Header, msg) + + return req, nil +} + +//dialinLookup check for cluster connection +func dialinCountLookup(sni string) int { + for _, dsrv := range Servers { + if dsrv.Type == utils.DIALIN { + // look dialin pool to find dilain connection key + cnt, err := dsrv.DialinPool.GetDialinConnectorCount(sni) + if err == nil { + return cnt + } + } + } + return 0 +} + +//ProcessPeerForwards ... +func (srv *Server) ProcessPeerForwards(w http.ResponseWriter, r *http.Request, lg *relaylogger.RelayLog, relayIP string, certIssue int64) { + + lg.Debug("ProcessPeerForwards:", relayIP, r.TLS.ServerName) + _, port, err := net.SplitHostPort(relayIP) + if err != nil { + port = "443" + } + + tlscfg, err := ClientTLSConfigFromBytes(srv.ServerCRT, srv.ServerKEY, srv.RootCA, r.TLS.ServerName+":"+port) + if err != nil { + lg.Error( + err, + "Failed to process peer configs", + ) + return + } + //setsup the upgradeaware peer handler + peerHandler, err := proxy.PeerKubeHandler(tlscfg, relayIP) + if err != nil { + lg.Error( + err, + "unable to create peer handler", + ) + return + } + + utils.SetXForwardedFor(r.Header, r.RemoteAddr) + //Update the UUID in the header to detect loops + utils.SetXRAYUUID(r.Header) + + r.Header.Set("X-Rafay-User-Cert-Issued", strconv.FormatInt(certIssue, 10)) + + // set peer security upstream headers + err = utils.PeerSetHeaderNonce(r.Header) + if err != nil { + lg.Error( + err, + "unable to set security headers in peer upstream", + ) + return + } + + //upgradeaware upstreaming + peerHandler.ServeHTTP(w, r) + lg.Debug( + "done Serving peerHandler.ServeHTTP ", + ) +} + +// ProcessCDPeerForwards ... +func (srv *Server) ProcessCDPeerForwards(ctx context.Context, conn net.Conn, lg *relaylogger.RelayLog, relayIP string, state tls.ConnectionState) { + var peerAddr string + lg.Debug("ProcessCDPeerForwards:", relayIP, state.ServerName) + _, port, err := net.SplitHostPort(relayIP) + if err != nil { + port = "443" + peerAddr = relayIP + ":443" + } + + tlscfg, err := ClientTLSConfigFromBytes(srv.ServerCRT, srv.ServerKEY, srv.RootCA, state.ServerName+":"+port) + if err != nil { + lg.Error( + err, + "Failed to process peer configs", + ) + return + } + + pconn, err := tls.Dial("tcp", peerAddr, tlscfg) + if err != nil { + lg.Error( + err, + "failed to connect to peer", + "addr", peerAddr, + ) + return + } + defer pconn.Close() + + utils.Transfer(flushWriter{pconn, lg, pconn}, conn, lg, "client to cdpeer") + +processCDPeerForwardDone: + for { + select { + case <-ctx.Done(): + break processCDPeerForwardDone + + } + } + + // wait 2 sec before closing + time.Sleep(2 * time.Second) + return +} + +//ProcessRelayRequest process user-facing request +func (srv *Server) ProcessRelayRequest(w http.ResponseWriter, r *http.Request, lg *relaylogger.RelayLog) { + var ( + rafayUserName string + sessionKey string + clusterServerName string + clusterID string + dialinSNI string + certIssue int64 + dialinAttempt int + ok bool + session *sessions.UserSession + ) + + srvlog := lg.WithName("RelayRequest") + srvlog.Debug( + "processing relay request", + ) + + // prepare the dilain lookup key + if r.TLS.ServerName == srv.ServerName { + srvlog.Error( + nil, + "Wildcard ServerName is expected", + "ServerName", srv.ServerName, + "expected SNI", "*."+srv.ServerName, + ) + errStirng := "ERROR: Unauthenticated access not allowed. Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "invalid cert in kubeconfig", http.StatusUnauthorized) + return + } + srvlog.Debug("ProcessRelayRequest", "r.TLS.ServerName", r.TLS.ServerName, "srv.ServerName", srv.ServerName) + + strs := strings.Split(r.TLS.ServerName, strings.ReplaceAll(srv.ServerName, "*", "")) + if len(strs) <= 0 { + srvlog.Error( + nil, + "ServeName is not the suffix of SNI", + "ServerName", srv.ServerName, + "SNI", r.TLS.ServerName, + ) + errStirng := "ERROR: Unauthenticated access not allowed. Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "invalid cert in kubeconfig", http.StatusUnauthorized) + return + } + + if len(r.TLS.PeerCertificates) <= 0 { + srvlog.Error( + nil, + "did not find any peer certificates in the request", + ) + errStirng := "ERROR: Unauthenticated access not allowed. Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "invalid cert in kubeconfig", http.StatusUnauthorized) + return + } + + if r.TLS.PeerCertificates[0].Subject.CommonName == "" { + srvlog.Error( + nil, + "cerficate CN is empty", + ) + errStirng := "ERROR: Unauthenticated access not allowed. Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "invalid cert in kubeconfig", http.StatusUnauthorized) + return + } + + issueDate := r.TLS.PeerCertificates[0].NotBefore + certIssue = issueDate.Unix() + srvlog.Debug("certificate usse epoch in secs", certIssue) + + sessionKey = "" + session = nil + //check the request is from a peer relay + if r.Header.Get("X-Rafay-XRAY-RELAYUUID") == "" { + srvlog.Debug( + "procesing user request common name from cert", + "CN", r.TLS.PeerCertificates[0].Subject.CommonName, + ) + + //User name is extracted from client certificate CN + rafayUserName = r.TLS.PeerCertificates[0].Subject.CommonName + sessionKey = r.TLS.ServerName + ":" + rafayUserName + session, ok = sessions.GetUserSession(sessionKey) + if !ok { + //Create session for the user + session = &sessions.UserSession{ + Type: srv.Type, + ServerName: srv.ServerName, + CertSNI: r.TLS.ServerName, + DialinCachedKey: "", + ErrorFlag: false, + } + sessions.AddUserSession(session, sessionKey) + srvlog.Info( + "Created new session", + "key", sessionKey, + ) + } else { + srvlog.Debug( + "Found existing session", + "key", sessionKey, + "dialin sticky key", session.DialinCachedKey, + ) + } + + clusterID = strs[0] + dialinSNI = strings.ReplaceAll(srv.DialinServerName, "*", strs[0]) + dialinAttempt = 0 + } else { + var err error + // to protect from a bad actor injecting rogue headers we need + // to validate the request tls parametes to make sure the client cert + // has expcted values. + + // 1. using a shared 256 key aes encryption blob added with a nonce + // validate the decrypted text using shared key produces expected result + // to make sure no tampering in headers + /* + if utils.CheckRelayLoops(r.Header) { + var allIds string + + if uuidHdr, ok := r.Header["X-Rafay-XRAY-RELAYUUID"]; ok { + allIds = strings.Join(uuidHdr, ", ") + } + srvlog.Error( + fmt.Errorf("LOOP detected in peerforwards"), + "failed peer loop detection check RelayUUID", utils.RelayUUID, + "header X-Rafay-XRAY-RELAYUUID allIds", allIds, + ) + } + */ + if !utils.CheckPeerHeaders(r.Header) { + srvlog.Error( + fmt.Errorf("failed to validate the peer upstream security header"), + "failed peer upstream due to secuity reasons", + ) + errStirng := "ERROR: Unauthenticated access not allowed . Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "failed to validate request in peer proxy", http.StatusUnauthorized) + return + } + + rafayUserName = r.Header.Get("X-Rafay-User") + clusterServerName = r.Header.Get("X-Rafay-Cluster-ServerName") + clusterID = r.Header.Get("X-Rafay-Cluster-ID") + issDateSecStr := r.Header.Get("X-Rafay-User-Cert-Issued") + if issDateSecStr == "" { + srvlog.Error( + fmt.Errorf("peer did not send a valid user cert issue date header"), + "failed peer upstream due to missing cert issuedate header", + ) + } + certIssue, err = strconv.ParseInt(issDateSecStr, 10, 64) + if err != nil { + srvlog.Error( + fmt.Errorf("peer did not send a valid user cert issue date header"), + "failed peer upstream due to missing cert issuedate header", + ) + } + + srvlog.Debug( + "forwarded request from peer", + "rafayUserName", rafayUserName, + "clusterServerName", clusterServerName, + "clusterID", clusterID, + "certIssue:", certIssue, + ) + + if rafayUserName == "" || clusterServerName == "" || clusterID == "" { + srvlog.Error( + nil, + "Did not find required headers", + "X-Rafay-User", rafayUserName, + "X-Rafay-Cluster-Name", clusterServerName, + "X-Rafay-Cluster-ID", clusterID, + ) + errStirng := "ERROR: Unauthenticated access not allowed (kubeconfig invalid cert). Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "failed to find user/cluster details", http.StatusUnauthorized) + return + } + + sessionKey = clusterServerName + ":" + rafayUserName + session, ok = sessions.GetUserSession(sessionKey) + if !ok { + //Create session for the user + session = &sessions.UserSession{ + Type: srv.Type, + ServerName: srv.ServerName, + CertSNI: clusterServerName, + DialinCachedKey: "", + ErrorFlag: false, + } + sessions.AddUserSession(session, sessionKey) + srvlog.Info( + "Created new session", + "key", sessionKey, + ) + } else { + srvlog.Debug( + "Found existing session", + "key", sessionKey, + "dialin sticky key", session.DialinCachedKey, + ) + } + + //prifixName is the clusterid/uuid for the endpoint + dialinSNI = strings.ReplaceAll(srv.DialinServerName, "*", clusterID) + dialinAttempt = 0 + } + r.Header.Set("X-Rafay-Audit", "yes") + +retryDialin: + // get the dialin server instance + if dsrv, ok := Servers[srv.DialinServerName]; ok { + if session.DialinCachedKey == "" { + // look dialin pool to find dilain connection key + key, err := dsrv.DialinPool.GetDialinConnectorKey(dialinSNI) + if err != nil { + srvlog.Info( + "No dialins", + "error", err, + "dialinAttempt", dialinAttempt, + ) + // Lookup peerCache to fetch cluster connection + // On cache-miss send the probe, wait and retry 5 times + relayIP, found := peerclient.GetPeerCache(utils.PeerCache, dialinSNI) + if found { + // forward the client request to peer upstream + r.Header.Set("X-Rafay-User", rafayUserName) + r.Header.Set("X-Rafay-Cluster-ServerName", r.TLS.ServerName) + r.Header.Set("X-Rafay-Cluster-ID", clusterID) + r.Header.Del("X-Rafay-Audit") + srv.ProcessPeerForwards(w, r, lg, relayIP, certIssue) + return + } else { + if dialinAttempt == 0 { + go SendPeerProbe(PeerProbeChanel, dialinSNI) + } + + dialinAttempt++ + if dialinAttempt > 7 { + errStirng := "ERROR: failed to forward request to cluster. Please retry" + jsonError(w, errStirng, "failed to find connection", http.StatusInternalServerError) + return + } + // wait for total ~20 sec to learn from core + // else drop the connection + time.Sleep(3 * time.Second) + goto retryDialin + } + } else { + //cache it + session.DialinCachedKey = key + } + } else { + //verify cache is valid + if !dsrv.DialinPool.CheckDialinKeyExist(session.DialinCachedKey) { + session.DialinCachedKey = "" + goto retryDialin + } + } + + srvlog.Debug( + "dialin connection lookup", + "key", session.DialinCachedKey, + "sessionKey", sessionKey, + ) + + //unix socket for stiching user request to dialin + socketPath := utils.UNIXSOCKET + srv.DialinServerName + + if session.UserName == "" { + userName, roleName, isRead, isOrgAdmin, enforceOrgAdminSecret, err := srv.Provisioner.ProvisionAuthzForUser(socketPath, rafayUserName, r.TLS.ServerName, session.DialinCachedKey, session.ErrorFlag, false, certIssue) + if err != nil { + var errStirng string + srvlog.Error( + err, + "unable to provision authz for user", + ) + if userName == "" { + errStirng = "ERROR: Unauthenticated access not allowed. Please log in to the portal via browser, or set up API key, for access via the secure kubectl proxy. Error:" + err.Error() + } else { + errStirng = "ERROR: Connection timed-out. Unable to provision cluster RBAC. Please retry." + } + jsonError(w, errStirng, "unable to proxy kubectl service", http.StatusUnauthorized) + session.DialinCachedKey = "" + sessions.DeleteUserSession(sessionKey) + return + } + session.UserName = userName + session.IsReadrole = isRead + session.RoleName = roleName + session.IsOrgAdmin = isOrgAdmin + session.EnforceOrgAdminOnlySecret = enforceOrgAdminSecret + } else { + go func() { + _, roleName, isRead, isOrgAdmin, enforceOrgAdminSecret, err := srv.Provisioner.ProvisionAuthzForUser(socketPath, rafayUserName, r.TLS.ServerName, session.DialinCachedKey, session.ErrorFlag, true, certIssue) + if err != nil { + srvlog.Error( + err, + "unable to provision authz for user", + ) + session.UserName = "" + } + session.IsReadrole = isRead + session.RoleName = roleName + session.IsOrgAdmin = isOrgAdmin + session.EnforceOrgAdminOnlySecret = enforceOrgAdminSecret + }() + } + + //check readrole vs pod exec + if session.IsReadrole { + if sessions.GetRoleCheck(r.Method, r.URL.Path) { + errStirng := fmt.Sprintf("ERROR: request forbidden for the role %s.", session.RoleName) + jsonError(w, errStirng, "unable to authorize user request", http.StatusUnauthorized) + return + } + } + + if session.EnforceOrgAdminOnlySecret && !session.IsOrgAdmin { + if sessions.GetSecretRoleCheck(r.Method, r.URL.Path) { + errStirng := "ERROR: request for secret resource is forbidden. Contact your organization admin" + jsonError(w, errStirng, "unable to authorize user request to secret", http.StatusUnauthorized) + return + } + } + + //setsup the upgradeaware unix handler + unixHandler, err := proxy.UnixKubeHandler(socketPath, session.DialinCachedKey, rafayUserName, r.TLS.ServerName) + if err != nil { + srvlog.Error( + err, + "unable to create unix handler", + ) + errStirng := "ERROR: failed to forward request to cluster. Please retry" + jsonError(w, errStirng, "unable to create forward handler", http.StatusInternalServerError) + return + } + + //These are the headers used by relay-agent to fetch + //user information. + r.Header.Set("X-Rafay-User", session.UserName) + r.Header.Set("X-Rafay-Key", session.DialinCachedKey) + r.Header.Set("X-Rafay-Namespace", "rafay-system") + r.Header.Set("X-Rafay-Sessionkey", sessionKey) + utils.SetXForwardedFor(r.Header, r.RemoteAddr) + utils.SetXRAYUUID(r.Header) + if session.ErrorFlag { + r.Header.Set(utils.HeaderClearSecret, "true") + } + + //upgradeaware uinix upstreaming + unixHandler.ServeHTTP(w, r) + srvlog.Debug( + "done Serving unixHandler.ServeHTTP ", + ) + if session.ErrorFlag { + session.DialinCachedKey = "" + sessions.DeleteUserSession(sessionKey) + } + } +} + +func jsonError(w http.ResponseWriter, message, reason string, code int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + resp := &kubeError{Kind: "Status", APIVersion: "v1", Metadata: nil, Status: "Failure", Message: message, Reason: reason, Code: code} + e, err := json.Marshal(resp) + if err == nil { + fmt.Fprintln(w, string(e)) + return + } + fmt.Fprintln(w, message+" "+reason) +} + +//ServeHTTP requests from userfacing connection +func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + servelog := slog.WithName("RelayServeHTTP") + servelog.Debug( + "start serving user http", + ) + + if len(r.TLS.PeerCertificates) <= 0 { + servelog.Error( + nil, + "no client certificate in request", + ) + errStirng := "ERROR: Unauthenticated access not allowed. Please log in to the portal and download new kubeconfig" + jsonError(w, errStirng, "invalid cert in kubeconfig", http.StatusUnauthorized) + return + } + + //client certificate + servelog.Debug( + "describe client certificate", + "ServerName", r.TLS.ServerName, + "Peer CN", r.TLS.PeerCertificates[0].Subject.CommonName, + "Peer Serial", r.TLS.PeerCertificates[0].Subject.SerialNumber, + "NegotiatedProtocol", r.TLS.NegotiatedProtocol, + "NegotiatedProtocolIsMutual", r.TLS.NegotiatedProtocolIsMutual, + ) + + switch srv.Type { + case utils.RELAY: + srv.ProcessRelayRequest(w, r, servelog) + default: + servelog.Error( + nil, + "unexpected server type", + "ServerName", srv.ServerName, + "type", srv.Type, + ) + } + +} + +//AddToDialinPool add connection to dialin pool of the server +func (srv *Server) AddToDialinPool(rconn *RelayConn, remoteAddr string) (string, error) { + slog.Info( + "AddToDialinPool", + "identifier", rconn.PeerID, + "ServerName", rconn.ServerName, + "CertSNI", rconn.CertSNI, + ) + + key, err := srv.DialinPool.AddConn(rconn.Conn, rconn.PeerID, rconn.CertSNI, remoteAddr) + if err != nil { + slog.Error( + err, + "adding connection failed", + "server", srv.ServerName, + ) + return "", err + } + + slog.Info( + "Added conn to pool of", + "server", srv.ServerName, + ) + + return key, nil +} + +func getSNIMuxDebugString(tmp []byte) string { + dstr := "" + for i := range tmp { + if i >= len(tmp) { + break + } + b := tmp[i] + if b < 32 || b > 126 { + dstr = dstr + " . " + } else { + dstr = dstr + " " + string(b) + } + } + return dstr +} + +//StartHTTPSListen start TLS listen on address +//Both user & dialin endpoint listen on 443 +//Based on SNI traffic is routed/muxed to appropriate handler +func (sl *ServerListen) StartHTTPSListen(ctx context.Context) { + slistenlog := slog.WithName("Listener") + slistenlog.Info( + "starting listener", + "addr", sl.Addr, + "protocol", sl.Protocol, + "num-servers", len(sl.ServerList), + ) + + l, err := net.Listen("tcp", sl.Addr) + if err != nil { + slistenlog.Error( + err, + "failed to listen", + "addr", sl.Addr, + ) + return + } + + //close the listener when ctx is done + go func() { + defer l.Close() + + <-ctx.Done() + slistenlog.Info( + "stopping listener", + "address", sl.Addr, + ) + }() + + sl.Mux, err = vhost.NewTLSMuxer(l, utils.DefaultMuxTimeout) + if err != nil { + slistenlog.Error( + err, + "failed in NewTLSMuxer", + ) + return + } + + for name, srv := range Servers { + slistenlog.Info( + "start mux listen on ", + "sni", name, + ) + snil, err := sl.Mux.Listen(name) + if err != nil { + slistenlog.Error( + err, + "failed in sl.Mux.Listen", + "sni", name, + ) + return + } + //listen routine for sni (name) + go srv.sniListen(ctx, name, snil, sl) + + if srv.Type == utils.DIALIN && utils.Mode != utils.CDRELAY { + //start the unix socket listen for each + //dialin server block + go srv.startUnixListen(ctx, slistenlog) + } + } + + // custom error handler for the mux + go func() { + for { + conn, err := sl.Mux.NextError() + vhostName := "" + tlsConn, ok := conn.(*vhost.TLSConn) + dbgStr := "" + if conn != nil { + tmp := make([]byte, 256) + n, _ := conn.Read(tmp) + if n > 0 { + dbgStr = getSNIMuxDebugString(tmp) + } + } + + if ok { + vhostName = tlsConn.Host() + slistenlog.Error( + nil, + "vhostName", vhostName, + "raddr", conn.RemoteAddr(), + "laddr", conn.LocalAddr(), + "peekbuf", dbgStr, + ) + } else { + slistenlog.Error( + nil, + "error in getting tlsConn", + "raddr", conn.RemoteAddr(), + "laddr", conn.LocalAddr(), + "peekbuf", dbgStr, + ) + } + + switch err.(type) { + case vhost.BadRequest: + slistenlog.Info( + "got a bad request!", + "addr", conn.RemoteAddr(), + "tlsConn", tlsConn, + "err", err, + ) + case vhost.NotFound: + slistenlog.Error( + err, + "got a connection for an unknown vhost", + "addr", vhostName, + "tlsConn", tlsConn, + ) + case vhost.Closed: + slistenlog.Error( + err, + "vhost closed conn", + "addr", vhostName, + "tlsConn", tlsConn, + ) + } + + if conn != nil { + slistenlog.Debug( + "closing connection", + "raddr", conn.RemoteAddr(), + "laddr", conn.LocalAddr(), + ) + conn.Close() + } + } + }() +} + +func (srv *Server) proxyUnixConnection(ctx context.Context, conn net.Conn, lg *relaylogger.RelayLog) { + var pmsg utils.ProxyProtocolMessage + + defer conn.Close() + conn.SetReadDeadline(time.Now().Add(utils.DefaultTimeout)) + //first utils.ProxyProtocolSize bytes reserved for proxy protocol like buffer + buf := make([]byte, utils.ProxyProtocolSize) + n, err := io.ReadAtLeast(conn, buf, utils.ProxyProtocolSize) + if err != nil { + lg.Error( + err, + "failed in unix listen to read", + "size", utils.ProxyProtocolSize, + "srv", srv.ServerName, + ) + } + + lg.Debug( + "unix listen read proxy protocol info", + "msglen", n, + "message", string(buf), + ) + + if n == utils.ProxyProtocolSize { + n = bytes.IndexByte(buf, 0) + err = json.Unmarshal(buf[:n], &pmsg) + if err != nil { + lg.Error( + err, + "failed to parse json", + "msg", string(buf[:n]), + ) + return + } + } else { + return + } + + if !srv.DialinPool.CheckDialinKeyExist(pmsg.DialinKey) { + lg.Error( + nil, + "did not find key in the dilain pool", + "key", pmsg.DialinKey, + ) + return + } + + pr, pw := io.Pipe() + defer pr.Close() + defer pw.Close() + + msg := &utils.ControlMessage{ + Action: utils.ActionProxy, + ForwardedHost: pmsg.SNI, + ForwardedService: srv.Name, + RafayUserName: pmsg.UserName, // for testing only + RafayNamespace: "default", // for testing only + RafayScope: "default", // for testing only + RafayAllow: "true", // for testing only + } + + req, err := srv.connectRequest(pmsg.DialinKey, msg, pr) + if err != nil { + lg.Error( + err, + "failed in srv.connectRequest", + ) + return + } + + nctx, cancel := context.WithCancel(ctx) + req = req.WithContext(nctx) + + done := make(chan struct{}) + go func() { + utils.Transfer(flushWriter{pw, lg, conn}, conn, lg, "unix to tunnel") + cancel() + close(done) + }() + + resp, err := srv.httpClient.Do(req) + if err != nil { + lg.Error( + err, + "io error:", + ) + return + } + defer resp.Body.Close() + + utils.Transfer(flushWriter{conn, lg, conn}, resp.Body, lg, "tunnel to unix") + +proxyUnixConnectionDone: + for { + select { + case <-nctx.Done(): + break proxyUnixConnectionDone + case <-ctx.Done(): + break proxyUnixConnectionDone + case <-done: + break proxyUnixConnectionDone + case <-time.After(300 * time.Second): + lg.Error( + fmt.Errorf("proxyUnixConnection waited 5 min for idle client to close"), + "break the loop to avoid memory exhaustion", + ) + break proxyUnixConnectionDone + } + } + + // wait 2 sec before closing + time.Sleep(2 * time.Second) +} + +func (srv *Server) proxyCDDilainConnection(ctx context.Context, conn net.Conn, dialinKey, rafayUserName, sni string, lg *relaylogger.RelayLog) { + defer conn.Close() + conn.SetReadDeadline(time.Now().Add(utils.DefaultTimeout)) + + pr, pw := io.Pipe() + defer pr.Close() + defer pw.Close() + + msg := &utils.ControlMessage{ + Action: utils.ActionProxy, + ForwardedHost: sni, + ForwardedService: srv.ServerName, + RafayUserName: rafayUserName, // for testing only + RafayNamespace: "default", // for testing only + RafayScope: "default", // for testing only + RafayAllow: "true", // for testing only + } + + req, err := srv.connectRequest(dialinKey, msg, pr) + if err != nil { + lg.Error( + err, + "failed in srv.connectRequest", + ) + return + } + + nctx, cancel := context.WithCancel(ctx) + req = req.WithContext(nctx) + + done := make(chan struct{}) + go func() { + utils.Transfer(flushWriter{pw, lg, conn}, conn, lg, "client to tunnel") + cancel() + close(done) + }() + + resp, err := srv.httpClient.Do(req) + if err != nil { + lg.Error( + err, + "io error:", + ) + return + } + defer resp.Body.Close() + + utils.Transfer(flushWriter{conn, lg, conn}, resp.Body, lg, "tunnel to client") + +proxyCDDilainConnectionDone: + for { + select { + case <-nctx.Done(): + break proxyCDDilainConnectionDone + case <-ctx.Done(): + break proxyCDDilainConnectionDone + case <-done: + break proxyCDDilainConnectionDone + case <-time.After(300 * time.Second): + lg.Error( + fmt.Errorf("proxyCDDilainConnection waited 5 min for idle client to close"), + "break the loop to avoid memory exhaustion", + ) + break proxyCDDilainConnectionDone + } + } + + // wait 2 sec before closing + time.Sleep(2 * time.Second) +} + +//start the unix socket that listens for connections to +//stich to a dialin. User request are handled here +func (srv *Server) startUnixListen(ctx context.Context, lg *relaylogger.RelayLog) { + //start listen the dialin unix pipe + socketpath := utils.UNIXSOCKET + srv.ServerName + syscall.Unlink(socketpath) + ul, err := net.Listen("unix", socketpath) + if err != nil { + lg.Error( + err, + "couldn't listen to", + "socketpath", socketpath, + ) + } + + //close the listener when ctx is done + go func() { + defer ul.Close() + + <-ctx.Done() + lg.Info( + "stopping listener", + "address", socketpath, + ) + }() + + lg.Info( + "started unix listen", + "socketpath", socketpath, + ) + + uctx, cancel := context.WithCancel(ctx) + defer cancel() + // Wait for a connection and accept it + for { + conn, err := ul.Accept() + + if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") || + strings.Contains(err.Error(), "Listener closed") { + lg.Error( + err, + "unix listener closed", + "srv", srv.ServerName, + ) + return + } + lg.Error( + err, + "Accept failed ", + "srv", srv.ServerName, + ) + continue + } + go srv.proxyUnixConnection(uctx, conn, lg) + } +} + +//sni based muxing (virtual hosting) handler that get called based on SNI +func (srv *Server) sniListen(ctx context.Context, sni string, l net.Listener, sl *ServerListen) { + var tlsconfig *tls.Config + var err error + + snilistenlog := slog.WithName("SNIListen") + + // prepare TLS configuration for the server + if srv.Type == utils.RELAY { + tlsconfig, err = ServerTLSConfigFromBytes(sl.Certs, sl.RootCAs, "http/1.1") + } else if srv.Type == utils.CDRELAY { + tlsconfig, err = ServerTLSConfigFromBytes(sl.Certs, sl.RootCAs) + } else if srv.Type == utils.DIALIN { + tlsconfig, err = ServerTLSConfigFromBytes(sl.Certs, sl.RootCAs) + } else { + snilistenlog.Error(nil, "unknown server type ", srv.Type, "addr", sl.Addr) + return + } + + if err != nil { + snilistenlog.Error( + err, + "failed to create tlsconfig", + "addr", sl.Addr, + ) + return + } + + if srv.Type == utils.RELAY { + //User facing server + srv.httpServer.TLSConfig = tlsconfig + srv.httpServer.Handler = http.HandlerFunc(srv.ServeHTTP) + + snilistenlog.Debug( + "starting srv.httpServer.ServeTLS", + "sni", sni, + "name", srv.ServerName, + "addr", sl.Addr, + ) + + srv.httpServer.Handler = audit.WrapWithAudit( + srv.httpServer.Handler, + audit.WithBasePath(srv.auditPath), + audit.WithMaxSizeMB(1), + audit.WithMaxBackups(20), + audit.WithMaxAgeDays(10), + ) + + //start TLS handshake + srv.httpServer.ServeTLS(l, "", "") + } + + if srv.Type == utils.CDRELAY { + for { + // accept connections + conn, err := l.Accept() + if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + snilistenlog.Error( + err, + "control connection listener closed", + "addr", sl.Addr, + ) + return + } + + snilistenlog.Error( + err, + "accept of connection failed", + "addr", sl.Addr, + ) + continue + } + + // handler for the accepted CD connections + go srv.handleCDRelayConnection(ctx, conn, tlsconfig) + } + } + + if srv.Type == utils.DIALIN { + for { + // accept connections + conn, err := l.Accept() + if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + snilistenlog.Error( + err, + "control connection listener closed", + "addr", sl.Addr, + ) + return + } + + snilistenlog.Error( + err, + "accept of connection failed", + "addr", sl.Addr, + ) + continue + } + + // handler for the accepted connection + go srv.handleDialinConnection(conn, tlsconfig) + } + } + + snilistenlog.Error( + nil, + "unknown service type ", + "type", srv.Type, + ) +} + +func (srv *Server) sendDialinHandshake(key string, lg *relaylogger.RelayLog) error { + var hMsg handShakeMsg + + sndlog := lg.WithName("sendDialinHandshake") + + sndlog.Debug( + "send dialin handhshake", + "key", key, + "httpClient.Transport", srv.httpClient.Transport, + "pool", srv.DialinPool, + ) + + req, err := http.NewRequest(http.MethodConnect, srv.DialinPool.URL(key), nil) + if err != nil { + sndlog.Error( + err, + "handshake request creation failed", + ) + return err + } + + { + ctx, cancel := context.WithTimeout(context.Background(), utils.DefaultTimeout) + defer cancel() + req = req.WithContext(ctx) + } + + resp, err := srv.httpClient.Do(req) + if err != nil { + sndlog.Error( + err, + "handshake failed", + ) + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("Status %s", resp.Status) + sndlog.Error( + err, + "dialin handshake response is not 200 OK", + "resp.StatusCode", resp.StatusCode, + ) + return err + } + + if resp.ContentLength == 0 { + err = fmt.Errorf("Tunnels Content-Legth: 0") + sndlog.Error( + err, + "dialin handshake response has 0 content length", + ) + return err + } + + if err = json.NewDecoder(&io.LimitedReader{R: resp.Body, N: 126976}).Decode(&hMsg); err != nil { + sndlog.Error( + err, + "dialin handshake failed to parse json", + ) + return err + } + + sndlog.Info( + "recieved handshake message", + "hMsg", hMsg, + ) + + return nil +} + +func (srv *Server) processCDRelayTLSState(ctx context.Context, conn net.Conn, lg *relaylogger.RelayLog, tlsconfig *tls.Config) (*Server, error) { + var ( + rafayUserName string + sessionKey string + dialinSNI string + dialinAttempt int + ok bool + session *sessions.UserSession + ) + + tlsConn, ok := conn.(*tls.Conn) + if !ok { + slog.Error( + nil, + "invalid connection type", + "err", fmt.Errorf("expected TLS conn, got %T", conn), + ) + return nil, fmt.Errorf("expected TLS conn, got %T", conn) + } + + // perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + slog.Error( + err, + "failed to compete TLS handshake", + ) + return nil, fmt.Errorf("ailed to compete TLS handshake") + } + + if err := conn.SetDeadline(time.Time{}); err != nil { + slog.Error( + err, + "failed to SetDeadline", + ) + return nil, fmt.Errorf("failed to SetDeadline") + } + + state := tlsConn.ConnectionState() + + _, err := GetRemoteCertID(tlsConn) + if err != nil { + slog.Error( + err, + "error in getting client cert id", + ) + return nil, fmt.Errorf("error in getting client cert id") + } + + // Handshake completed + // prepare the dilain lookup key + if state.ServerName == srv.ServerName { + slog.Error( + nil, + "Wildcard ServerName is expected", + "ServerName", srv.ServerName, + "expected SNI", "*."+srv.ServerName, + ) + return nil, fmt.Errorf("ERROR: Unauthenticated access not allowed") + } + + slog.Debug("processCDRelayTLSState", "ServerName", state.ServerName, "srv.ServerName", srv.ServerName) + + strs := strings.Split(state.ServerName, strings.ReplaceAll(srv.ServerName, "*", "")) + if len(strs) <= 0 { + slog.Error( + nil, + "ServeName is not the suffix of SNI", + "ServerName", srv.ServerName, + "SNI", state.ServerName, + ) + return nil, fmt.Errorf("ERROR: Unauthenticated access not allowed") + } + + if len(state.PeerCertificates) <= 0 { + slog.Error( + nil, + "did not find any peer certificates in the request", + ) + return nil, fmt.Errorf("ERROR: Unauthenticated access not allowed") + } + + if state.PeerCertificates[0].Subject.CommonName == "" { + slog.Error( + nil, + "cerficate CN is empty", + ) + return nil, fmt.Errorf("ERROR: Unauthenticated access not allowed") + } + + sessionKey = "" + session = nil + rafayUserName = state.PeerCertificates[0].Subject.CommonName + sessionKey = state.ServerName + ":" + rafayUserName + session, ok = sessions.GetUserSession(sessionKey) + if !ok { + //Create session for the user + session = &sessions.UserSession{ + Type: srv.Type, + ServerName: srv.ServerName, + CertSNI: state.ServerName, + DialinCachedKey: "", + ErrorFlag: false, + } + sessions.AddUserSession(session, sessionKey) + slog.Info( + "Created new session", + "key", sessionKey, + ) + } else { + slog.Debug( + "Found existing session", + "key", sessionKey, + "dialin sticky key", session.DialinCachedKey, + ) + } + + clusterID := strs[0] + dialinSNI = strings.ReplaceAll(srv.DialinServerName, "*", clusterID) + dialinAttempt = 0 + +retryCDDialin: + // get the dialin server instance + if dsrv, ok := Servers[srv.DialinServerName]; ok { + if session.DialinCachedKey == "" { + // look dialin pool to find dilain connection key + key, err := dsrv.DialinPool.GetDialinConnectorKey(dialinSNI) + if err != nil { + slog.Info( + "No dialins", + "error", err, + "dialinAttempt", dialinAttempt, + ) + // Lookup peerCache to fetch cluster connection + // On cache-miss send the probe, wait and retry 5 times + relayIP, found := peerclient.GetPeerCache(utils.PeerCache, dialinSNI) + if found { + // forward the client request to peer upstream + srv.ProcessCDPeerForwards(ctx, conn, lg, relayIP, state) + return srv, nil + } + if dialinAttempt == 0 { + go SendPeerProbe(PeerProbeChanel, dialinSNI) + } + + dialinAttempt++ + if dialinAttempt > 7 { + return nil, fmt.Errorf("ERROR: no dialns in both local and peers") + } + // wait for total ~20 sec to learn from core + // else drop the connection + time.Sleep(3 * time.Second) + goto retryCDDialin + + } else { + //cache it + session.DialinCachedKey = key + } + } else { + //verify cache is valid + if !dsrv.DialinPool.CheckDialinKeyExist(session.DialinCachedKey) { + session.DialinCachedKey = "" + goto retryCDDialin + } + } + + slog.Info( + "dialin connection lookup", + "key", session.DialinCachedKey, + "sessionKey", sessionKey, + ) + + session.UserName = rafayUserName + + //dilain proxy + dsrv.proxyCDDilainConnection(ctx, conn, session.DialinCachedKey, rafayUserName, state.ServerName, slog) + } else { + slog.Info( + "did not find dialin server name ", + "srv.DialinServerName", srv.DialinServerName, + ) + } + + return srv, nil +} + +func (srv *Server) processDialinTLSState(conn net.Conn, lg *relaylogger.RelayLog, tlsconfig *tls.Config) (*Server, error) { + tlsConn, ok := conn.(*tls.Conn) + if !ok { + slog.Error( + nil, + "invalid connection type", + "err", fmt.Errorf("expected TLS conn, got %T", conn), + ) + return nil, fmt.Errorf("expected TLS conn, got %T", conn) + } + + // perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + slog.Error( + err, + "failed to compete TLS handshake", + ) + return nil, fmt.Errorf("ailed to compete TLS handshake") + } + + if err := conn.SetDeadline(time.Time{}); err != nil { + slog.Error( + err, + "failed to SetDeadline", + ) + return nil, fmt.Errorf("failed to SetDeadline") + } + + // Handshake completed, add to Connections map; key is remoteAddr + if !isConnectionExist(conn.RemoteAddr().String()) { + var rconn *RelayConn + state := tlsConn.ConnectionState() + + identifier, err := GetRemoteCertID(tlsConn) + if err != nil { + slog.Error( + err, + "error in getting client cert id", + ) + return nil, fmt.Errorf("error in getting client cert id") + } + + rconn = &RelayConn{ + Conn: conn, + Type: srv.Type, + ServerName: srv.ServerName, + CertSNI: state.ServerName, + PeerID: identifier, + server: srv, + } + + addConnection(conn.RemoteAddr().String(), rconn) + + if rconn.Type == utils.DIALIN { + // add to dialin pool + key, err := srv.AddToDialinPool(rconn, conn.RemoteAddr().String()) + if err != nil { + deleteConnections(conn.RemoteAddr().String()) + return nil, err + } + // send init handshake + err = srv.sendDialinHandshake(key, slog) + if err != nil { + // handshake failed; delete added connection + srv.DialinPool.DeleteConn(rconn.PeerID, rconn.CertSNI, conn.RemoteAddr().String()) + deleteConnections(conn.RemoteAddr().String()) + return nil, fmt.Errorf("failed to send dialin handshake") + } + } else { + return nil, fmt.Errorf("unknown service type") + } + + return srv, nil + } + + return nil, fmt.Errorf("connection already exist %s", conn.RemoteAddr().String()) + +} + +func (srv *Server) handleCDRelayConnection(ctx context.Context, conn net.Conn, tlsconfig *tls.Config) { + defer conn.Close() + var vhostName string + hlog := slog.WithName("handleDialinConnection") + + vhostName = "" + tlsVConn, ok := conn.(*vhost.TLSConn) + if ok { + vhostName = tlsVConn.Host() + } + + if vhostName == "" { + slog.Error( + nil, + "vhost name is empty", + ) + return + } + + // prcess the TLS layer + _, err := srv.processCDRelayTLSState(ctx, tls.Server(conn, tlsconfig), hlog, tlsconfig) + if err != nil { + hlog.Error( + err, + "failed to process TLS state", + ) + return + } + +} + +func (srv *Server) handleDialinConnection(conn net.Conn, tlsconfig *tls.Config) { + var vhostName string + hlog := slog.WithName("handleDialinConnection") + + vhostName = "" + tlsVConn, ok := conn.(*vhost.TLSConn) + if ok { + vhostName = tlsVConn.Host() + } + + if vhostName == "" { + slog.Error( + nil, + "vhost name is empty", + ) + conn.Close() + return + } + + // prcess the TLS layer + _, err := srv.processDialinTLSState(tls.Server(conn, tlsconfig), hlog, tlsconfig) + if err != nil { + hlog.Error( + err, + "failed to process TLS state", + ) + conn.Close() + return + } + +} + +func cleanServerName(s string) string { + return strings.ReplaceAll(s, "*", "star") +} + +// StartServer starts server +func StartServer(ctx context.Context, log *relaylogger.RelayLog, auditPath string, exitChan chan<- bool) { + var config *ServerConfig + var err error + slog = log.WithName("Server") + + config = &ServerConfig{} + config.Relays = make(map[string]*Relay) + config.Relays["kubectl"] = &Relay{ + Protocol: "https", + Addr: fmt.Sprintf(":%d", utils.RelayUserPort), + ServerName: utils.RelayUserHost, + DialinSfx: utils.RelayConnectorHost, + RootCA: utils.RelayUserCACert, + ServerCRT: utils.RelayUserCert, + ServerKEY: utils.RelayUserKey, + } + config.Dialins = make(map[string]*Dialin) + config.Dialins["kubectl"] = &Dialin{ + Protocol: "https", + Addr: fmt.Sprintf(":%d", utils.RelayConnectorPort), + ServerName: utils.RelayConnectorHost, + RootCA: utils.RelayConnectorCACert, + ServerCRT: utils.RelayConnectorCert, + ServerKEY: utils.RelayConnectorKey, + } + config.AuditPath = auditPath + + utils.PeerCache, err = peerclient.InitPeerCache(nil) + if err != nil { + slog.Error( + err, + "failed to init peer cache", + ) + return + } + + loadRelayServers(config) + + err = sessions.InitUserSessionCache() + if err != nil { + slog.Error( + err, + "failed to init user session cache", + ) + return + } + + if err := proxy.InitUnixCacheRoundTripper(); err != nil { + slog.Error( + err, + "failed to init unix cached round tripper", + ) + return + } + + if err := proxy.InitPeerCacheRoundTripper(); err != nil { + slog.Error( + err, + "failed to init unix cached round tripper", + ) + return + } + + // Start listeners + for _, listener := range ServerListeners { + switch listener.Protocol { + case utils.HTTPS: + go listener.StartHTTPSListen(ctx) + default: + slog.Error( + nil, + "unknown protocol", + "listener.Protocol", listener.Protocol, + "Addr", listener.Addr, + ) + } + } + + go StartPeeringMgr(ctx, log, exitChan, config) + + StartDialinPoolMgr(ctx, log, exitChan) + + for { + select { + case <-ctx.Done(): + slog.Error( + ctx.Err(), + "stoping Server", + ) + return + } + } +} + +// StartCDServer starts server +func StartCDServer(ctx context.Context, log *relaylogger.RelayLog, auditPath string, exitChan chan<- bool) { + var config *ServerConfig + var err error + slog = log.WithName("Server") + + config = &ServerConfig{} + config.CDRelays = make(map[string]*Relay) + config.CDRelays["cdrelay"] = &Relay{ + Protocol: "https", + Addr: fmt.Sprintf(":%d", utils.CDRelayUserPort), + ServerName: utils.CDRelayUserHost, + DialinSfx: utils.CDRelayConnectorHost, + RootCA: utils.CDRelayUserCACert, + ServerCRT: utils.CDRelayUserCert, + ServerKEY: utils.CDRelayUserKey, + } + config.Dialins = make(map[string]*Dialin) + config.Dialins["cdrelay"] = &Dialin{ + Protocol: "https", + Addr: fmt.Sprintf(":%d", utils.CDRelayConnectorPort), + ServerName: utils.CDRelayConnectorHost, + RootCA: utils.CDRelayConnectorCACert, + ServerCRT: utils.CDRelayConnectorCert, + ServerKEY: utils.CDRelayConnectorKey, + } + config.AuditPath = auditPath + + utils.PeerCache, err = peerclient.InitPeerCache(nil) + if err != nil { + slog.Error( + err, + "failed to init peer cache", + ) + return + } + + loadRelayServers(config) + + err = sessions.InitUserSessionCache() + if err != nil { + slog.Error( + err, + "failed to init user session cache", + ) + return + } + + if err := proxy.InitUnixCacheRoundTripper(); err != nil { + slog.Error( + err, + "failed to init unix cached round tripper", + ) + return + } + + if err := proxy.InitPeerCacheRoundTripper(); err != nil { + slog.Error( + err, + "failed to init unix cached round tripper", + ) + return + } + + // Start listeners + for _, listener := range ServerListeners { + switch listener.Protocol { + case utils.HTTPS: + go listener.StartHTTPSListen(ctx) + default: + slog.Error( + nil, + "unknown protocol", + "listener.Protocol", listener.Protocol, + "Addr", listener.Addr, + ) + } + } + + go StartPeeringMgr(ctx, log, exitChan, config) + + StartDialinPoolMgr(ctx, log, exitChan) + + for { + select { + case <-ctx.Done(): + slog.Error( + ctx.Err(), + "stoping Server", + ) + return + } + } +} + +type flushWriter struct { + w io.Writer + lg *relaylogger.RelayLog + c net.Conn +} + +func (fw flushWriter) Write(p []byte) (n int, err error) { + n, err = fw.w.Write(p) + fw.lg.Debug( + "data wrote", + "len", len(p), + "wrote", n, + "data", string(p), + "err", err, + ) + if f, ok := fw.w.(http.Flusher); ok { + f.Flush() + } + // tolerate max read/write idle timeout + fw.c.SetDeadline(time.Now().Add(utils.IdleTimeout)) + return +} + +func isConnectionExist(key string) bool { + connectionsRWMu.RLock() + defer connectionsRWMu.RUnlock() + _, ok := connections[key] + return ok +} + +func addConnection(key string, rconn *RelayConn) { + connectionsRWMu.Lock() + defer connectionsRWMu.Unlock() + connections[key] = rconn +} + +func deleteConnections(key string) { + connectionsRWMu.Lock() + defer connectionsRWMu.Unlock() + delete(connections, key) +} + +//DialinMetric for cluster connection +func DialinMetric(w http.ResponseWriter) { + for _, dsrv := range Servers { + if dsrv.Type == utils.DIALIN { + // look dialin pool to find dilain connection key + dsrv.DialinPool.GetDialinMetrics(w) + } + } +} diff --git a/components/relay/pkg/tunnel/server_config.go b/components/relay/pkg/tunnel/server_config.go new file mode 100644 index 0000000..148ecd5 --- /dev/null +++ b/components/relay/pkg/tunnel/server_config.go @@ -0,0 +1,43 @@ +package tunnel + +// Dialin defines a dialin. +type Dialin struct { + Protocol string + Addr string + ServerName string + RootCA []byte + ServerCRT []byte + ServerKEY []byte + Version string +} + +// Relay defines a relay. +type Relay struct { + Protocol string + Addr string + DialinSfx string + ServerName string + RootCA []byte + ServerCRT []byte + ServerKEY []byte + Version string +} + +// ControllerInfo defines controller info. +type ControllerInfo struct { + Addr string + PeerProbeSNI string + RootCA string + ClientCRT string + ClientKEY string +} + +// ServerConfig is the configuration for relay server +type ServerConfig struct { + RelayAddr string + Relays map[string]*Relay + CDRelays map[string]*Relay + Dialins map[string]*Dialin + Controller ControllerInfo + AuditPath string +} diff --git a/components/relay/pkg/utils/utils.go b/components/relay/pkg/utils/utils.go new file mode 100644 index 0000000..f0aa8bf --- /dev/null +++ b/components/relay/pkg/utils/utils.go @@ -0,0 +1,713 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "net" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/RafaySystems/rcloud-base/components/relay/pkg/relaylogger" + "github.com/dgraph-io/ristretto" + "github.com/felixge/tcpkeepalive" + "github.com/google/uuid" +) + +// Known relay services +const ( + KUBECTL = "kubectl" + KUBEWEB = "kubeweb" + GENTCP = "tcp" + HTTPUP = "httpupgrade" +) + +// Known server types +const ( + RELAY = "relay" + CDRELAY = "cdrelay" + RELAYAGENT = "relay-agent" + CDRELAYAGENT = "cdrelay-agent" + DIALIN = "dialin" + JoinString = "--" +) + +// Relay Network Types +const ( + KUBECTLCORE = "rafay-core-relay-agent" + KUBECTLDEDICATED = "rafay-non-core-relay-agent" + CDAGENTCORE = "rafay-core-cd-relay-agent" +) + +//SNICertificate sni based certs +type SNICertificate struct { + CertFile []byte + KeyFile []byte +} + +//Relaynetwork configmap data +type Relaynetwork struct { + Token string `json:"token"` // bootstrap agent token + Addr string `json:"addr"` // bootstrap register host + Domain string `json:"endpoint"` // dialout domain + Name string `json:"name"` // network name + TemplateToken string `json:"templateToken"` // bootstrap template token + Upstream string `json:"upstream"` // upstream tcp service host:port +} + +//RelayNetworkConfig config for relay agent +type RelayNetworkConfig struct { + // Network configmap + Network Relaynetwork + // RelayAgentCert used for relay-agent client cert + RelayAgentCert []byte + // RelayAgentKey used for relay-agent client cert + RelayAgentKey []byte + // RelayAgentCACert used for relay-agent client cert + RelayAgentCACert []byte +} + +// Known protocol types. +const ( + HTTP = "HTTP" + HTTPS = "https" + TCP = "tcp" + UNIX = "unix" +) + +// Known dialin types +const ( + KUBECTLDILAIN = "kubectldialin" + KUBEWEBDIALIN = "kubewebdialin" + PEERKEY = "04112005676520746869732070617373776f726420746f206120736563726574" +) + +const ( + //HeaderError .. + HeaderError = "X-Error" + //HeaderAction ... + HeaderAction = "X-Action" + //HeaderForwardedHost .. + HeaderForwardedHost = "X-Forwarded-Host" + //HeaderForwardedService .. + HeaderForwardedService = "X-Forwarded-Service" + + //HeaderRafayUserName .. + HeaderRafayUserName = "X-Rafay-User" + //HeaderRafayNamespace .. + HeaderRafayNamespace = "X-Rafay-Namespace" + //HeaderRafayScope .. + HeaderRafayScope = "X-Rafay-Scope" + //HeaderRafayAllow .. + HeaderRafayAllow = "X-Rafay-Allow" + //HeaderRafayAuthZSA yaml contains service account + HeaderRafayAuthZSA = "X-Rafay-AuthzSA" + //HeaderRafayAuthZRole yaml contains role + HeaderRafayAuthZRole = "X-Rafay-AuthzRole" + //HeaderRafayAuthZRoleBinding yaml contains rolebinding + HeaderRafayAuthZRoleBinding = "X-Rafay-AuthzRoleBinding" + //HeaderRafayServiceAccountNoExpire don't expire service account + HeaderRafayServiceAccountNoExpire = "X-Rafay-ServiceAccount-NoExpire" + //HeaderClearSecret to clear the current secret cache of user + HeaderClearSecret = "X-Rafay-Clear-Cache" +) + +// Known actions. +const ( + ActionProxy = "proxy" + + // DefaultAuditPolicyPath default audit policy filter path + // k8s audit need a file path + DefaultAuditPolicyPath = "./relayaudit.yaml" + + //DefaultAuditPath defailt audit log files path + DefaultAuditPath = "-" // - means standard out + + //RafayRelayServiceAccountNameSpace namespace used to create service account for relays + RafayRelayServiceAccountNameSpace = "system-sa" +) + +var ( + // LogLevel loglevel set from commadline + LogLevel int + // Mode relay/relay-agent + Mode string + // ClusterID unique id of the cluster + ClusterID string + // AgentID unique id for cd agent + AgentID string + // ExitChan trigger this channel to exit + ExitChan = make(chan bool) + // TerminateChan trigger this channel to exit + TerminateChan = make(chan bool) + // IdleTimeout is the maximum amount of time to wait for the + // next read/write before closing connection. + IdleTimeout = 5 * time.Minute + // DefaultTimeout specifies a general purpose timeout. + DefaultTimeout = 5 * time.Minute + // DefaultPingTimeout specifies a ping timeout. + DefaultPingTimeout = 5 * time.Second + + // DefaultKeepAliveIdleTime specifies how long connection can be idle + // before sending keepalive message. + DefaultKeepAliveIdleTime = 15 * time.Second + + // DefaultKeepAliveCount specifies maximal number of keepalive messages + // sent before marking connection as dead. + DefaultKeepAliveCount = 3 + + // DefaultKeepAliveInterval specifies how often retry sending keepalive + // messages when no response is received. + DefaultKeepAliveInterval = 5 * time.Second + + //DefaultMuxTimeout specifies vmux timeout + DefaultMuxTimeout = 10 * time.Second + + //UNIXSOCKET prefix path for unix socket + UNIXSOCKET = "/tmp/relay-unix-" // need to change this from tmp to appropriate path after integration + + //UNIXAGENTSOCKET prefix path for unix socket + UNIXAGENTSOCKET = "/tmp/relay-agent-unix-" // need to change this from tmp to appropriate path after integration + + //ProxyProtocolSize Default PROXY PROTO buffer size + ProxyProtocolSize = 1024 + + //RelayUUID runtime Unique ID for relay + RelayUUID string + + //RelayIPFromConfig IP address of the relay for peering + RelayIPFromConfig string + + //PeerCache stores peer dialin info + PeerCache *ristretto.Cache + + //ServiceAccountCache stores service account, role, role binding in relay-agetn in connector + ServiceAccountCache *ristretto.Cache + + //ServiceAccountCacheDefaultExpiry default expiry + ServiceAccountCacheDefaultExpiry = 600 * time.Second + + //PeerCacheDefaultExpiry default expiry + PeerCacheDefaultExpiry = 600 * time.Second + //PeerHelloInterval heartbeat interval + PeerHelloInterval = 60 * time.Second + //PeerServiceURI is the URI to join peering service + PeerServiceURI string + //PeerCertificate used for peering service communication + PeerCertificate []byte + //PeerPrivateKey used for peering service communication + PeerPrivateKey []byte + //PeerCACertificate used for peering service communication + PeerCACertificate []byte + + //RelayUserCert used for user/peer communication + RelayUserCert []byte + //RelayUserKey used for user/peer communication + RelayUserKey []byte + //RelayUserCACert used for user/peer communication + RelayUserCACert []byte + + //RelayUserPort user facing seerver port + RelayUserPort int + // RelayUserHost user facing seerver host (domain) + RelayUserHost string + //RelayConnectorCert used for relay-connector termination + RelayConnectorCert []byte + //RelayConnectorKey used for relay-connector termination + RelayConnectorKey []byte + //RelayConnectorCACert used for relay-connector termination + RelayConnectorCACert []byte + // RelayConnectorHost connector facing server host (domain) + RelayConnectorHost string + // RelayConnectorPort connector facing server port + RelayConnectorPort int + + // CDRelayUserCert used for client/peer communication + CDRelayUserCert []byte + // CDRelayUserKey used for client/peer communication + CDRelayUserKey []byte + // CDRelayUserCACert used for client/peer communication + CDRelayUserCACert []byte + // CDRelayUserHost client facing server host + CDRelayUserHost string + // CDRelayUserPort client facing server port + CDRelayUserPort int + // CDRelayConnectorCert used for cd-relay-connector termination + CDRelayConnectorCert []byte + // CDRelayConnectorKey used for cd-relay-connector termination + CDRelayConnectorKey []byte + // CDRelayConnectorCACert used for cd-relay-connector termination + CDRelayConnectorCACert []byte + // CDRelayConnectorHost connector facing server host (domain) + CDRelayConnectorHost string + // CDRelayConnectorPort connector facing server port + CDRelayConnectorPort int + + //RelayNetworks list of relaynemtworks from configmap + RelayNetworks []Relaynetwork + // RelayAgentConfig map of relay agent configurations + RelayAgentConfig map[string]RelayNetworkConfig + + // MaxDials max connections dialed + MaxDials = 10 + + // MinDials minimum connections dialed + MinDials = 8 + + //PODNAME name of the pod + PODNAME string + + // DialoutProxy setting used while connecting to relay IP:PORT or HOST:PORT format + DialoutProxy = "" + + // DialoutProxyAuth Proxy-Authorization header base64 encoded value of user:password + DialoutProxyAuth = "" + + // DefaultTCPUpstream default TCP upstream + DefaultTCPUpstream = "127.0.0.1:16001" + + // ScalingStreamsThreshold concurrent streams count to trigger scaling + ScalingStreamsThreshold = 400 + + // ScalingStreamsRateThreshold new streams rate to trigger scaling + ScalingStreamsRateThreshold = 200 + + // MaxScaleMultiplier multiplier to limit max scaled connections + MaxScaleMultiplier = 3 + + // HealingInterval time to close idle scaled connection + HealingInterval = 24 // Hour +) + +//CountWriter to measure bytes +type CountWriter struct { + W io.Writer + Count int64 +} + +// ControlMessage is sent from server to client before streaming data. It's +// used to inform client about the data and action to take. Based on that client +// routes requests to backend services. +type ControlMessage struct { + Action string + ForwardedHost string + ForwardedService string + RemoteAddr string + RafayUserName string + RafayNamespace string + RafayScope string + RafayAllow string + RafayAuthz string +} + +// ProxyConfig configs for the proxy +type ProxyConfig struct { + Protocol string + Addr string + ServiceSNI string + RootCA string + ClientCRT string + ClientKEY string + Upstream string + UpstreamClientCRT string + UpstreamClientKEY string + UpstreamRootCA string + UpstreamSkipVerify bool + UpstreamKubeConfig string + Version string +} + +//ProxyProtocolMessage used across dialin unix socket +type ProxyProtocolMessage struct { + DialinKey string + UserName string + SNI string +} + +type ServiceAccountCacheObject struct { + RafayAuthzSA string + RafayAuthzRole string + RafayAuthzRoleBind string + Md5sum string + Key string +} + +//OnEvict cache on eviction call back function +type OnEvict = func(item *ristretto.Item) + +//Fatal to exit the program +func Fatal(format string, a ...interface{}) { + fmt.Fprintf(os.Stderr, format, a...) + fmt.Fprint(os.Stderr, "\n") + os.Exit(1) +} + +//CloneHeader clone http headers +func CloneHeader(h http.Header) http.Header { + h2 := make(http.Header, len(h)) + for k, vv := range h { + vv2 := make([]string, len(vv)) + copy(vv2, vv) + h2[k] = vv2 + } + return h2 +} + +//SetXForwardedFor ... +func SetXForwardedFor(h http.Header, remoteAddr string) { + clientIP, _, err := net.SplitHostPort(remoteAddr) + if err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := h["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + h.Set("X-Forwarded-For", clientIP) + } +} + +//SetXRAYUUID ... +func SetXRAYUUID(h http.Header) { + var reluuid string + + reluuid = RelayUUID + // If this isn't the first relay retain prior + // UUIDs information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := h["X-Rafay-XRAY-RELAYUUID"]; ok { + reluuid = strings.Join(prior, ", ") + ", " + reluuid + } + h.Set("X-Rafay-XRAY-RELAYUUID", reluuid) +} + +//SetXForwardedRafay set rafay headers +func SetXForwardedRafay(h http.Header, msg *ControlMessage) { + h.Set("X-Rafay-User", msg.RafayUserName) + h.Set("X-Rafay-Namespace", msg.RafayNamespace) + h.Set("X-Rafay-Scope", msg.RafayScope) + h.Set("X-Rafay-Allow", msg.RafayAllow) +} + +//UnSetXForwardedRafay set rafay headers +func UnSetXForwardedRafay(h http.Header) { + h.Del("X-Rafay-Scope") + h.Del("X-Rafay-Allow") + h.Del("X-Forwarded-For") + h.Del("X-Rafay-XRAY-RELAYUUID") + h.Del("X-Rafay-Audit") + h.Del("X-Rafay-Cluster-Id") + h.Del("X-Rafay-Cluster-Servername") + h.Del("X-Rafay-Peer-Hash") + h.Del("X-Rafay-Peer-Nonce") + h.Del("X-Rafay-Sessionkey") + h.Del("X-Rafay-User-Cert-Issued") +} + +//Transfer transfer by io.Copy +func Transfer(dst io.Writer, src io.Reader, tlog *relaylogger.RelayLog, direction string) { + n, err := io.Copy(dst, src) + if err != nil { + if !strings.Contains(err.Error(), "context canceled") && !strings.Contains(err.Error(), "CANCEL") && !strings.Contains(err.Error(), "i/o timeout") && !strings.Contains(err.Error(), "closed network") { + tlog.Error( + err, + "io.Copy error", + "direction", direction, + ) + } + } + + tlog.Debug( + "action transferred", + "bytes", n, + "in direction", direction, + ) +} + +// WriteToHeader writes ControlMessage to HTTP header. +func WriteToHeader(h http.Header, c *ControlMessage) { + h.Set(HeaderAction, string(c.Action)) + h.Set(HeaderForwardedHost, c.ForwardedHost) + h.Set(HeaderForwardedService, c.ForwardedService) + h.Set(HeaderRafayUserName, c.RafayUserName) + h.Set(HeaderRafayNamespace, c.RafayNamespace) + h.Set(HeaderRafayScope, c.RafayScope) + h.Set(HeaderRafayAllow, c.RafayAllow) +} + +func (cw *CountWriter) Write(p []byte) (n int, err error) { + n, err = cw.W.Write(p) + cw.Count += int64(n) + return +} + +//CopyHeader copy header +func CopyHeader(dst, src http.Header) { + for k, v := range src { + vv := make([]string, len(v)) + copy(vv, v) + dst[k] = vv + } +} + +// ReadControlMessage reads ControlMessage from HTTP headers. +func ReadControlMessage(r *http.Request) (*ControlMessage, error) { + msg := ControlMessage{ + Action: r.Header.Get(HeaderAction), + ForwardedHost: r.Header.Get(HeaderForwardedHost), + ForwardedService: r.Header.Get(HeaderForwardedService), + RafayUserName: r.Header.Get(HeaderRafayUserName), + RafayNamespace: r.Header.Get(HeaderRafayNamespace), + RafayScope: r.Header.Get(HeaderRafayScope), + RafayAllow: r.Header.Get(HeaderRafayAllow), + RemoteAddr: r.RemoteAddr, + } + + var missing []string + + if msg.Action == "" { + missing = append(missing, HeaderAction) + } + if msg.ForwardedHost == "" { + missing = append(missing, HeaderForwardedHost) + } + if msg.ForwardedService == "" { + missing = append(missing, HeaderForwardedService) + } + + if len(missing) != 0 { + return nil, fmt.Errorf("missing headers: %s", missing) + } + + return &msg, nil +} + +//FlushWriter flush writer +type FlushWriter struct { + W io.Writer +} + +func (fw FlushWriter) Write(p []byte) (n int, err error) { + n, err = fw.W.Write(p) + if f, ok := fw.W.(http.Flusher); ok { + f.Flush() + } + return +} + +//KeepAlive set keepalive +func KeepAlive(conn net.Conn) error { + return tcpkeepalive.SetKeepAlive(conn, DefaultKeepAliveIdleTime, DefaultKeepAliveCount, DefaultKeepAliveInterval) +} + +//GenUUID generates a google UUID +func GenUUID() { + id := uuid.New() + RelayUUID = id.String() +} + +//GetRelayIP get relay IP address +func GetRelayIP() string { + if RelayIPFromConfig == "" { + name, err := os.Hostname() + if err == nil { + addr, err := net.LookupIP(name) + if err == nil { + return addr[0].String() + } + } else { + return "" + } + } + return RelayIPFromConfig +} + +// GetRelayIPPort get relay IP:PORT of user facing server +func GetRelayIPPort() string { + if RelayIPFromConfig == "" { + return GetRelayIP() + ":" + strconv.Itoa(RelayUserPort) + } + return RelayIPFromConfig +} + +//CheckRelayLoops :does XRAY UUDI already present in header? +func CheckRelayLoops(h http.Header) bool { + if uuidHdr, ok := h["X-Rafay-XRAY-RELAYUUID"]; ok { + allIds := strings.Join(uuidHdr, ", ") + matched, err := regexp.Match(RelayUUID, []byte(allIds)) + if err == nil && matched { + return true + } + } + return false +} + +// InitCache initialize the cache to store dialin cluster-connection +// information of peers. When a dialin miss happens look into this cache +// to find the peer IP address to forward the user connection. +func InitCache(evict OnEvict) (*ristretto.Cache, error) { + return ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e7, // Num keys to track frequency of (10M). + MaxCost: 1 << 30, // Maximum cost of cache (1GB). + BufferItems: 64, // Number of keys per Get buffer. + OnEvict: evict, + }) +} + +// InsertCache inserts the value to cache +func InsertCache(cache *ristretto.Cache, expiry time.Duration, key, value interface{}) bool { + return cache.SetWithTTL(key, value, 100, expiry) +} + +// GetCache get value from cache +func GetCache(cache *ristretto.Cache, key interface{}) (interface{}, bool) { + return cache.Get(key) + +} + +// DeleteCache delete value from cache +func DeleteCache(cache *ristretto.Cache, key interface{}) { + cache.Del(key) +} + +//WriteFile overwrite if exist +func WriteFile(filename, data string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + + defer file.Close() + + file.WriteString(data) + return nil +} + +// validOptionalPort reports whether port is either an empty string +// or matches /^:\d*$/ +func validOptionalPort(port string) bool { + if port == "" { + return true + } + if port[0] != ':' { + return false + } + for _, b := range port[1:] { + if b < '0' || b > '9' { + return false + } + } + return true +} + +// SplitHostPort separates host and port. If the port is not valid, it returns +// the entire input as host, and it doesn't check the validity of the host. +func SplitHostPort(hostport string) (host, port string) { + host = hostport + + colon := strings.LastIndexByte(host, ':') + if colon != -1 && validOptionalPort(host[colon:]) { + host, port = host[:colon], host[colon+1:] + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + + return +} + +// IsHTTPS returns true if port is 443 +func IsHTTPS(addr string) bool { + _, port, err := net.SplitHostPort(addr) + if err == nil { + if port == "443" { + return true + } + } + return false +} + +// PeerSetHeaderNonce header +func PeerSetHeaderNonce(h http.Header) error { + key, _ := hex.DecodeString(PEERKEY) + + data := h.Get("X-Rafay-XRAY-RELAYUUID") + if data == "" { + return fmt.Errorf("no X-Rafay-XRAY-RELAYUUID header") + } + value := []byte(data) + + block, err := aes.NewCipher(key) + if err != nil { + return err + } + + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + + ciphertext := aesgcm.Seal(nil, nonce, value, nil) + hash := hex.EncodeToString(ciphertext) + + peerNonce := hex.EncodeToString(nonce) + h.Set("X-Rafay-Peer-Nonce", peerNonce) + h.Set("X-Rafay-Peer-Hash", hash) + return nil +} + +// CheckPeerHeaders validates upstreams request +func CheckPeerHeaders(h http.Header) bool { + key, _ := hex.DecodeString(PEERKEY) + + hash := h.Get("X-Rafay-Peer-Hash") + if hash == "" { + return false + } + + peerNonce := h.Get("X-Rafay-Peer-Nonce") + if peerNonce == "" { + return false + } + + expected := h.Get("X-Rafay-XRAY-RELAYUUID") + if expected == "" { + return false + } + + ciphertext, _ := hex.DecodeString(hash) + block, err := aes.NewCipher(key) + if err != nil { + return false + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return false + } + + nonce, _ := hex.DecodeString(peerNonce) + plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return false + } + + if string(plaintext) == expected { + return true + } + + return false +} diff --git a/components/relay/testdata/README.md b/components/relay/testdata/README.md new file mode 100644 index 0000000..c0c7c4d --- /dev/null +++ b/components/relay/testdata/README.md @@ -0,0 +1,89 @@ +# rafay-relay unit testing +k8s api server relay unit testing + + +## Environment +Please refer below image to setup your test environtment. You can run server and client in the same system. + +![](images/testsetup.png) + + +## Testing +Add entries in /etc/host or in your DNS-server for the relay-server ipaddress.\ +The User facing DNS is **cluster1.kubectl.relay.rafay.dev**\ +The Dialin facing DNS is **cluster1.kubectldialin.relay.rafay.dev**\ +For example; to run client and server in local system, the /etc/host will have entries like below. +``` +127.0.0.1 cluster1.kubectl.relay.rafay.dev +127.0.0.1 cluster1.kubectldialin.relay.rafay.dev +``` + +### Client System setup +Makesure $KUBECONFIG environment variable is set in the client system\ +(OR) +You can use testdata/kubectlconfigs/kubeconfig and customise it for your setup. +``` +export KUBECONFIG=testdata/kubectlconfigs/kubeconfig +``` + +### Cluster serviceaccount setup +Apply config pkg/proxy/testdata/role.yaml +``` +kubectl apply -f pkg/proxy/testdata/role.yaml +``` + +Run service using below commands +``` +sudo relay.exe -config ./testdata/server.yaml start server +``` +### Relay client for cluster1 +``` +relay.exe -config ./testdata/client-cluster1.yaml start client + +``` +### Relay client for cluster2 +``` +relay.exe -config ./testdata/client-cluster2.yaml start client + +``` + +### Using Relay for kubectl +In kubeconfig set server address to relay.\ +Relay uses MTLS to authenticate. Set `client-certificate` and `client-key` to cert signed by the relay service rootCA. For kubectl to validate the relay server set `certificate-authority`. Use `--insecure-skip-tls-verify` to skip the server validation. + +Examples: +``` +kubectl --kubeconfig testdata/kubectlconfigs/kubeconfigproxy-cluster1 get pods +kubectl --kubeconfig testdata/kubectlconfigs/kubeconfigproxy-cluster2 get pods +kubectl --kubeconfig testdata/kubectlconfigs/kubeconfigproxy-cluster1 exec -it demo-sa1 -- /bin/sh +kubectl --kubeconfig testdata/kubectlconfigs/kubeconfigproxy-cluster2 exec -it demo-sa1 -- /bin/sh +``` + +### Production +Relay server need following environment variables + +POD_NAME - name of the relay instaance (auto generated) +POD_NAMESPACE - namespace of the relay instance + +RAFAY_SENTRY - URL to boostrap with sentry service +RAFAY_RELAY_PEERSERVICE - To join gRPC with sentry peer service + + +RELAY_USER_DOMAIN - domain suffix for relay user facing server +RELAY_CONNECTOR_DOMAIN - domain suffix for relay connector facing domain + +RELAY_TOKEN - Peer Service Token bootstrapping +RELAY_USER_TOKEN - Relay user facing server bootstrapping +RELAY_CONNECTOR_TOKEN - Relay connector facing server bootstrapping + +Examples: +``` +export RAFAY_SENTRY=http://sentry.rafay.dev:9000 +export RAFAY_RELAY_PEERSERVICE=http://peering.sentry.rafay.local:7001 + +export RELAY_USER_DOMAIN="user.relay.rafay.local" +export RELAY_CONNECTOR_DOMAIN="core-connector.relay.rafay.local" + +export RELAY_TOKEN=bqfvhabipt3a2g46986g +export RELAY_USER_TOKEN=bqfvhabipt3a2g46987g +export RELAY_CONNECTOR_TOKEN=bqfvhabipt3a2g469870 diff --git a/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt new file mode 100644 index 0000000..abeb56a --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWjCCAkICCQD0kF4yo85tQzANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDEwOTE5MTUx +NVoXDTMwMDEwNjE5MTUxNVowgYAxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTET +MBEGA1UEBwwKUGxlYXNhbnRvbjEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0Rl +djExMC8GA1UEAwwocmVsYXlzZXJ2ZXIxLUFCQ0QtMTIzNDU2LnJlbGF5LnJhZmF5 +LmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN51AZQAYIoAAQUk +G7qFudNDMT03FO6j+ThIsqQzqXgHOCBDVMhaWs4qKJzMD+8vtnPHzz2t0ksYsnKj +YfO21C/kzbMz3yBZSyFA/eqEA/CLbyzECs+eaNHX7hDaVcHX22Rbi+W7zmjJE2LZ +5oxGoQKnSQFEn/0SsjKX4DdF+Rlw4TBgBuJ77tXnslZr+Hcgrt8dOApuWZ6v5PPN +aM72L1Gp1eP7ViUia8yc4AO2UjdO8iGSaDjy/mlWVLsK9Lb+/O5RGMmqcN+o6Mnd +U6WMOtVgkjfNeIZWJ0RdmEkR/JMyOmZauyqZiW7stOVhtOxfhE5b2N7VbqtT989q +OPZ/x38CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAng07c+Kn65mvGawiV3ZICuTy +cpfnlPEmpx4ZtDUsTIyRgaRkPpmPFkJee8kNPIrY68EeLmQB6oA976cx6+N9B61U +2oBG1HpbdtTOwvj9qid3B+syzDJXmgpEWczsNAptDqOfa/4jGzp53rORfljXnqBx +DwJhe5XI6ZJ0DLK1piNneq076qElylPgCV//tzyNn9mjaNCl64FjJyhER36UWEm7 +o4JCQkprppyocWs/NKq6ENsNsNPdWH+ZxiVy2eIGOGMk6zWu8VcCGaRDQDumkQT3 +yMh4DBfyCfbd8liOYLsLuYkGaz0Ew76TgMr875v4t53VtudNdREEqDkkLfNJA2hQ +WwgLylCSqmaWL8V10OvlLG+EnT0XEIkHbzRW5XYxs+U7saG6ze5wnpPNWDb8MYj6 +4bV6V4My5o7dejDBVzhk894MtOjdrhLWCUuZ0WcZvfIm4z5c6jWuKio3VB2hcVn+ +2rtLu7LFWhlmyiX2TtB3cx1gkGpYTllQGdWkmsQMR0uxVy1KVyiotrlK5oDBuoW/ +/XhQ1pKUL9gW1PeTrUexUJBxFP+MWONOuzarDi+fGNyDCh8wgkFkYotExGUiVxjT +9h/mc7fSVypygrWkZmtjb2gAJQRBH1GOu3fhr1MX8AXY3wtZXMWCDmoad02PZvrB +Rnx4d4QuHqlO8LsMvyo= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.csr b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.csr new file mode 100644 index 0000000..0ba4a6b --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICxjCCAa4CAQAwgYAxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTETMBEGA1UE +BwwKUGxlYXNhbnRvbjEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjExMC8G +A1UEAwwocmVsYXlzZXJ2ZXIxLUFCQ0QtMTIzNDU2LnJlbGF5LnJhZmF5LmRldjCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN51AZQAYIoAAQUkG7qFudND +MT03FO6j+ThIsqQzqXgHOCBDVMhaWs4qKJzMD+8vtnPHzz2t0ksYsnKjYfO21C/k +zbMz3yBZSyFA/eqEA/CLbyzECs+eaNHX7hDaVcHX22Rbi+W7zmjJE2LZ5oxGoQKn +SQFEn/0SsjKX4DdF+Rlw4TBgBuJ77tXnslZr+Hcgrt8dOApuWZ6v5PPNaM72L1Gp +1eP7ViUia8yc4AO2UjdO8iGSaDjy/mlWVLsK9Lb+/O5RGMmqcN+o6MndU6WMOtVg +kjfNeIZWJ0RdmEkR/JMyOmZauyqZiW7stOVhtOxfhE5b2N7VbqtT989qOPZ/x38C +AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQDYnQFv2aG7pN3idts3EkuIlBlfVGKM +V1gQjasdkdVlBVS0ehCRCXOzey3kgNtQiFF1YHZ/0uoq1abZjRCXg4EG5++mTuMk +yIB3Yng1bEJ8A4juRrWn2ihEul7sfPfEt0lfxqn2sqN5ghC1UpbcPh6T74mbIflq +pMdv2IDDpOzCNAOHSqjbJJTg1oCi+nMabXOtwn0pqsC1tiqROLtG8qWV1WlT59zb +9FxDeIhT/pbZNSlwJ7akJ7ykeIhrQdSQpnclvzum0/vO0aLoojQg0UH9ldEHGu0K +Pn+k1eX3xOD9KZn7qrgu0qZR0v+18brJ9jPTdxWKyMLhVmGX6Fzj4zIL +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key new file mode 100644 index 0000000..92c3e67 --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA3nUBlABgigABBSQbuoW500MxPTcU7qP5OEiypDOpeAc4IENU +yFpazioonMwP7y+2c8fPPa3SSxiycqNh87bUL+TNszPfIFlLIUD96oQD8ItvLMQK +z55o0dfuENpVwdfbZFuL5bvOaMkTYtnmjEahAqdJAUSf/RKyMpfgN0X5GXDhMGAG +4nvu1eeyVmv4dyCu3x04Cm5Znq/k881ozvYvUanV4/tWJSJrzJzgA7ZSN07yIZJo +OPL+aVZUuwr0tv787lEYyapw36joyd1TpYw61WCSN814hlYnRF2YSRH8kzI6Zlq7 +KpmJbuy05WG07F+ETlvY3tVuq1P3z2o49n/HfwIDAQABAoIBAFxDMPVeNqvlloa4 +VBvHDKwXSRu3daIINfvnAthhaN8UN24CHu69sr2g9JkbdVxhcI2SiFxvdDGbO5+a +Lazz3gtDHSE+2ULEZgmTt/wE5tFkMybg5A76B0VJBj7NHxY3XZHA+kO84b3J75Vn +BFyXin3cPcYd3TEMCQY8wUyLY+Xtii6yrA618H/27Dhe/F04/MHchMf1VlI2zQxI +/Yi3snIAl3YFRVb+TxQCd1LG3UvbafPRTc2zv9HiXQey7ahwbtzyikQwOZT0htJg +iTy72nJVHq5Iel1L0/gwD/0m91QrlZlTqbfkCvRZCKNgtaf2+wJ+/HWIoLF8SlqX +MO9DWEkCgYEA8kisxKhEoAVj4e2mQmuLSzc1zB+/kPtWCXSnc/gTvQPC2LMlwk8k +8IRVah4SvqdgLgXXCbARRatUj43yetPZXlKF45OXFs/39abpFyFWG2Dzcwmy1gYC +hHOFYieZWvinYs5mvBuG0qLnSpw0sTWsUvuJB+jp1M+W/6pvaw8O8fUCgYEA6wz9 +I3Q+E+KEz4o+P+PhxbFd6F3BRPIT6hDn/EOISuaZ/8Zo1t0fhzjH5TXL9ipeNKfF +VmNE4enKg3uNAYR0PbWFWZjLRn9pG9WIKh6ws28eTxjJ8RSlHJELXsc+cLFkXtKu +9Z3R3Fzw+j3JKTE8bXbu99WrzIv72QIu5tIbByMCgYEA3KgxqR1y+qZOFIqi/CUE +BKmwtkpH5tvwvMXgaC8Mkv2k7EUkY3Y/N0nenLns6nR3lRK+iNUwtdnXNrLZnuHo +Ow4/YEefuCgKNVEhQm6bBG5k6mVv2Zo5mrW3SfC0q3SAxe0jBbiwLpAsw/fFyYsP +ebgaUfjfVXEGnax0svVQ8C0CgYBt/P77NyrRTT8OiscESJNeWdfyouSMaabN8alM +jyFYwz2uxV5LpN9a/EP1ys2tXDgG9bbO7NbPzAG6svbRjfSC580Flw0P6RD7OFlm +NERFQqrxIk1k5V851dl3OU7Llwa7pGTAhGAueMM+pejawLJhGrVJ/Y9NwPdRncbq +tch19wKBgD1Ua1yiYWbKPP3l9btMwjKUJlD08+4JXCTSVKzKe/JclQ+fhnpBZZGS +58zgrUugfpdOnzavGxs9kKVskl3nNGtpGtuWyY8fSIU/qQQXEJRcCYpcgu8bMJNI +eAo8PrJR6BU9nGuKBBhEs+PWt1LiNaTLqU1GMMnwneqsrCwKPOlG +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/clientcerts/star.admin.crt b/components/relay/testdata/certs/clientcerts/star.admin.crt new file mode 100644 index 0000000..f9c0a02 --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.admin.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAhYCCQD0kF4yo85tTTANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDEyMzE3NDM0 +NloXDTMwMDEyMDE3NDM0NlowVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEOMAwGA1UE +AwwFYWRtaW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkOPicn8J1 +BQlrZLxAX815+T9IZC/Zb/KYv1VYo+7oRGpLivJbJEb7MRJkyqCN33/1bH4B4REv +56qF9LdLO0xMSErK9vXafqWis++HAPp1p7LSFmjRw75x7tKdzlUEkciqt3SBoxQj +wRwlgGBMHxqlJt/DhvQ8mxodxbdyWiOzU+gDHeexZqFrmoWe498ywU/l0ollBn5P +aaKPU39uaVcn6W2t0jkIioKKm8qiaA6bHAK3voflE/8+/6DVhVAjdacwidgHnS6z +xCSWGvENinXBN9zZBuL0mCxCKcOJGy1NGME+8IrCEJNI2MgL+hVFvCBXW+cgtSeI +imK/3ETfELHFAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAKr5EH4MBun6Hb1CQt7t +LqeDZ+J1KyvMV6AUPu8v3zReOG3YjQORurKxDeK/vI3QCrJLzrAUQodJM0eGEnRu +mG42UxUxZowKqg5uiEJVTMZEIOr8W1dpyQAEHlU7DbiarjnZAzdTSafquc0PU4uf +sBFK//lDluyVaetNC7f1Sn61Gh0piirvIXZW7z9K+HgheK3vz4r40RkjTDm3ZBUh +nPDU/DtkVJET/yM4VcdqggRr2XTah0CeDw6nUKB+gBw352q7upMI4KLkry/sjbQ1 +D151h2E1qkhG6gqnau+dmNcT6CKNVRQSAhLT4ULf4oc+IFvMhXi/I1Ghxqla2z3R +5DpIi9GF/efljKujshy3FrLLjCDZgUAl0qv936T5vZAQZ9SL/iY3cIUr9nOmcfFd +67lf040D3SJvELzEoS8bbCJ3VgBShSVc+TE5LQ0LC3DjdunWJgPAXmpKAkHhAM7P +ftU0jaLeLlonnV8xPYMRCsitb/3Xz43VImbAcrCVA6glu0bg/5CpEBS+2fIVpBVl +hKuVCKzDs5ei2q1H3f2ejOHy9PbCl8deAkhNwfk7RSsWaMk0CTU7WJXLindFQuQI +G2o6d0O4AQcQYlLaIlwCgY7O+HE9YBkfLMgiA28xtw2p4sDxuYzQBPuNt/FiT7ew +AL5fJTfMjVuWBjMr4XS84L+3 +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/clientcerts/star.admin.csr b/components/relay/testdata/certs/clientcerts/star.admin.csr new file mode 100644 index 0000000..794fcae --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.admin.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICmjCCAYICAQAwVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEOMAwGA1UEAwwFYWRt +aW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkOPicn8J1BQlrZLxA +X815+T9IZC/Zb/KYv1VYo+7oRGpLivJbJEb7MRJkyqCN33/1bH4B4REv56qF9LdL +O0xMSErK9vXafqWis++HAPp1p7LSFmjRw75x7tKdzlUEkciqt3SBoxQjwRwlgGBM +HxqlJt/DhvQ8mxodxbdyWiOzU+gDHeexZqFrmoWe498ywU/l0ollBn5PaaKPU39u +aVcn6W2t0jkIioKKm8qiaA6bHAK3voflE/8+/6DVhVAjdacwidgHnS6zxCSWGvEN +inXBN9zZBuL0mCxCKcOJGy1NGME+8IrCEJNI2MgL+hVFvCBXW+cgtSeIimK/3ETf +ELHFAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAmVSuFX+p7D/9tGfBgt9OKGn6 +N0xVc6d1gVf9ADrx1KEWjoqMnng2oe8yc5LXIfYHMZEIm9J0EigYsMEIUBDMer86 +CIXBdVN94H5gUQDIzaZ+M0+eU8vg9/K1GGFBkv6hBzV89Kps+1v3ykzsWlN7JcMM +HZYzUTrgeSAHiQ7CjVEjUMkcAkeF23YYdSsNcpDx1+8m/ptFaaaVzhGL/2XmvPjk +xCJDsZYX/DmHaHWcYRFUVxND9bDOEF7c6Gt+J9spHR7hUIDfeyADIlvItI2Rw4i7 +70Fg/JUCUG25KyD8BKSwBic6GAJ04g8+aE3d3FMRnPxc5iNdFJyRG/KGgNY0CQ== +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/clientcerts/star.admin.key b/components/relay/testdata/certs/clientcerts/star.admin.key new file mode 100644 index 0000000..7776b8c --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.admin.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5Dj4nJ/CdQUJa2S8QF/Nefk/SGQv2W/ymL9VWKPu6ERqS4ry +WyRG+zESZMqgjd9/9Wx+AeERL+eqhfS3SztMTEhKyvb12n6lorPvhwD6daey0hZo +0cO+ce7Snc5VBJHIqrd0gaMUI8EcJYBgTB8apSbfw4b0PJsaHcW3clojs1PoAx3n +sWaha5qFnuPfMsFP5dKJZQZ+T2mij1N/bmlXJ+ltrdI5CIqCipvKomgOmxwCt76H +5RP/Pv+g1YVQI3WnMInYB50us8QklhrxDYp1wTfc2Qbi9JgsQinDiRstTRjBPvCK +whCTSNjIC/oVRbwgV1vnILUniIpiv9xE3xCxxQIDAQABAoIBADiWj752NodBenZw +geUMXCyUSsauYNbvVR3ew1C5RTG+VQTqLPHAawE5x6/KNkk3U8kM36mjntlb/gI9 +DX3P9Dkt662r0oYcxQE00H2PFos5y7Mf71mjyYMCjqKmWJpLc5Aw8Oh8ATBkX5BE +5pu5UqgEnZMj0Ud9xdn//l6STyvVeGDQCEtLPn9AGZFuZL/HpXKQgr1nfmSA4gMI +tDdDumVVlJL6efEdKF4whgS+ejGqsyJ7oB1MHWemqVtA4WYn8J2YL+XRN4JwVRJh +Pu5aDvHFfrn+2k33nmGWVIlXFl75v1pOCzYv+Aa7Bxx4PMvyU7WZONsJ2xjiJhp2 +OLiP4LkCgYEA844zPjAI1WWVe4HViYjggJrQZl20D0JBy22wqw55eqGSWX1gON3k +d6RapaVyThKd660Qmsdemgjr0owY/0Fr8JnM2ybL4/0MG9jBynZ27vbyLG1od9gW +96ewn5F0oyT8HT0bicz4EZfVEeUD1sqCCRIriFSccR/qzjVjt8qY+rsCgYEA7+I1 +2oKqXSvyDxlexVsKKPXoH1fyTpk3/uU5esyDatQJ2t4xlZ0xeOcvh0WsAmHJeax1 +nNHlv6RabiBtiNME6XMXI6oCE4yYeJy7l3UAOTB568n1oIED9Phq/dkoMsjs3qx0 +qSxfhYY98LOPObiF73Iar5Qw0FPvR3alvvnNfX8CgYEA68u7LrTgiIFd1IIhk5MS +D1BS/RgxECjMfD/JMALGd4RfpQYuF0A0ReS34fEolg+OFrKqBb+JhkWm0mPSe6YB +86FjZLTN5v+BLqCPFg3VHYu1cG/IGWb67W/LHuF688wFzeTweVwYuyetIDMBoXzT +oiaWQNnxz8/fyx6DWVfq/asCgYASLjvUpsiQrS74U/zpyj+X4SNJlNnJeTVUPT1B +AFUJeK6ShvqNE0/XkPX6M56Yw56IeTikvQfDoYo1BIIot42Mv+tGCbGEl8hKg953 +2Sc+Ll1fMHxMQNry6wz41keEqdPCAKIK3FEVBpqVLpaxUjncbvJ00i9s6p+c8/IV +6krXswKBgFfQ1lc2ffE62AcXJofw+uaNZM2+FUbaDEcjcAR/qPuRR9pGCNtnGsa+ +CkTsatI7Ip3CrqBIwcqTvl8agt+I/Fn+Zcdp956uaqyyEYvTEetFbdvaFSqf+krF +YvOkAt3xy3Am7dmT8Qx5+FrXBkRDtfYk3xh0tAMZV1H1jiKq9qcN +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/clientcerts/star.default.crt b/components/relay/testdata/certs/clientcerts/star.default.crt new file mode 100644 index 0000000..58ab928 --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.default.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUTCCAjkCCQD0kF4yo85tTDANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExODE2MDA0 +NFoXDTMwMDExNTE2MDA0NFoweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEQMA4GA1UE +AwwHZGVmYXVsdDEfMB0GCSqGSIb3DQEJARYQZGVmYXVsdEByYWZheS5jbzCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALDzZVMm5TvFvMxMIryQGu/uTl84 +59lCgTEX6pr7H55OPg5/ZM5w/s66+qC3Teyw98i+X3PIywUizsnVkmylcLBRDLUY +/doo3Xp1KkTClasC2XeDIKl7zlNgIyqLLfRF0CkjdWsyFTpNi5NemezRDUBa9qXt +6EK5qKWL5SN3kelTZk3slOhfXyr/a2mEFMfqF6SYp2vZGBfF/h8mpb02TEivaTIN +haldIxDi0LaWXTqWfJf4UPCiHFtq78OGysA3LfFPfRvwe9WxQKbV7D88HC5H7EKA +gYjT7AFgSvl6vCRwj+DrCw3rkS6bUDrpOItp157n1KVaiQ1wnvi0oOcbI+MCAwEA +ATANBgkqhkiG9w0BAQsFAAOCAgEAPQg5nhu35IcKrzFw8CfmUqY+w0qvxteaaULr +KHXJ7Fm9qoD02q+V4xxysFm2dP2R1Q+LqSzLnVIJapNQMpeAqfl4yiPZpBdPIMZK +2LPTD/A4o/4mhADte7pUtZSc0XuuZ+ZAb7wQis9ujkUxJk5S+uy7++zK5Pvh4e3/ +b463riy8GyOIRKipgEyTeLFz/VpjkqadRUZY59N4pFA1Hr1EjZOfdiVLWKlVT1iV +JyWijo8+SL5bt8naj8fKdnQNqZkHZSZ4Ep0lDRCxeOfNyUG27xsSPoa1ixKh+N3x +ipB2ZWy/0k87vmLEMc4Zr13o/iLEhDp3y5NuG/djwtnzd81/RFvD7fPjf+nmAzdI +XQe2VARmm8KeYvAEsyawQnVxx4cx/N8M+8VlF8PXS/rZGt6W3+OW2R4ko6Bi2R2K +efb10HKFi9uhalumrmMH24XriH0hQS9cyeSRTsjjWr3HUNrFb6/uAOZCoEBqPHJh +ohL1dwUKcxdM+ygptqIopCLNapXFbZur60V93N8SCVu+BbK02yg9AbdbaZh4vdCO +pmiIIzTcgkirfn8Cbp22513x7P/AE0bxxZlAJB+dPpgT3c5QFLaS5ImSoDWRkhFH +s0NBmbBYF/PkfApItiOWQU5ZNNOfn2TO94MKJMY9nNYBWT9xT45ON/qo+k50/ean +zrh9Cy8= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/clientcerts/star.default.csr b/components/relay/testdata/certs/clientcerts/star.default.csr new file mode 100644 index 0000000..ed6b228 --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.default.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICvTCCAaUCAQAweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEQMA4GA1UEAwwHZGVm +YXVsdDEfMB0GCSqGSIb3DQEJARYQZGVmYXVsdEByYWZheS5jbzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBALDzZVMm5TvFvMxMIryQGu/uTl8459lCgTEX +6pr7H55OPg5/ZM5w/s66+qC3Teyw98i+X3PIywUizsnVkmylcLBRDLUY/doo3Xp1 +KkTClasC2XeDIKl7zlNgIyqLLfRF0CkjdWsyFTpNi5NemezRDUBa9qXt6EK5qKWL +5SN3kelTZk3slOhfXyr/a2mEFMfqF6SYp2vZGBfF/h8mpb02TEivaTINhaldIxDi +0LaWXTqWfJf4UPCiHFtq78OGysA3LfFPfRvwe9WxQKbV7D88HC5H7EKAgYjT7AFg +Svl6vCRwj+DrCw3rkS6bUDrpOItp157n1KVaiQ1wnvi0oOcbI+MCAwEAAaAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCYoAQUOBUab2qTkc7OHQU2LBBWgSbE8TZQYkqCL7HS +jm6g5Od9sQwKC9mA5BT+PNajnzJQ0CtHvkzALo+Ep+d9JQYtW7AJKbpCuewPe4GJ +4c3eLb7a2HQPnpMJP6ZL1h2qGraWs7+pBCi7hrZR1s/AfzpJ4uuAlhcCpvQK+FMX +AVMwSsu1PlBAFN7irX5qMga5MWUDEzgEJ27TMOov53OT/JU6LNHYizIpY+ZORsh2 +169EhaW3xfwtB45jIRnkQegmr8gPlqY9IEn2eH8dCKZrS41iKHiZ2IZbbu2wt8rp +/wxU7MCYm7tvw6O5oFQy04MRmEifloaBF9v8l5mNeWd7 +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/clientcerts/star.default.key b/components/relay/testdata/certs/clientcerts/star.default.key new file mode 100644 index 0000000..cc607dd --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.default.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsPNlUyblO8W8zEwivJAa7+5OXzjn2UKBMRfqmvsfnk4+Dn9k +znD+zrr6oLdN7LD3yL5fc8jLBSLOydWSbKVwsFEMtRj92ijdenUqRMKVqwLZd4Mg +qXvOU2AjKost9EXQKSN1azIVOk2Lk16Z7NENQFr2pe3oQrmopYvlI3eR6VNmTeyU +6F9fKv9raYQUx+oXpJina9kYF8X+HyalvTZMSK9pMg2FqV0jEOLQtpZdOpZ8l/hQ +8KIcW2rvw4bKwDct8U99G/B71bFAptXsPzwcLkfsQoCBiNPsAWBK+Xq8JHCP4OsL +DeuRLptQOuk4i2nXnufUpVqJDXCe+LSg5xsj4wIDAQABAoIBAFP0+BNu9f7ZQE4S +pbygBxfmEutdPaw4wMGBXn/wPL1I1GreiP7mYA/IaZgNwPxXrwlhQjrWf3VNYZ4k +JlhEWuZdXlEirXdwQj2TD94WERtVu8bXwYyIwImQqL7L0J8A7NZIC7MzzRyfpGTk +0tWpDseFIVfzPGtEWb9KVAry9ZklfUlj+sLiRBmFfDOJ6k+k7bSBG5BygQ7+OdSv +UEhXpqM7l3mJKnTHBtnWNYJphQ/NNhWyilL5e5DW13VlRdWbWjLyHP49YWoqkW/3 +xo5ETI5GXDSLaEtJZzaBZ0iJ+Wq6jHusVt1htvUWgRNyTJtvVaiB8S4AFBM/79IO +LSIPCQECgYEA55NxU3AHYG34AekzjbzL/OPVEYstA0PB9LyNazbgptHm+lMT1yc6 +7JC+Ggmrx+kgC8MsN9UOPaHWdrNeeDuBywLsqfoJ+9/h8q1RvcVcaiRzXgahgtB7 +tluEfCx5BOd888L3bLyP0LNPVR+kDk9qPokRC2S9DSP4Nz6iZc5UF4ECgYEAw50S +U7cWADShebYjzth5t3QE4SdzBg3sVfWgp6dfm7WD58cBF17TyB/C2NYru+k9rzyk +HodnluHvGPWTbfvvVsbuhR18CSGVevzGWDMhWHXe17qrDBFrCLBqkSENuDhh9gSJ +0FfCP3VQ4uTn2oSczZ7Nwd421h/sFYv3pT2kjWMCgYBK1EkFVij8DuzZ6TogbcZY +pGJANgHIkUeGRrtHzsbPAPqrHtdFacbPlIHpNgDNK1aQlnOeYvkkDzyWxKUdItVw +/TPuRP6e5hxD55+HO9P99GNXGbts4uNm7LGl0lUUsZydw/1hpJiwLpDPB9diTGUf +afkY/mdcbKgTaejeDsr7gQKBgFldRKYiGlb1tF9aKnmww8pfomsKDq5RZgrPMBnn +iPIltqEIdSMsGyxt9iIBlfE+2QNZfNDFozl29jg82KGCWxEVyJnfom65fQS2vhZF +JZK4Jpn/mWzl+DP8BHNQ2meyum4eHKelsAQ1oEJHuPRHaSs2xWEwF3UzGDOvwe8n +filpAoGBAL69XLGVL2StuZYFXJvIclEnOrcafCtQqpu50dZ0DxXczR8OG846rttj +3v9tcxGMaXrnBu/vOYGsvO6HuUU6ooidms1OrWnOyZTFxxH1xwv7xGlFPSb5mMAn +FvmSU/8uwkgOuIrxKGNxKKhbNM4pYINfCe9ODhGf5qaG87zy7Ofy +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt new file mode 100644 index 0000000..c87989d --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEOzCCAiMCCQD0kF4yo85tSjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExODE1MTU1 +MVoXDTMwMDExNTE1MTU1MVowYjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEbMBkGA1UE +AwwSbmFtZXNwYWNlLWFkbWluLXNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAwPyZqt6HNI1fCzhSNHvZP1ymqJJbYMyKTkVOvlIkEBMe4oWzvMZS6CUl +q54j00SSF8bTcwLj1o5MgREzPxoqK7zCGWknkwcP7k8wIQxpA2MTWBkBlQMPVfx7 +8KojIGtjP30rBAYoYbnuZS53rV7aJl1GrOfgi0FIB49iLw20iTZARk35ntR51nRs +s1iWD9+6+gB+kqDo4amTQDRs6TU8ltqE5bSSn1tXXn8jPP3oQX3Hkmb6ck71ztPb +VUoh2lfu04UGmJ0+2JJDEchdRWrdUmJmASLtZF7uRtdkEQfhJiULFZsLvzNbyrQZ +5WYLotpj+SYMam9YQi/Kh8heeXgKPQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCP +nMyXeFZ4pU0kdMClj3IUhr8NQfHBUVWtyy8tffWT4b7xLaUtTeKXrzSAuTGt9AFa +Eww4cO8leEt8ZqpJYoqoPsrGgxCFUannmsQoKYnHCT/+zo9KAY2I7rE4PffFcrFU +VmlSDgeeV7pybCXXIpVV2SU+264O96Eyykl13I1t2dMrFZ7F5oSU9wOYvLgfQw79 +qzwuE2EN1vM73pAhbnShjSOKE1DXnSaXNCsdbmcUcla/6W2przU1Xx/0TrNj4esT +Nsy5UYlf76XXQJisUryux0+htLPbUJgu5rcFj+dOakg3rI0+h1GeYSum1vhQGBeM +NwkSgA89Lwi5zaYPzc5SOx/IVfh341xtdJYdGF9DfB0ghVJ6Z/5BaQRW2H0jxIM6 +vrhr03387GO9l7R8q1gnoklqY6KkIHq43maqKTJ6ZV/sjV16Z9yn63+f6SViFCzF +bkXRZdypSqc8vOP8f2q/9Gbc5jPyIMVtJtDZlyHRQ93yashf6BFcP13iWutkdNbo +fZYt1ZzC0Kwn5SQDnKGmNEzPyl4/nW0RgDNP6hYyxcL9JVzN0MSyjwajNwhDbxpN +FKXnwhk1MMclGAcGnDZRoYbweuJS3x8ZoAP/pTrxMSGw60iwc7co54Ina3c+xHAW +Dd8ac41yljTr1Z0o2ppiE46ozcxT9sU0pKAO9bAdnQ== +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.csr b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.csr new file mode 100644 index 0000000..466e8da --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICpzCCAY8CAQAwYjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEbMBkGA1UEAwwSbmFt +ZXNwYWNlLWFkbWluLXNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +wPyZqt6HNI1fCzhSNHvZP1ymqJJbYMyKTkVOvlIkEBMe4oWzvMZS6CUlq54j00SS +F8bTcwLj1o5MgREzPxoqK7zCGWknkwcP7k8wIQxpA2MTWBkBlQMPVfx78KojIGtj +P30rBAYoYbnuZS53rV7aJl1GrOfgi0FIB49iLw20iTZARk35ntR51nRss1iWD9+6 ++gB+kqDo4amTQDRs6TU8ltqE5bSSn1tXXn8jPP3oQX3Hkmb6ck71ztPbVUoh2lfu +04UGmJ0+2JJDEchdRWrdUmJmASLtZF7uRtdkEQfhJiULFZsLvzNbyrQZ5WYLotpj ++SYMam9YQi/Kh8heeXgKPQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAI9SY580 +Th9Q4H3rUBMhdzVOW2f6Oyb4TPk0VBrVpOq2Puu50YW64UknAFz0beV5EkcjUjEo +Gib9SKLnDuQnJk90IbjivfAqrimT0bradbHXVoteuulftRzjhFBAnwrDD5Y1eSiU +mbKRDcp9IVPDFBhJrDgZhOoO/d7EeLETudXHNTTknxn4a9olYuAHupq+bsTif7A8 +DRWmCPC5BsNNK5w7nOC9ZcKCSvYbo0gLHzejivJl/GomYWpAXR8gV9xuJ5e3tVPx +5CcNRCYyepRiOYwjn0nUTJpRmRHjzm2vCx2s0Z9y8cOtwHBgEWLVrpCkWm+ik1as +EqfByEDkI4TVdKw= +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.key b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.key new file mode 100644 index 0000000..197369e --- /dev/null +++ b/components/relay/testdata/certs/clientcerts/star.namespace-admin-sa.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwPyZqt6HNI1fCzhSNHvZP1ymqJJbYMyKTkVOvlIkEBMe4oWz +vMZS6CUlq54j00SSF8bTcwLj1o5MgREzPxoqK7zCGWknkwcP7k8wIQxpA2MTWBkB +lQMPVfx78KojIGtjP30rBAYoYbnuZS53rV7aJl1GrOfgi0FIB49iLw20iTZARk35 +ntR51nRss1iWD9+6+gB+kqDo4amTQDRs6TU8ltqE5bSSn1tXXn8jPP3oQX3Hkmb6 +ck71ztPbVUoh2lfu04UGmJ0+2JJDEchdRWrdUmJmASLtZF7uRtdkEQfhJiULFZsL +vzNbyrQZ5WYLotpj+SYMam9YQi/Kh8heeXgKPQIDAQABAoIBAC9o3sdfS4/ZexPi +ji9ejYxHUpnu+ovZM9Durd2LiFh6mgfEGNezUQK+vXZfryVjnxBARAnpF9fJtXmv +pAtIfjuob3aWoqY4Ae3lfklgJxwx5rqpyf21drsE/92rrl10hDlAoqSmbp0zdCFf +PP9WYfkyyP4JKNyM+QkCbHhqwYNDZrZIJs7BATmsNoDQ0kieFYCvJlaZ3HRy9fpw +/HIWVtJryBNbZJXLaN58lbOoJRK9h3ipsqw8NdqDDtOQsJ1lPbQwO/+FvdW3v8Cz +1R4bGHls2ESESiaGbP9VgSt5nC0whmLyKePOHsq+xLU+k5kIPqRADzMCATE6OEhI +8R/q/gECgYEA+OMypJwZyMIHY/FQdN/bDnEluS6zdpLW4CLJiZxCbKThYzKJxxWx +RV20Pg0i2tCOLM2SpyvmwzVNgF/XeFxra1DKty/8K9aiibPkCbbruzRrgjSVLz0m +tynVjSGx6xR905aRg0ZZlnMLW5Oaag1FxpcXBW/WrqTmA7+BNE7dCJ0CgYEAxoBy +EYu7ZETePmwNS7b6FQ2VJFIt5A5W5AC/0J2+MtXBgNzmGvVX6ss3VaHdlcJORgP3 +03eGoUnRANsfyQFsn5moTsbL+/33xzmU26KXc7X0EH1sX2eDNdAy/w1wbOXBnlmR +DoPKRi/yRs9r9axKA6XhLbpGa95c5Zb3cslrRiECgYBb2a0E5r71QfXsQlBI8Zws +FkGvt7ZvWf2Gtg/egxZYdfjf3PTPzgef69bq13o3eqQdkLhAjYkaHX++kqA4QRM7 +hZNqS4EqxIkzfHn5w9H3Qn8yjClKR60UB5JSBU9dzDArd/O3mKzTQx277BsebEuF +4JiUXxPBXAWu+Gu8XCZiWQKBgQCyGKw/cc8/t0pvVyLWu+TE3trhK5pg8zyidMmk +suLjWUW0CH2r0lR/JLUQ75mPtDXmO8kuwh7YtxH+YpuyDMUYrRSFGXgLL2IQLWmR +Zd/cwXOpF98T+huPPK2EiNpF2h5NpK6xBy2Eq6AESDXkxh/iBgJytJ5aSbevhjOd +mXnWAQKBgBoJ2dsi12LRCaqwmnnnANKjlBnNzCzzSpcNOHte6SxDuI5x4PO1ZHa3 +JlCzAeLGYiwjQB9NXW37JDLq2FhREU3szpS6ew2+NHZdmHRZcETysC+eCyWOsZmd +EDIqe7EdJUbHXP4MS03r6b5ROc6vRfZO15R4cGufeDohrKrYf/8Q +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/gencert.sh b/components/relay/testdata/certs/gencert.sh new file mode 100644 index 0000000..5563667 --- /dev/null +++ b/components/relay/testdata/certs/gencert.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Script to generate test certs using the custom rootCA in the code +# Pass the name of the cert as the argument. +KEY="star.$1.key" +CSR="star.$1.csr" +CRT="star.$1.crt" +echo "***********************************" +echo "When prompted for CN please use $1 " +echo "***********************************" + +openssl genrsa -out $KEY 2048 +openssl req -new -sha256 -key $KEY -out $CSR +openssl x509 -req -in $CSR -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out $CRT -days 3650 -sha256 + diff --git a/components/relay/testdata/certs/rootCA.crt b/components/relay/testdata/certs/rootCA.crt new file mode 100644 index 0000000..1864f5f --- /dev/null +++ b/components/relay/testdata/certs/rootCA.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNjCCAx4CCQCUC+ovW15jKzANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDEwOTE4MjQz +OVoXDTMwMDEwNjE4MjQzOVowXTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRMw +EQYDVQQHDApQbGVhc2FudG9uMQ4wDAYDVQQKDAVSYWZheTEMMAoGA1UECwwDRGV2 +MQ4wDAYDVQQDDAVSYWZheTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALfQiTsC6u3g99OgQ6TH0t4cR66Hapaouyh9p+8J2iUMPIkZKw9Z1iyKd+n5zqBV +lPZl2t9E821A15MjMCJOKjT3F9wuNeiBvxDTAmpyK+1Nj5OXXEbVXgVsq2uy9I+Q +f5nX47tXiiqrmSLH1cFYWGzs7RCuK4Df8MykKkKYCIW/PAEReHFqjVlUwFaff35e +T1b/NcGIyQfNw3MRcLRtn3BF/LvwraqV4MothqCYQoEVXfp7ON5lOvaQaPLg++F/ +I/U2aVI9iQ47glBY2t+lVXPX8TwwMFNu3k+xnJ0+NRDlkXoyDLTfLNcDKxf1wCCl +NWw2DvtVMAyDtibsMUeXR/q3kl3UhTNupHxf3YNMIhn5NpBRHi9JEvJbe9hF2O1f +NvIaJeafsKvT1Co5sYxokVM0vjns31qi7G5f7PvPxmHg64srV0aeXRpdOb4kLzPD +Vlep6kmJnfCoDjNLXUQeZRrjJgU7pnba+2QVsXwaK4FUPe+fhrO2OFaDeZ78MY4z +2QsC7rinspkvl/mwoxfyB6g/8IS3zI6Ii2LRnnqjpqw4sphnxz2gDAhnEo7Nc9A2 +Cy4I8g04Lqn4O64WP/FFUEheBWIpKPIFEzWtZvguR2zETvB+H0o3gDDfgKM5odsd +06Ujh+C1pouMfyuTTDdNWrTDpNa9MPBsKRMM7rvm9RcnAgMBAAEwDQYJKoZIhvcN +AQELBQADggIBABUBeOADlWbNz+FwjC4TtX5+0td4pt3sPJjS4tREYZ5lrWYlByG4 +To9Pl5AMfd7Bc9r/5tkDyk0mIvawpPBDJL8G+cAJr6SOn6Pp4puLXCoyew2U9UiX +De+SPEeTKi/YuZjgZIO7xReat75w5ib8Zvx2GyyFKhS5fxxCtWVq2gI/UFD4j0wn +OXwE5GUkmQRSIFcmzmuPhGwDOO2VGsd1UJF/trvH8x0wOhiJsCjtvXSnVad+Dub+ +EXGeKYCJMlfnplqERXq0mkakUT1kA6qdJ9m1cvu0bbCap8jnrfuRDkGQMkB/1UHt +c7YRBG5y0cwa47tMlKZ1/lXkwYjMPGWqOshtm3lpIvmv+/CyxaxTdN6cs14uJpy/ +QsOmOkNuaZv+1DSwaL3bQYLHYXhm1TdcfFVZVsTVTO3ZfZH2LqORpqTjVe7JCGkZ +09kliztztVNtT/4C+CIhY6i2zncU1lCFQkUZh9e4uyNR8Y+EjMcivMnaAVoCK8lQ +Shj0uPSEkuYrxHFIJ9AQuTrctI53u9jBdOkjXk00NB2gwLyDRhWRUHz6KxpNMwpc +/8jMdU/GjESFASGMPuazMns0g4NBqE4jT6tGJhL4sFmn7HOHF3g8uYZnJmTyQCny +FQOQw3/KbgGNONiTpYnmUrlCvtZKC85awJC07G/YNxAZM0MPxo+pqgz/ +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/rootCA.key b/components/relay/testdata/certs/rootCA.key new file mode 100644 index 0000000..064d2c3 --- /dev/null +++ b/components/relay/testdata/certs/rootCA.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/rootCA.srl b/components/relay/testdata/certs/rootCA.srl new file mode 100644 index 0000000..c1b3a51 --- /dev/null +++ b/components/relay/testdata/certs/rootCA.srl @@ -0,0 +1 @@ +F4905E32A3CE6D52 diff --git a/components/relay/testdata/certs/run.sh b/components/relay/testdata/certs/run.sh new file mode 100644 index 0000000..1f035ff --- /dev/null +++ b/components/relay/testdata/certs/run.sh @@ -0,0 +1,53 @@ + +## For refernce only +### These are the commands to generate certificates. Also refer gencert.sh script + +# ROOT CA +openssl genrsa -out rootCA.key 4096 +openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.crt + +# Generate relay.rafay.dev certs +echo "User star.kubectl.relay.rafay.dev.key as CN *.kubectl.relay.rafay.dev.key" +openssl genrsa -out star.kubectl.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key star.kubectl.relay.rafay.dev.key -out star.kubectl.relay.rafay.dev.csr +openssl x509 -req -in star.kubectl.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out star.kubectl.relay.rafay.dev.crt -days 3650 -sha256 + +# Generate relay.rafay.dev certs +echo "User star.kubectldialin.relay.rafay.dev.key as CN *.kubectldialin.relay.rafay.dev.key" +openssl genrsa -out star.kubectldialin.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key star.kubectldialin.relay.rafay.dev.key -out star.kubectldialin.relay.rafay.dev.csr +openssl x509 -req -in star.kubectldialin.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out star.kubectldialin.relay.rafay.dev.crt -days 3650 -sha256 + + +# Generate relay.rafay.dev certs +echo "User star.kubeweb.relay.rafay.dev.key as CN *.kubeweb.relay.rafay.dev.key" +openssl genrsa -out star.kubeweb.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key star.kubeweb.relay.rafay.dev.key -out star.kubeweb.relay.rafay.dev.csr +openssl x509 -req -in star.kubeweb.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out star.kubeweb.relay.rafay.dev.crt -days 3650 -sha256 + +# Generate relay.rafay.dev certs +echo "User star.kubewebdialin.relay.rafay.dev.key as CN *.kubewebdialin.relay.rafay.dev.key" +openssl genrsa -out star.kubewebdialin.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key star.kubewebdialin.relay.rafay.dev.key -out star.kubewebdialin.relay.rafay.dev.csr +openssl x509 -req -in star.kubewebdialin.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out star.kubewebdialin.relay.rafay.dev.crt -days 3650 -sha256 + +# Generate relayserver1 client certs +echo "User relayserver1-ABCD-123456 as CN" +openssl genrsa -out relayserver1-ABCD-123456.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key relayserver1-ABCD-123456.relay.rafay.dev.key -out relayserver1-ABCD-123456.relay.rafay.dev.csr +openssl x509 -req -in relayserver1-ABCD-123456.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out relayserver1-ABCD-123456.relay.rafay.dev.crt -days 3650 -sha256 + + +# Generate relayserver2 client certs +echo "User relayserver2-ABCD-123456 as CN" +openssl genrsa -out relayserver2-ABCD-123456.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key relayserver2-ABCD-123456.relay.rafay.dev.key -out relayserver2-ABCD-123456.relay.rafay.dev.csr +openssl x509 -req -in relayserver2-ABCD-123456.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out relayserver2-ABCD-123456.relay.rafay.dev.crt -days 3650 -sha256 + + +# Generate relayclient1 client certs +echo "User relayclient1-ABCD-123456 as CN" +openssl genrsa -out relayclient1-ABCD-123456.relay.rafay.dev.key 2048 +openssl req -new -sha256 -key relayclient1-ABCD-123456.relay.rafay.dev.key -out relayclient1-ABCD-123456.relay.rafay.dev.csr +openssl x509 -req -in relayclient1-ABCD-123456.relay.rafay.dev.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out relayclient1-ABCD-123456.relay.rafay.dev.crt -days 3650 -sha256 + diff --git a/components/relay/testdata/certs/servercerts/.srl b/components/relay/testdata/certs/servercerts/.srl new file mode 100644 index 0000000..b7ce295 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/.srl @@ -0,0 +1 @@ +CB6AAE2A400B8C7E diff --git a/components/relay/testdata/certs/servercerts/relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/relay.rafay.dev.crt new file mode 100644 index 0000000..1e1158e --- /dev/null +++ b/components/relay/testdata/certs/servercerts/relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQDCCAigCCQD0kF4yo85tQjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDEwOTE5MDE0 +MVoXDTMwMDEwNjE5MDE0MVowZzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRMw +EQYDVQQHDApQbGVhc2FudG9uMQ4wDAYDVQQKDAVSYWZheTEMMAoGA1UECwwDRGV2 +MRgwFgYDVQQDDA9yZWxheS5yYWZheS5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC3xFSCk12MhfqrREW1UpGHbnFQ3Icl65TN+H99rueD7eL0CXf4 +bRp4V3ncQk35HHVBn6FaSbplXfNNFq9YSxfT+GGW07AHkUryubVQSrjH4GfCn9gW +ngxM1cslAiJNULmkFG9y6xIK56auQl8xbf1qGKjuAXh6c5AKmCIx4Zyq26ucdvTY +a0kOmBEjdMFA/5hiAvOsAlUt8fSC6TnJ4eL4XZNrRpbbTVyJ3m4yFmWv1Az1hTRJ +ZYbTFiOEgPBi/FxUSkaCt7rsY+JSjKIsYJH3/Lo4kvH1X+9QuA/r7ZCzx+mKxCEi +qRddbintLMqCkuYCH12XVAmLeElCLEOqOUk3AgMBAAEwDQYJKoZIhvcNAQELBQAD +ggIBAF89TL54hR3mvAiegldIajkVW2QYFCkbKhH1/E2m5jEzdA0dqJk9lJgtIJGb +vXBGopVRDad6lBqyOfC/E0QrwXt36fEUQZtU8KIc+21XzcICPcmh9ro5LecDmo+e +CME7xuzbpYBH9s8jWL5FykU7UlZ2ETg1BTj8kHIPRV3WkVHb34/M2CREOdnikKfF +oU3R+PmoOjOb/zVSbc9cPe9Wa75cp9moortM7SzE+be7Ah8uTYlydleRYlQ96PTM +Sd4Vx8qMyRgEYgQTzMhMeba9XTtJpeojmdWM5GsR+TBPhqhGSrDY1qcfMYj2Vxrf +YusIfB/hlnrk6s0cS5b0EoIstCi8/Xo++rU0HcU8GSSzU3+wvgcgfH9v+qeFK1XF +zr3cKsakdvwwdllk1Ko8nIttpPXfG4h2vyt82hwbesz7qJ/LcUtTUkDKa9ap+r+0 +bioO1YIio6bVwWd4e4i6HekKVv/+Jbrq516cJVpYsmkviRQsbIqmPMxAjTuFnJ2w +GPF49oUbbByUeEqsRb0R/vVEeuJek01QRLkiBhwrZySBzfhpoNprXljmPMw1w8zh +YMGsEp57a9HC5A6kYL7d8fnw8xjDqcv7FU4VFtwYqm3CLJRhQTMr5+3EnH4xJel2 +z7F85OoCigjIN7a4iU//JeZZB/tfhjaot2g+5gWWKFG2EivL +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/relay.rafay.dev.csr new file mode 100644 index 0000000..4e0c3c1 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrDCCAZQCAQAwZzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRMwEQYDVQQH +DApQbGVhc2FudG9uMQ4wDAYDVQQKDAVSYWZheTEMMAoGA1UECwwDRGV2MRgwFgYD +VQQDDA9yZWxheS5yYWZheS5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC3xFSCk12MhfqrREW1UpGHbnFQ3Icl65TN+H99rueD7eL0CXf4bRp4V3nc +Qk35HHVBn6FaSbplXfNNFq9YSxfT+GGW07AHkUryubVQSrjH4GfCn9gWngxM1csl +AiJNULmkFG9y6xIK56auQl8xbf1qGKjuAXh6c5AKmCIx4Zyq26ucdvTYa0kOmBEj +dMFA/5hiAvOsAlUt8fSC6TnJ4eL4XZNrRpbbTVyJ3m4yFmWv1Az1hTRJZYbTFiOE +gPBi/FxUSkaCt7rsY+JSjKIsYJH3/Lo4kvH1X+9QuA/r7ZCzx+mKxCEiqRddbint +LMqCkuYCH12XVAmLeElCLEOqOUk3AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEA +DWm2eXew675cXj5pdsL4mKLnpQjSy+3Ewdg+NRQw8VUF12xjxVNUCPcrX2wcrHx6 +1KqCxOKr5b3d1SlqAKVwEh+6J5+A/nbfFSxT2gUmqy1IEe+obYSvj6AiN9D528rw +e7MKlwBi6ARgyIT3HjC95gKehX6qrifNiY0aNnP/BfaCXNxPfP8Ltyfqswv9+P26 +kHePUcVBLOA6+SKimVvYqhAtLhaqjpjBo4JeX+MB/tP+kzAb06wBQOb84SILOBEy +aOMSF1uR0peB92zxLqblhJMoyZc/rOH6zE3PHjNPiPcX9BFJJ8LHGLXPAxoR44SX +flK/mNcmRPVplIyJ57OB2w== +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/relay.rafay.dev.key new file mode 100644 index 0000000..9f86b6a --- /dev/null +++ b/components/relay/testdata/certs/servercerts/relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAt8RUgpNdjIX6q0RFtVKRh25xUNyHJeuUzfh/fa7ng+3i9Al3 ++G0aeFd53EJN+Rx1QZ+hWkm6ZV3zTRavWEsX0/hhltOwB5FK8rm1UEq4x+Bnwp/Y +Fp4MTNXLJQIiTVC5pBRvcusSCuemrkJfMW39ahio7gF4enOQCpgiMeGcqturnHb0 +2GtJDpgRI3TBQP+YYgLzrAJVLfH0guk5yeHi+F2Ta0aW201cid5uMhZlr9QM9YU0 +SWWG0xYjhIDwYvxcVEpGgre67GPiUoyiLGCR9/y6OJLx9V/vULgP6+2Qs8fpisQh +IqkXXW4p7SzKgpLmAh9dl1QJi3hJQixDqjlJNwIDAQABAoIBAQCn/dOf1qWzW5tu +y6fJ/aRKIV13pVHrCXRr4lpyA91jDqnlm25P7TVAg/9Dql1reGd8yTgQuMur9PoB +I9bBqUwsG6ji37vQ/zJ7t+cXcmd/WRahpcw9yNfHub3OpCfFdCFw7LmBXYGN9Hy8 +WrJ5FmqOfeKYSUHg9xnbhX3lCbFVwpI4KE7WXuvZH+7A+hJl6UFXm0nMGDWscylI +5MUS6QPoQZ0eMR2I3XgX9MrspdSzGdg8mTPIfyK3Di1RfbJEXba7HqlVk04r1PtP +LXQfftRpZVrpRt1CpzHoJy0sxl/iuqWtWSHJ0KxvyxZpaZMOmjPiAkvQ9cw5hA84 +POWfLXLBAoGBAPG20UqKH1pbeFN6Ne5XCYQJbqDw5nbreDHcfZp8cZ8NClf5Yn15 +XRax7eK54awYyW5sR64dTqT66lUncO1MKItbSiuIJ8Yji50oObYD/r3jLhmTXtei +I5sNIrOM/eTEaQtKgX6wUHnKrsaDExP1AcOXQB3z1S1xiog2HxGD8Z7hAoGBAMKg +wn/gU9y3/NOf724KiS00+nEmFnjuW6MFZFhx0qKLNLeSfjgUdd5WMdDCO4fGjhKT +gajpvQ73btFrjnfpsRW8DHCKV4lZCWV0q8Lhd7GdP9xf/Yete93I712yGFfxxQ0T +Gp8eQ6atUJ5McQryx84zLUzPrQDPMDnguiAAc2MXAoGAUNlmnfkfTZjk4X8yRNuG +Ck0hXOHXJJXUaVEoDiFr29h/yjdXU0Lh+TJgsIKeUnMkBTRgsYOKB7MJ6Ju9sRHV +Se+qGm97ZlIDY/jNfO1PKvKvO8zIX3YTVMyK++XAEuIDhMxSDi8pa+N4nFvBaN59 +zY9s2dfySTX+MVtw/unUSuECgYEAri5pY0BETVhr8IlGIllTgx3StMi9e+T2JeOB +dpYHSGkE+PUl+NJ22lCRrJ7n4A+WvpE8jQsCpA5VIx77Fd4TGbx99YoZ5VXFzwKn +ZXWbRV3R9UwC/hYIsOhPMK9ekfZRv+3E+XqAWP7rE4Cd+opkvumWNRP3ZK4IHwQ8 +Dr/YhGkCgYBTWyOAS2D/xa3lgFbsErh+k+BgvyfwvLfLmEugwqioiWn59oUvA6At +MCiB1eTbq8CzUZGElZCH1vQjzvL475iSZtXHD7qN3OHp6iUzF0Sw0SV/ptwMnQXN +mMQtu9hlonPCwklIfHVvhTAu+eaYFpIkZnrbwxlcEcLeFk8EZM9VRg== +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt new file mode 100644 index 0000000..a78144a --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQjCCAioCCQD0kF4yo85tRzANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExNTAwMDMw +N1oXDTMwMDExMjAwMDMwN1owaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEiMCAGA1UE +AwwZKi5rdWJlY3RsLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAO7cbcZP2XZZ6djdO+RyjH4NOpqaxURgLPosXVYr2mTduizA +jtxAS4sZPfdZk9wQY7RT7hVoOQY836kjiiyjwuy2BhhiYjc4ovzGR25bNwi3I6bW +QN8EHF5KIySB2gXgn9br5LgDz6chZwcWYfyqWcbJQ2oesYQIeOCsSkm1cNA1U6J5 +gAu6t8Pe9nbaLiH2t38QnVmRbCoNYUpzUNQ5shrM+AfJ8CRZtahkd8ucBPly4T/9 +dbjIjV+f8KqRTfbT932KkKLREyOhugfuFJ//YH5XETr38EcdHJ0i9Hx9P+D7+Mou +X7o9+quX9qBDPkVSJZRREmwHw0y3ZkvK9VtRbqMCAwEAATANBgkqhkiG9w0BAQsF +AAOCAgEAAZ1vLleh+w+6SCNiMmuwYFyIQ1hEv5GlMR47Tz0P6iJdLAaCTU0zBihM +ChNjoy0C3OmzKmmlAv02r8GCzkDLAvcfh5T3QbF9fDDhAGKqoOZXFsUiYn1gx4o6 +8UgWI7Y6golreD7VWEa0FCctoi2TFsmWgAPtaY9EJKUsPpt+2Awtb1ZJ27xEcYfI +gNN7ZZe3KwropMiFjCdIT1Uy/99TDiv8z/w1RYH0WeMBQLATED3flYewyLg962z6 +tkTooS5tvbtpbxtrkjBsKkktHo8wxacVvcDO8VuOnZJZC4JCn5CFgBYkdBUBpQSo +R+bI+ldroxSgcUndNGwnNWJt7C47p1WUMohR7Uqf9uuCYFuy9tJMA9LN1nyRRKV5 +D2/N2pwias89+Rk4PNg03jAeJRoCQsV6zMMMd/Waryg04ujWUnBpLR3ZyOLL82sN +lIpTed3+2aR1F6dK/dE3n5AFNcZ3HTvPNpgYL96IjLxPyInMSBQ8tvzmIrESMRXt +ypBRFg8iU5Ri0crB/aHxmqerjDHbwvLLU62424uR24IJK+nRIkYdrthqaCFsCT6u +JcHi2dfy+s9aCjhSmu1BxIXgJCV6tItQ+dwGmRVMQnQC5VBqgPm3UUdTa+rs3pwo +0VYyQi1QDQWftJv2Y/rvtLv/OUws+aCk0AkjjohTo9kBkxsD1zI= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.csr new file mode 100644 index 0000000..97d47f6 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrjCCAZYCAQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEiMCAGA1UEAwwZKi5r +dWJlY3RsLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAO7cbcZP2XZZ6djdO+RyjH4NOpqaxURgLPosXVYr2mTduizAjtxAS4sZ +PfdZk9wQY7RT7hVoOQY836kjiiyjwuy2BhhiYjc4ovzGR25bNwi3I6bWQN8EHF5K +IySB2gXgn9br5LgDz6chZwcWYfyqWcbJQ2oesYQIeOCsSkm1cNA1U6J5gAu6t8Pe +9nbaLiH2t38QnVmRbCoNYUpzUNQ5shrM+AfJ8CRZtahkd8ucBPly4T/9dbjIjV+f +8KqRTfbT932KkKLREyOhugfuFJ//YH5XETr38EcdHJ0i9Hx9P+D7+MouX7o9+quX +9qBDPkVSJZRREmwHw0y3ZkvK9VtRbqMCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IB +AQAPkDdgYNELjqRgveMqBVBafxbuRAR4AdAzPDbCLOBDva5WiSkiOOKAhLBbspal +xkeOdAg0KkUhSX/J+l1yKpP7AnitpTEG/sJog7SXv4/Pk5oaPYwMKpHfqXKjcgBE +QrtPBgDmQbyQwTkcc4f77TvUb9dv+dXK8UGjcjcnDlKdnG6XdyGDFInLjfR8SE3/ +u2CQ7fKZIO3Z7G1IsI3YKlK3oVTWtquhQvL/YY8hhT80xMItHJ4GL9eFQYT7NFvE +xQHXsfLuIhlG50wT/Ce/YXmpndOX13FppfgkXUC7zpGtiecX+oOa2ybT+wkZ2kHP +/ouZqrH4LUzSK4ufip6Xip5B +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key new file mode 100644 index 0000000..9d1a6c9 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA7txtxk/Zdlnp2N075HKMfg06mprFRGAs+ixdVivaZN26LMCO +3EBLixk991mT3BBjtFPuFWg5BjzfqSOKLKPC7LYGGGJiNzii/MZHbls3CLcjptZA +3wQcXkojJIHaBeCf1uvkuAPPpyFnBxZh/KpZxslDah6xhAh44KxKSbVw0DVTonmA +C7q3w972dtouIfa3fxCdWZFsKg1hSnNQ1DmyGsz4B8nwJFm1qGR3y5wE+XLhP/11 +uMiNX5/wqpFN9tP3fYqQotETI6G6B+4Un/9gflcROvfwRx0cnSL0fH0/4Pv4yi5f +uj36q5f2oEM+RVIllFESbAfDTLdmS8r1W1FuowIDAQABAoIBAGiWbBqha/6+b/5d +IJRu88Vd1YNMsYO2+Pqx4kyQ9xejXpmp1uPp4yoM620a8/sv0fhrp5wH/rAUh3pv +7ViOfBNZwGF88rRrQDKePzi7B+XxCtNKjS0Zh7Jhk/GwSd649EQPFh7czFfJ8GrS +CiWwEY9RIQueVOwTKZp21jBgGf2QA3jcedOgS36q2zRGU23uBFLGVMNsY+lXx3TZ +DqDwckWWLtUfI2PqU5tKhUUcBlSQYKk3WL2C6otVly56u4C+Q4TPE/PSq7sH4n1H +1q2GGhX3gmp92V+HZ4HJiuY4xXw4QRycKSfcGvIF1ZKGcA/Dl+STv3TvY8nfS0AW +18VFyYECgYEA+FSz4N6RzN3ndn9t6dp/gw0PX6HlrjXnW3rphOuJMkuj9LOinusD +K/xrnpIoPvfh/QXWuebqBhw0LTgGMA3yYW63nr+te5/N+A0VytcGD6Htp01IYzt9 +n1V7fAbB4CDFM/NLBEzNsbJT6308eVR9e53lvJxT+sizz24gFFc2VisCgYEA9jzb +ppkM8uafsxMHbNmduVO4hTagL6obKuGXCkR6X38m1R+k6/7oRvaRty9OarhaY/aQ +oKgDtgaKyVWxft5yEe1B6qczlRvhxKVTAd5py++Yg4gyaCvNrHGuWfzM5c7vo1Lz +k+puDC8kbkPSATEsPI15gGmVce8cc4s3wyWrxWkCgYAzRHlmYUzEeMOCaUht0Rgm +HBoiajAayTIcZsGzGVVXttoAeN2H5YjgGdq2AAKjDVXwgLYV5yBFZqk29KwCW77A +crUh5vyP2ywQm9L173Jm481GjuP06KFEtccqkdKQzPMpiH7iQmDs4J4GrfJAMjs9 +vBlj2PpVef31Qw0IZgDnxQKBgQD1SIw4+S6369nbZTI5UNFXojklodORh26Gma9+ +T9ZiuGNzW5FKcGLrBYUDqEEB9+A6wYsyLgt8UCoCArXLvkR87Q2DdMjiWKLd4x27 +t9VFi8cuBwbHVBMOGzPlefiVVzzJQcFXb+fthSxSFobkHxqEuxz0OLjghKKfymdi +omzoOQKBgQCn0i7HSy8nZsGbq3ApNYfit2hfcGK2cfihvMbSunx1myk4UDFjiExf +tKnycpLriTsKYQ+3Jj9zO8+QqjPxb19XkTAWKZ5HYQEwS19f+mPwMWyn8mvwOruL +FDk16um5sMM1kVwStMnlg7TxCZwrT3dXHPQuZj+qHcJnacrt8JBqEw== +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt new file mode 100644 index 0000000..f1b989d --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESDCCAjACCQD0kF4yo85tSDANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExNTAwMDQw +NloXDTMwMDExMjAwMDQwNlowbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEoMCYGA1UE +AwwfKi5rdWJlY3RsZGlhbGluLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANU/MiB/kLKlcMhMtXuKIGu2riYUfmfUIz2MQNo+ +oz4wQHUGg9P1sO3KT+kzfOFN7EkAzwryCIT3GEKTYoVY5X+Cllow3+IhtovgHhhr +R9vSDBf6QSWqsVbai7U6jGQwtr9D+4Tv8O0vXtUWnTylE95/2Ecdf+n/7US435Rm +9eg/CCsdgPw/m/WTPA9mU7xrpou5Pwbb+TiCJsnQKbEGGuFilBdBLNZkj0qOAUMl +YBu7A7n2yIljC5zJ93dlLX0kdssSiDwt0KcXwrWs7vhXxrnTRx6sEneloa10pbG2 +0BPoqleGKk6LOy6237H5wJmJ2Feh2Kcl/o9Au5vZ1GvksRcCAwEAATANBgkqhkiG +9w0BAQsFAAOCAgEAhhUNQL+cxlA3ZAOuySKyNaYFkVKEjoo+0hPpMzxUl1TkHRIE +OllT5LzwjduFEvZRTE5aMiDpEUTM5XGw0zKd26Q9SC/vOnBwYl19oFhwyZ93bz2d +15eoyEYQOZf+VKmTEVw96epMrt98mACPO3hrGGtszW1zGEMRfcHZwIMHSM9INAGS +Rtjx5Ul0HR/srTZlc6kTpeLnnoBKO7TuPwqEav7MA7MWT62UGst0pz72o1B41sEY +rBIeJyGoFVg0NnET0nFBQcZqipdDjSWfUiDcMxOSve0s9IOpdLYtcMdark41gAXp +qY+Hy1uVXQx6UFJysbKaRZwgx4C/MnD/zs2a/LmGXdt1+7/AlrP7HG8NKkYCkf2o +mSs3ALgmt5wQ/DTwfVZM/6EPmnq1rU8s44DST0CDTlpVeYq0PkuweAQYYYgHBzc+ +hjx5b8k9xiqudhTveYCuV4AlIGTdIGbsK1F7HSnNE5S84dwcvcFL025TQyc3mEkK +HOdtGqBTZkxrehHSZs+Q17KwfrYx8Ad4OX/hXsszly4sgvV41jAcAN37hQCMbNkk +KlsaYXnsSDSkCt/jjh127hWaTzXJnCaQg9zijvlE8/ufYXoKW3+yCSBGrVt6cQ6d +/o1jnGh7D8mgLjTCR98trHaV2XZT8YiAPGbqf0RWmYdm2v/zubYl2Nor9KA= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.csr new file mode 100644 index 0000000..f960d97 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICtDCCAZwCAQAwbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEoMCYGA1UEAwwfKi5r +dWJlY3RsZGlhbGluLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANU/MiB/kLKlcMhMtXuKIGu2riYUfmfUIz2MQNo+oz4wQHUG +g9P1sO3KT+kzfOFN7EkAzwryCIT3GEKTYoVY5X+Cllow3+IhtovgHhhrR9vSDBf6 +QSWqsVbai7U6jGQwtr9D+4Tv8O0vXtUWnTylE95/2Ecdf+n/7US435Rm9eg/CCsd +gPw/m/WTPA9mU7xrpou5Pwbb+TiCJsnQKbEGGuFilBdBLNZkj0qOAUMlYBu7A7n2 +yIljC5zJ93dlLX0kdssSiDwt0KcXwrWs7vhXxrnTRx6sEneloa10pbG20BPoqleG +Kk6LOy6237H5wJmJ2Feh2Kcl/o9Au5vZ1GvksRcCAwEAAaAAMA0GCSqGSIb3DQEB +CwUAA4IBAQAtZX8bZMGTkScmCE41UCNdAOXEb8NfnOXJW1TrnkLCgpl6fSen1g3N +/2GDyB4VZ6/F93DKBZY1zqQxr6V/qhwSyCOcM/1Kw0CvBP00QYhrVe0POFUOHpnB +nvGKjSy+5oRXN1F0VHJ3smy4WsPGAyRnFwsVxVKlArYAJe0Pu4zsNnopam/M8Bkl +CBwmmiTeBOqw+ue1P+uQ1R2J+V00oimhZoYqSYEfOhWkBw+lNp4z9iyFmYT028jD +kAsSeE5wBYWdF9lW63EqxvnZyY4bVq4K6FIOH2Ddf7mFkuXDp2V9/Xc47rwdbVSd +T3ImNZgN6u0fiSmNAQH8ydvGKOhX2mM8 +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key new file mode 100644 index 0000000..9b36182 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1T8yIH+QsqVwyEy1e4oga7auJhR+Z9QjPYxA2j6jPjBAdQaD +0/Ww7cpP6TN84U3sSQDPCvIIhPcYQpNihVjlf4KWWjDf4iG2i+AeGGtH29IMF/pB +JaqxVtqLtTqMZDC2v0P7hO/w7S9e1RadPKUT3n/YRx1/6f/tRLjflGb16D8IKx2A +/D+b9ZM8D2ZTvGumi7k/Btv5OIImydApsQYa4WKUF0Es1mSPSo4BQyVgG7sDufbI +iWMLnMn3d2UtfSR2yxKIPC3QpxfCtazu+FfGudNHHqwSd6WhrXSlsbbQE+iqV4Yq +Tos7LrbfsfnAmYnYV6HYpyX+j0C7m9nUa+SxFwIDAQABAoIBAC63961zBSp+YGUh +mSkFq/CtRx4Eo2Tv7wCO965Yxr7bJ2P8F2majDasejFnHFboEXo0+Sc2u3MWB3Se +gZkhTrZOz83Wo1YReMLQSQekiUu5ivi91QQBj3St0KnVt+hDW+YvaK1JPAlatXYN +zEj7CY1XiPSbm+iSL1M9BY6nt/w3XJOUm+Zna+aNsUXhsyUBRb7tcXmG2HEQk1HF +oyXHngRk+OncjFYTeOfZV4srbz1+lp+dA2+eaTqbHlRbgRkXU4lT0uqTziTVH5nK +QFHvIoflYP0ScmmE0TmCvo2h47K8aCEHfnaaZVd40bh/32SzYzjlziSFB2GQEKNg +bfnCfqECgYEA76z1NmXmdOE5OXPqa5sFsK/rjQZmRaN3YM/62CwAzj5wKi2oYLLW +XbNwDbyHsutdqIVArUq7kU+QTWvuXsFm3QcEd8H7CS9y4aS8Y6XXLzxp8xWPyDSP +O7+RSCQKzAyCZJ/8SoFcVs+Jv1/QJ8Pfj6TYBe64vrdAUt5Ib2lrbp8CgYEA48Vr +cpRKxsNygusF7TebVFE1kIALQh11GnJ8+SplZ88TaUF4vGbiVTDOJ++v7UnLAeAG +v+jJ6OZFGCKcMEBsG3vR8CCAbs5qte/+KgoopaDb0LqiRbLAVqAdfUigi5hs0XoR +df9ypW9c44iWE0eDIZMj2FAsR4kh7PDBi+kuwokCgYAgvBe1uU8zkXtWXH2c+sxZ +KH3cgg4/3qa0Y1OYaNInWSHKsPe5VVOv8o0QYzqJdloRZTnDMVzS6PIFkQM6j+Gz +uHykuqrFw2RGa1CLr5/nRLIX0E8WAg+6El0OZFXRLrg0dCFqL6bj+G6Q484zUC5d +mR8kGEnFSpQPfWqZROW2cwKBgAgwLLCtzBJhLFiwgkII4biiQr6zG7SAwpj0zLFY +aNTWiOgbxm9fc3uEiaS95SEuqfGjUBrOD5Qj0xAPDnZdmoVSajWrtXJGUWfA/O4d +92Ac7EoQq4y95FQ45VZ/qeXa0O6m6MHJMSHpYvl7C4od5ATs+TjpiO35bBLMhOi2 +tUuhAoGBANHsvkzWxXGxwJ+H2dRbh4DLHqMYehgP+2Bt/goTD13MWPDt12ptM1QV +QmIhX4YHwybdJUilMiH4jpA6b9zd2lUPWmILKp1MeHczE3RJioQoJ8cRackWmbi1 +n856DbUV34dSStyw8cSQMb6dwIgAP6JyebaVcFQwCe/K2OPsMyPb +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.crt new file mode 100644 index 0000000..442e526 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQjCCAioCCQD0kF4yo85tRjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExNDIzNTk1 +M1oXDTMwMDExMTIzNTk1M1owaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEiMCAGA1UE +AwwZKi5rdWJld2ViLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALk/RVVcU7Di4xwE3Y2/pKyZbDQBvZ5YZy4LYT6WsWtGdKFT +tBNJUSJDjQlVq+g75X5mbf53EDP5VKJkrOd8v326NOlXTUVdEyUcHGUB7xrY4oFg +owG00fiuaDWRRVrOK01jrW2cmZ3tzqpHw998uO3gwLGiE/CL8xjcdWRl/zX3WqY3 +oVzmfSxTIdvMVAb+fiU/rlXUp9zNamYe5vG8P9XvNk8lf7pvgBNSb3pWlQoU7A41 +HLm0gqy+yauEvGq/nbbVyXQ7VAV7XxwoHEu2wabSC4iTfMHCx2sVd8P3WvBuBDf5 +4InwSW87NUJvtwzIRqlKcO1l1q2xWvWFeNUPc2cCAwEAATANBgkqhkiG9w0BAQsF +AAOCAgEArVNrGgaiHYQKIfs91Vor/A9GPRsj6o4VavCRiEjaBuPSq5G0v1fb8m9Z +B541y27N2QPdbYza2TdNqt22E4DcPXCi/JPSM73BLml3mw6ZuSLw+ghFFx3ZMN8X +RPknim1q9gmLbEU0zMy9kgjCY0OrUpmgfEE0DVQ5MxVovdz3B4QZ38hrKgwvYPyB +UBB5ovZYjPmaQPeRu0Cht9eSIm27c2/qAUrYNUWjmAVgrOQvKbIc+uBaj8at/7K2 +b6tOi6zyNdL+Mn6LpN+0tfz6H4NToROPXMaHXudoOMmYq2AHF9Ad8PjNsHZd30c7 +0muL2FEzD3t0P/9i80UNeKKTlY1BjOQ/npAxEoFoTpPSF58soLzO4GyXPIHZBkXa +DETmziyA18e+1vytyx2nMrC1qMc9tziZlWzOOL5THk1/plPaIoLD+rtPM2VwxBdH +W5mY7gSoqFDXQG/qaeLzF9uqFI32zmLHBcn6aMA6kiBkYK1HBcS48cC1LMvd/3lE +FTOdzhPzlRD2TZZDBsFXqDtP22oPBCwUMQDguvgajQGkUIlloipiba91LNIVLLGu +hmij1F43nXsygAXFNca9trXESPImataLvi15Gb+dad8ca0zfifncH/+ph4LtqsqI +trHkz7EhmlNVAKRSzExbcUPczzOGU3Ng7yoxezuVDnc1+2+6pec= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.csr new file mode 100644 index 0000000..dcd818a --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrjCCAZYCAQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEiMCAGA1UEAwwZKi5r +dWJld2ViLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALk/RVVcU7Di4xwE3Y2/pKyZbDQBvZ5YZy4LYT6WsWtGdKFTtBNJUSJD +jQlVq+g75X5mbf53EDP5VKJkrOd8v326NOlXTUVdEyUcHGUB7xrY4oFgowG00fiu +aDWRRVrOK01jrW2cmZ3tzqpHw998uO3gwLGiE/CL8xjcdWRl/zX3WqY3oVzmfSxT +IdvMVAb+fiU/rlXUp9zNamYe5vG8P9XvNk8lf7pvgBNSb3pWlQoU7A41HLm0gqy+ +yauEvGq/nbbVyXQ7VAV7XxwoHEu2wabSC4iTfMHCx2sVd8P3WvBuBDf54InwSW87 +NUJvtwzIRqlKcO1l1q2xWvWFeNUPc2cCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IB +AQBH/vo7xzWwV3skhCW1rE/GCjPakEEBFypoNtQ3qDOGvY5c1W3QHQAXETVE2XYA +9rBEZe91RM05W1Bwxf1Abiddq7hGWcttqsQfKbvinRnya/tmNyv1nHNkpjkQIhrd +8uyGhpWwf5lrqXybStJTm1d4GUgH3ss/Pt8aZ2OjDWpRJ+ur1pGzGFpgOtGGoUV2 +2ZucrBn3sjQThux/tQdZ4R2IwSm+YExvBECWCh+Uvd9neK9CVoHQHD3uDug/0c9R +9sRQW2vbOidNyffnZ9wXcUAW6V0ccz1cI6lP0Ent7Qx691wn9kUr5ixvembfxUyH +jQuT79ngmEQq3aJ2Ol+pP0/9 +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.key new file mode 100644 index 0000000..6d2aec9 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubeweb/star.kubeweb.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAuT9FVVxTsOLjHATdjb+krJlsNAG9nlhnLgthPpaxa0Z0oVO0 +E0lRIkONCVWr6DvlfmZt/ncQM/lUomSs53y/fbo06VdNRV0TJRwcZQHvGtjigWCj +AbTR+K5oNZFFWs4rTWOtbZyZne3OqkfD33y47eDAsaIT8IvzGNx1ZGX/Nfdapjeh +XOZ9LFMh28xUBv5+JT+uVdSn3M1qZh7m8bw/1e82TyV/um+AE1JvelaVChTsDjUc +ubSCrL7Jq4S8ar+dttXJdDtUBXtfHCgcS7bBptILiJN8wcLHaxV3w/da8G4EN/ng +ifBJbzs1Qm+3DMhGqUpw7WXWrbFa9YV41Q9zZwIDAQABAoIBAQCgvkpHe8oTX416 +rq4hZf5a1vaRWkFTuZou1ikXymNBlu11X9COO7xvYvZq9zAoYHEPcYpYmLAyqVPL +Zliytgjj7EQlhl/CpLUMKoxpCXiVb0V8xoIr5N+eTpiWguwPk373tFq1FoImDEic +cZGByBQgNjp+6QksXgQfuo0TBX6jL/m/6EDNoONtiPsHqZkRaADqoYiPE0kfyRaO +GmptOae0R8Y0Z4SkKnRqiokUzTIkjhf5QXYO0AkPXSkNgGS0Kf/JVnxyxfMMiCkF +sQHXoEeqMevnmuny6S0oXQkp+UbJBtCH+bZlQ4d9PWdRd8UZUDBdeyEu/NRun5qj +BS7zBRXBAoGBAOWNqkjYhq6lro5uu3pYkT2yFBQ9vQ0bXCRGuvG7fkqcH5+zgH4+ +WiqQuPVlNAG7Gg1D4XR1F5tIjPIs9qlY7G31aGOqYFdzWkEqQ6+wUXrM12rJAZut +Pfu/aheuEH0tMiwNvQt9GUl0CFY8HtKhG61HZRXxaotxOc48GqbP9OOvAoGBAM6W +3AgOGWVpEv1o+bn+g+38WgbQtvZOMLERSXMiPH7FBbkXAAZGoDLJ9uWaX602T1Gj +sjaOFqWrgkaC2h/erItpht5x9FBO80PgYXbZlLWZWJzsjkC9tnm7yyHSueZ8R8/Q +vBMD+NAdv9eqQJWw2Y3ZxKlSUHMkLQhdJvZtqwHJAoGBAOFywdQrfBMYu5g9nrkp +NPQf6mJ6xCqUPWmPYdk56G4EaXaih8b1G/8EBfNUwsAUbyCqE0oaGeYzWeIbUOG/ +aO0RgcWotlgM/Y5UoQdypoNxnPvOkFrBycdvFhZNqPoXolfr4kffRC+E2w8+mpMJ +EbKcFOZ5JMfwmvI2y4IbaYsXAoGBAIx25oRTkdDx8Id8qM9dzjYyLsQThVNTNqgu +KNKvpJJNQ+blTI731lSyHb1fiU4sbyIDO1j8o0lho+JtdgHVVVw1FA6LTKKOC7lL +BvnYVtRiv21zKgW6+CctGKCWtu+P8QPL2RTUzogysg3hJKii5kkcmCrIXQrsvfHB +5IwJZQyBAoGBAND4BL5oABu7ygLIUxJVxrvJlr5Uk4CUa6I5MPJu6Yb+RPbQW5kX +JEe5nl/OVyC/mc0yMZwPkudkYNHGNeaKwsjcQMNGHav6jnJoHHcdDmnXkO23xGYC +TTdKoXlCkC0NeFzpLAI0x5zwgd+1wKFfc4ZCCvwQAjx4AUKX+kYPN6hI +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.crt new file mode 100644 index 0000000..57d729c --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESDCCAjACCQD0kF4yo85tSTANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDExNTAwMDUx +OFoXDTMwMDExMjAwMDUxOFowbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEoMCYGA1UE +AwwfKi5rdWJld2ViZGlhbGluLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALX/q3xPMpTD9ewp87HtRs0XAOd4qcHIch75UMWb +eaW8nqd4xL5YTfe89yVDbdq4FHqqQ1rivcwp/WSHbNvRyvDMNlXZTS8PS6RqL75B +LljvzzrcIHax5WrUtrVUuFrlPN5yZW8fWB84ZY9EsX8UHB79rJdaS5r8B48/Ws/X +BIxiNQyIC9pNIfEDRQCjbn4GRCvCaf91tBxVwQGA38PF5JRSxblEx+Cte5d3ODH6 +5gCKMBVCsMn84gr/WGamZannth8/Lrasduq5Pvj/cJ2h4PUoh1oKwpSsXAG9d2tY +7AluVOVWvqJbvKKB1yegtZi77+D7oQ5ee3EOXriz+LWFsXMCAwEAATANBgkqhkiG +9w0BAQsFAAOCAgEAm4oPL/mJAolyewL/jdbMWZT22oRKIYqxJQoHuTzeTQQInAUp +KbTUuRKgbSz0HeoRESrxV14BI3XrVJlbctkyBRpi14bQPd4TzdBxDY+Rh9bkuaHw +4zc8/mMYOT2xpq885BMTkd8YE98t3fyrrUYTBMCstBNMamcuV9hzA9RTuvtggUyT +3E6lgGLaHyFQUp7z++zDiNDrEhILC698zC2EYbqM50PqqB2kcfMRLmYKv4yRgQPZ +boSX15mc7ZBtU4Mb3bNPFR50PKbfrCi7KDIQiFse6TQONu7Pi3sN9GHVLfPhf7Nh +acJkweVgP2zZUObcW1BtrI+yvgGMh4rbXGzoIerBRQgC64eH/I0OVZBSy2EiJDna +T9tAczjvYAgR5qrXV33xY6DW6nPdUE0N5JgdHFoFn5uElEMiOyAWbAEvsQ6WtQCi +f689rh4CwU1XQ+pEQBtoj9vy3ioeTvDzSj8js/ofdzIMC5xLQdwyFUcGbx3ntwrn +4pQuny4A1rrK4pL8RQX7gvsXpuC+rlbew2IpbZ7CP6DJ8xbmktJtmfL0VsYiZjNr +zpL/YSAazr+NZYCpYguBrL9aIzU38QC7hIWvN5s22Mx0r6RsOP3jAFkZy9TFOJaO +HYfszg8SI1iG3jfB18TMLTcFDjVefM8rIsYiQ6ZPQQnP3uPht70Tz2vUHFI= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.csr new file mode 100644 index 0000000..276bd95 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICtDCCAZwCAQAwbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxDDAKBgNVBAsMA0RldjEoMCYGA1UEAwwfKi5r +dWJld2ViZGlhbGluLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALX/q3xPMpTD9ewp87HtRs0XAOd4qcHIch75UMWbeaW8nqd4 +xL5YTfe89yVDbdq4FHqqQ1rivcwp/WSHbNvRyvDMNlXZTS8PS6RqL75BLljvzzrc +IHax5WrUtrVUuFrlPN5yZW8fWB84ZY9EsX8UHB79rJdaS5r8B48/Ws/XBIxiNQyI +C9pNIfEDRQCjbn4GRCvCaf91tBxVwQGA38PF5JRSxblEx+Cte5d3ODH65gCKMBVC +sMn84gr/WGamZannth8/Lrasduq5Pvj/cJ2h4PUoh1oKwpSsXAG9d2tY7AluVOVW +vqJbvKKB1yegtZi77+D7oQ5ee3EOXriz+LWFsXMCAwEAAaAAMA0GCSqGSIb3DQEB +CwUAA4IBAQBogrS+PnokRvp1rFYiYAEg4PPaIq6EYoPbRQS88ms0+MEjblXWW2EE +CnsXL/eFCQqi08HQCMzVAkAS9jqF0ZQ5X2tvVQB3OVddAOvlFBSFGhakoSDPuqZN +btQMfH//GRubwBftHbu66qKsltutHnobIts0LiIWxFsK0gNUOQDAU7ts44EDY5yq +iUR2slS077+jGFI4GvB2vmdNTX8JupOMH+60wpZIDSIwI919ANPTj3vBxdVmITP2 +V8wFtKl12Lznww2u8OcAH6g0v74FH+FfCZIVcJWBrinuWCmrw0VdG+s+D9/2PnkN +6XAarzL3eYilbin/aBE6NafdERkSCTat +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.key new file mode 100644 index 0000000..c4b3ffc --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-kubewebdialin/star.kubewebdialin.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtf+rfE8ylMP17Cnzse1GzRcA53ipwchyHvlQxZt5pbyep3jE +vlhN97z3JUNt2rgUeqpDWuK9zCn9ZIds29HK8Mw2VdlNLw9LpGovvkEuWO/POtwg +drHlatS2tVS4WuU83nJlbx9YHzhlj0SxfxQcHv2sl1pLmvwHjz9az9cEjGI1DIgL +2k0h8QNFAKNufgZEK8Jp/3W0HFXBAYDfw8XklFLFuUTH4K17l3c4MfrmAIowFUKw +yfziCv9YZqZlqee2Hz8utqx26rk++P9wnaHg9SiHWgrClKxcAb13a1jsCW5U5Va+ +olu8ooHXJ6C1mLvv4PuhDl57cQ5euLP4tYWxcwIDAQABAoIBAGs88vaUU+GEdetp +7MXyYb2GyGmbl6svSM8naIiHS+50baeQaZ0euxI2rgmu6ZPu02JT4Mw0vlEGjOAn +S+VagJHSQlHjcK9B+/gs+WTJU13kL98mxZoqyFtiayX9fYNRf8Qy88G+XlZHDNCW +UV/noYpBPukPs2znBC1Lz16X+34bgBn1kydD5XrRi1WujYQV+Bd+qbyd5xfo3ICA +7aLo1mZF5ZRSYXz044w4E8pLzi3SfdvHBGqIHqhu1I7g/aumb//QbPUGinVi0lJp +Gne0fMSWC19Ueg3XUv2YgTOKRZ0teQ4gNTqJZU2omU+tNkQOL8Lem9EXgUr3ULmd +BEeWlgECgYEA72NahW69StWIQrxUB08ePKmYZhtpi7NlGsuMPVsJwIjn50LnmkPx +tb54OROqGYYPKeWj1LDaL8/WKSzU6Xi2JC5c4K7w4GsHP0Fibk7qMs4PI8gHNYUW +OR6SpbkHSj+XaO4Y5ZYxweEddjLmzBl1cU5xQFwE6B4v7QmjssoWcSECgYEAwqDQ +TwT/m0rY5KAZ7C1ziWhzTSnYDYkh8UyeW9jcbjGFafuWk5pbkGGJhiE8N899wfEB +xM44U5zryjWXfNnRVyL/0E0UUaIiEFXmbh9PpiMn67qIm0/bE4cTR5EK+1Cv5lH5 +40PcIAuaHCrwExF2n/o8G1Qw1CtM/2Mh5QwVzBMCgYEAvaj2p+6ztF13JoB1BpFt +gFxRrNwdBByuhssrodTF5i/Ro8zDF9qq3vY34+irhKtMykHxSeqQAv9kSp0FVrBX +hlThKvYPS44gYMJZzNFjBbtyDURGSeXWVIzfXMM6fl3hhLXXLdaQN242XFY5uwyU +mp+uZHJDRO/dwqw3cMhhfWECgYEAwcdhf7uHTzOnXYI2LRtX1HCllCdcUKW/oMNb +TDFPz3sIzWvMESv3jHpAdcZzcVHqcQWncC6ukDlF3m+bY5hvQKW9M8T6vQiDh+Yu +5mfQYqbQIXj4eEJ66idrv66e03gTmmr2NRukeFd02iS6IRIbHM8Ft2QMtcJjCEfI +QIfWz/cCgYEAw/zG/jKrkrk+TSgy6UuMgzFd9I2p8Fi8yl7QZFG2nB7uRtH80FFc +LyvsdfDDV+8NWUNgnKreBFWR5I9oI3vB74PMp27YrBV4L11ne1L5FXKGry5G2D1M +2GgkZSe34gTsmtM1tFF7mBNEs/DfMxURb0jsPw/9zC8ztqA+3QvV7jw= +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.crt new file mode 100644 index 0000000..20f0c48 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIETzCCAjcCCQD0kF4yo85tUjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDQwNzA3MjYy +N1oXDTMwMDQwNTA3MjYyN1owdjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFcmFmYXkxFDASBgNVBAsMC2VuZ2luZWVyaW5n +MScwJQYDVQQDDB4qLmt1YmVjdGwucGVlci5yZWxheS5yYWZheS5kZXYwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAx45z7f2bXFjh0yPRaPqmiJWhRXDf +Q5JiyUlZlBfon12mmMx8C/1RY5NROd7xGN2Fokys7yNLq/PFl0NzoQDseJNO6aTg +wO1gi7MB3roiDFe1FRevsSapwu0ngOPovSShiFKsZOaXUeNoDT1d+eCZd9cu2Pwi +FhxCiblju6EckXKAPJGzD2SknLM8P5ky+n1+5801ebNoobP5E2/XSyaiyNSzqoIs +LkPgdYU9PpBH3CS0YDBSRgmjM1L3R0gO6Oy6KitgUEXXNQ4mCANcJX46bgfjt2iw +fjiiywJjLkRgBkhHUtzFPe19mma1P96a9XRKlrWtyCLWr0TRCxzftwjzAgMBAAEw +DQYJKoZIhvcNAQELBQADggIBAKc7sPjByTWLp/zPU1ALe0vRa1iytnPy1rdUdSeX +kEivS7ktsmPstD8c1jq9MmmkTQaOMTD4jbyd3YFPyhIH8NynI5OtADTdRTH7N2+M +bkDzW+zuGhxReErS7FsDJ30L3H8Ry2Ep2ZBCUD69KglEk7sJLKt5iQtyVAiuoH4Y +qn9xwGXdyAT1NiibepYSr5NNDeOanbeXTFZsGbDgYdE4QdWbIdlaj8aqmY2cw2LW +cflLjTZw0sIILGhJywjVjF29gb/1HykV8/UVYg344moKjjRaYUPFwFR2R/1vGfEB +O4aNM6e/AeBhY5WZIIxCFGQiG9o6QomzEFryD3sbx4V+qiDN3xd5AJEbpTvxOCts +3dUeacxmlaYfDPvJb3qR17sPgjot6Wxz8EcsHxqO/AvcBpcAZbLRsMszv78JICpT +FYnBJreOL6C9I7MjhQRS6DAhOExrCqgYaWxf8vDUXkY/Y0TFERwNsOhO5uIRPmBL +VroyQkamWvEfkobGRe9RL9pi5kxPp6VxColl7T/C53XTCku7il2avER/yTMG+ceB +E+htQLuPBkZt2vaDsvHaFgageFgmUYTps92Ku6cjBX1UYpAuaRIKRwCHLDd3PM27 +QwgrBIKx2LOuMlLpKF/D3emGqZHty4bWJTpXURMUofviZ6mi2kFaqTcySWCzCEuP +9maR +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.csr new file mode 100644 index 0000000..310d916 --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICuzCCAaMCAQAwdjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFcmFmYXkxFDASBgNVBAsMC2VuZ2luZWVyaW5nMScwJQYD +VQQDDB4qLmt1YmVjdGwucGVlci5yZWxheS5yYWZheS5kZXYwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDAx45z7f2bXFjh0yPRaPqmiJWhRXDfQ5JiyUlZ +lBfon12mmMx8C/1RY5NROd7xGN2Fokys7yNLq/PFl0NzoQDseJNO6aTgwO1gi7MB +3roiDFe1FRevsSapwu0ngOPovSShiFKsZOaXUeNoDT1d+eCZd9cu2PwiFhxCiblj +u6EckXKAPJGzD2SknLM8P5ky+n1+5801ebNoobP5E2/XSyaiyNSzqoIsLkPgdYU9 +PpBH3CS0YDBSRgmjM1L3R0gO6Oy6KitgUEXXNQ4mCANcJX46bgfjt2iwfjiiywJj +LkRgBkhHUtzFPe19mma1P96a9XRKlrWtyCLWr0TRCxzftwjzAgMBAAGgADANBgkq +hkiG9w0BAQsFAAOCAQEAimAaHYatwlCfq0OtI3tkASb+KkJxk49INZTswzQvyjvr +6mMFVplX+1fQGAYkZGJSjxwdAt2HnlgEyUybZHFFoBgnc/EQWlXfdY6kWIos5bgD +j3PyEEIQ0tCDnXQrABWZ7FSY7WRzEXmSQQwBznq5bprSKM0k5XBxXRo130xV7zCZ +fm2BTIl4hHc9hCc/UWQ6Z6R+pniiZQEaekZ6SpGjxdss5QaWMcvaDhtHSZi0wmRP +X+SyPsY6v3wv6Xp7cnMRsetthmm0iwy/Ctzk6Stl0KTFPqXoAttR/Oi5ynNT7CgY +KrTFCNr3UCY1xdr8vLYV+6RyIU9NdlgwnYrBiBU6gg== +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.key new file mode 100644 index 0000000..cd5399e --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-peer/star.kubectl.peer.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwMeOc+39m1xY4dMj0Wj6poiVoUVw30OSYslJWZQX6J9dppjM +fAv9UWOTUTne8RjdhaJMrO8jS6vzxZdDc6EA7HiTTumk4MDtYIuzAd66IgxXtRUX +r7EmqcLtJ4Dj6L0koYhSrGTml1HjaA09XfngmXfXLtj8IhYcQom5Y7uhHJFygDyR +sw9kpJyzPD+ZMvp9fufNNXmzaKGz+RNv10smosjUs6qCLC5D4HWFPT6QR9wktGAw +UkYJozNS90dIDujsuiorYFBF1zUOJggDXCV+Om4H47dosH44ossCYy5EYAZIR1Lc +xT3tfZpmtT/emvV0Spa1rcgi1q9E0Qsc37cI8wIDAQABAoIBAQC3M63r/Aqbv1n0 +WdvbIYG6C2jvVveaJTQTTyi2yWcjOXgdX25MoHQ/CXsxoOZ+htvWKFPnsGcBfcrv +Rv3h/+rJzxhm0OFQeefcbUZTi5+lxccAHqhg3eBZShZoCTQNtNn/7HZswILm4PpU +ouhjy5inH91N9wU9O3flv85/ugq1AtmL5jLp97qKyafWgy3nsQrPf5N+k59qCEXI ++giCKg3wKVgsFZW7bm+dBR6pgSvjfMnu3Yp5bJKNRQ4s/0DRcQJ9rsVvcQn5mWGW +NabBZgzb/nHNAEOFvzypinm96h4Ma4afLZhThvFKMpn5pc9+32rq5/nqbK3rg8sX +uNOOWLPpAoGBAOecyPI7nWcibSXsNFHKrAVdH8r7G+hY7v79+xdPx3FpQfSrlTFC +EBOmPBWTxk1Un80Wby8ObL2oqNcbg37Y78OzmEswyNJkcz24OHY3IVF0uCtPJQVw +JN2Z9cse/XOq/9RkSJzNtQOlSsezuDvid7tuWMmbGl6+OypNt+GOS1pfAoGBANUU +A1PVoN8V18rWJEsKpJrUavUaVFTC4D0XeVyV4+6YydP+mw2KdLEO0+xBA0YTUIsM +OLnY0FDPHby3XpjKoorBgGwm+CPbHVkUuqd5wuNn0b9ZdblFDFh0Ps+bzwEXs4d8 +isnPDOQaJuLlf20zTmIqnpZVGbZEb57oLaUhpgHtAoGAVcLlWWSfrQ+/ULp8yQh9 +1+KaoiOHOuqrXHoZHpGo7SKnjHx4Wuv2XgBA4VB9ria2oU88TjVOKJAqLLSAZVjY +tyNqMfVXdyhAz8rGBWf2i13n0AmXUMMss22N6O+Js+nOnwufI4i8DSSIYxf2ER6T +bTC36bbn9csEx25Nt1RkLIsCgYBUQAKEzm46Zw+ZAwz8ouus0Jqt6Ug474jsfvCc +nCOwvrbm3MAAHaPAMoW234nk1lQ9doqjaUgM+6fda3+62DxmstYNbWcl/khfZaPb +/ngBpJY5m6zBobyc6PHA8vFplC6HPsY9v2Fwe6qugmHdErFGfWWROXreJ7ce1Nzf +cyuJjQKBgQCRFhYBHj2yTaDrZjFpP7ybyGejVS2xGQ3+r54YWh1+MA41A0As97Kd +p1xK5KGxsdHOra6ApfsitGa8JjA7owFaMk+zrpCGyoRdoPAhyz/E9JksK8XW90s3 +zQffnMEuOoPp8GN51GlmsF2N5GpyMjUzuebrjTyYR1gX8qKTmERHaQ== +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.crt b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.crt new file mode 100644 index 0000000..3383f9d --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESzCCAjMCCQD0kF4yo85tUTANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDQwMzIzMTY0 +NFoXDTMwMDQwMTIzMTY0NFowcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFUmFmYXkxFDASBgNVBAsMC0VuZ2luZWVyaW5n +MSMwIQYDVQQDDBpzdGFyLnByb2JlLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKVRchQ2YyJTBqoeXiGVgDBm2wxZVIqqOcLP +TVVlRlgsIwboo1kfC1PNAUCcm5BSF1rqE9BZyKynYoOPlXRpI8LYNW+QMXyHg3/U +8chBEuJNkU3tGdbwN3EkuThbWs/7VbKnw/3tS0bjBX858l2yqVWcgKqxH1xe9q4j +f0RrC4+oZffwzSwkvjYTiVURW1Z/qT7VduwbkX6XzKc1JWLWOe5BJvlbZHZ13RWN +vm0lMGdwzAtoIS7lKF0p12kD+kp1EeW+Sz2fYC4jHQq/i8chol+2PrOTe7MUQvO1 +mlJdfbcK7HABU3oDAc3xl6LTRAVPzThHJKoJCj+drXDRrJzECaUCAwEAATANBgkq +hkiG9w0BAQsFAAOCAgEAmhwJ/5fEYevog5AXzFpINkZGxSTSsH9nMMPEjNNDNpfh +AliVNS+LVYISNe6w0vCJCNm2v6C/z/W3XlQ2leCXZd7aChWIVBoLjbg68sNbMyYA +QFSERXkX0sPTskmsz7i26nX/j0BgoJ8WFTwfpc+6JA9QjanTnT/uaCGdoSvdHotJ +1PzfzP/WnY1yq/YS73fRjf8rSaSv8qR1GbdRk2GXABXXwxjSLv3fQK7TzF+Pf7/w +dnzKCNw/4XWyF6PhnDOyRFHrKyqWp6YMctk52WYUpYg0o+zDSG7si7pNW+NJxkde +mnnLvWrgX5BMfSRnY3d0siQdyaGr2447z2Nr5gMNnsWv5NS43KrM+MXFFUUKMwuN +lGE9Y4mScVb9rxouYZ2HCqnL8vi3dK9oq5jMTjF/ytEF4ACprd6WRRlPLX4rR4sp +IXYyCGLRnCRBeMdKa5YDr08YsL3rOCWAc5EByALi/WQgFxr1qctqrylFhH88+eSB +7KfLVl4Ji4ti2ETqwzqtQ2kfcemNgWBwxodnqXpVF197aCiZ3gnjT5t93FSLXRXx +7JubuFzAeIOI/ICo2OiRZ2Gw+ANKSVtWnYODV/RiM/gRDYbv0jCHxA3PCbGJWTd1 +twx1oSo87N8gdC7NRL49bjtc6PRfPwow4/MUDLD0thjh29NaG7DiZf+1IsdGFvo= +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.csr b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.csr new file mode 100644 index 0000000..9c034bc --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICtzCCAZ8CAQAwcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFUmFmYXkxFDASBgNVBAsMC0VuZ2luZWVyaW5nMSMwIQYD +VQQDDBpzdGFyLnByb2JlLnJlbGF5LnJhZmF5LmRldjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKVRchQ2YyJTBqoeXiGVgDBm2wxZVIqqOcLPTVVlRlgs +Iwboo1kfC1PNAUCcm5BSF1rqE9BZyKynYoOPlXRpI8LYNW+QMXyHg3/U8chBEuJN +kU3tGdbwN3EkuThbWs/7VbKnw/3tS0bjBX858l2yqVWcgKqxH1xe9q4jf0RrC4+o +ZffwzSwkvjYTiVURW1Z/qT7VduwbkX6XzKc1JWLWOe5BJvlbZHZ13RWNvm0lMGdw +zAtoIS7lKF0p12kD+kp1EeW+Sz2fYC4jHQq/i8chol+2PrOTe7MUQvO1mlJdfbcK +7HABU3oDAc3xl6LTRAVPzThHJKoJCj+drXDRrJzECaUCAwEAAaAAMA0GCSqGSIb3 +DQEBCwUAA4IBAQB9o+RvCbgUengcfOU8yLPtUwu3vVx0/4flcJvkq4UaYXIo6BqS +Jt97bzOYAc213YeAqzt2ldx50Ja/55CF/z5L0nwviKNk8MRyIlXy6TYPsWsfOG55 +LqOrZEUtAJA9CVrZDt097JWl+LWFKRnV3Y90GzsnS7SRNGrKKZxSbLArh+T8toz/ +zjfK1Q+Gz2TR6UOtc/0/kylon3SwGcJdyESAhQ/RrG10XGwSy30lT0Eyp92I9A/f +z+QJJrNugFkRwpBDzbiLIWjeVJqY3FSnuAqdKPSmsW4jVlzQhcLGIo/tN96BGjt9 +NZZVW0lpRX1K/5vL3fk992zIngep4o3355EL +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.key b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.key new file mode 100644 index 0000000..ed62d6b --- /dev/null +++ b/components/relay/testdata/certs/servercerts/star-probe/star.probe.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEApVFyFDZjIlMGqh5eIZWAMGbbDFlUiqo5ws9NVWVGWCwjBuij +WR8LU80BQJybkFIXWuoT0FnIrKdig4+VdGkjwtg1b5AxfIeDf9TxyEES4k2RTe0Z +1vA3cSS5OFtaz/tVsqfD/e1LRuMFfznyXbKpVZyAqrEfXF72riN/RGsLj6hl9/DN +LCS+NhOJVRFbVn+pPtV27BuRfpfMpzUlYtY57kEm+VtkdnXdFY2+bSUwZ3DMC2gh +LuUoXSnXaQP6SnUR5b5LPZ9gLiMdCr+LxyGiX7Y+s5N7sxRC87WaUl19twrscAFT +egMBzfGXotNEBU/NOEckqgkKP52tcNGsnMQJpQIDAQABAoIBAQCVaoxh6lolLsdK +QJIxZm7q0DvQ6Hg5r+ruwPCTaypuDM5Hs0HYDe/t+M7V9/W+NgP33rKz0EJIkiy+ +3apmAZUZyNJbGAcyF0Tp56vzo6UOtq7KsK53Drv9XgSuBBuK+Q2WoHzK54ru8ccJ +G3WL7CuSPhYosFe44/euolSnoNxdJJGrvO4jCs80BbkMrCqf0CHFLm+WdoCEDDY+ +2g0552tGPMOh7rTRP8myK+uphNZnKH6LJEW9DjfCqbAhAwonJXmQy3yGPd+cfxOw +UtdMnw6gWGQrrX5AQnQPDr7Y38UIKG5hoLohLacPdo3cs9CPA69TD9107392NuHh +OUGFzsNtAoGBANNE48HAKdLSlhhx+hC+TVoz4GTzrHIaBr+R+kEe+jq8BI9tUnX1 +mBdsVi1lss21TQBDxTcMd7kvDQQh/T83XUJxH0XvgDyVaeGIIAAcfhDo5FDT4b5b +VxvfW9ZWn7g5X3qTO289WzuUhZja4l8TV8c4RTUDBUZvNMFIvqCtrhbjAoGBAMhR +8Y2vN2dUNCZNVNRxLMzUroq3b7gZ2/G+AhjXY7RIGeEA62w19wMoX7AFB1ls5QET +Lyt+Tqii3OV8qjPBLm2eg2XiRDr44zTDEtscKUCsP1qBPYPWHgXOo5WTh1MJyvds +GsMVfJ50G0KtXNQlioxAtcty1h+ulcZg0yt4fbvXAoGBAJpVMdgAy8yxf2oeIOII +UrN5QAwfIbM8pf9d4SWfPoaXgPFRmDKJlXBO+Oky6JpzPevalf/vwKnr4CNLvRdo +F12JC6JoIhKfx3g0m8PZDJLHvrn4/miziv/We90o7IaEiuYdFgbeGH5+sdWG2GVB +2Ra0WYIvUbQxWqVzwaXZGlqnAoGBAI6cj+x/nmDouIvAKDFqNSvKCkzlVRrrBgOv +cwy+YtQZNJtULUaHkMhluOG2YsoOVWaTgtRjAprZ/4s9WmqoEGaoIAGruUmGoTKR +4l2tSc0qE5xaQDIYgJ0/xIQbWI8MfLvy8YaLSIICRNoOU/UQmPdKpmIl6jz57NUj +X5MmH9VXAoGAKNLUmkjdZL/1nM4jO27ayp7vNdQ7Jwlpta4LA+BODDnb5KM80EKb ++hIpeBgwytKJItm/PvVValW69iGgUx5Q+gN0sdKk+AtUQ3AvVnZf+C3pmkLqYvRl +ASCXe+3FkuQI6wu8s5E+K5BZa+eHtMr1MlVE3KZ+LQ6Z5XakNAD5seA= +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.crt b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.crt new file mode 100644 index 0000000..20f0c48 --- /dev/null +++ b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIETzCCAjcCCQD0kF4yo85tUjANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExEzARBgNVBAcMClBsZWFzYW50b24xDjAMBgNVBAoMBVJh +ZmF5MQwwCgYDVQQLDANEZXYxDjAMBgNVBAMMBVJhZmF5MB4XDTIwMDQwNzA3MjYy +N1oXDTMwMDQwNTA3MjYyN1owdjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQsw +CQYDVQQHDAJQTDEOMAwGA1UECgwFcmFmYXkxFDASBgNVBAsMC2VuZ2luZWVyaW5n +MScwJQYDVQQDDB4qLmt1YmVjdGwucGVlci5yZWxheS5yYWZheS5kZXYwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAx45z7f2bXFjh0yPRaPqmiJWhRXDf +Q5JiyUlZlBfon12mmMx8C/1RY5NROd7xGN2Fokys7yNLq/PFl0NzoQDseJNO6aTg +wO1gi7MB3roiDFe1FRevsSapwu0ngOPovSShiFKsZOaXUeNoDT1d+eCZd9cu2Pwi +FhxCiblju6EckXKAPJGzD2SknLM8P5ky+n1+5801ebNoobP5E2/XSyaiyNSzqoIs +LkPgdYU9PpBH3CS0YDBSRgmjM1L3R0gO6Oy6KitgUEXXNQ4mCANcJX46bgfjt2iw +fjiiywJjLkRgBkhHUtzFPe19mma1P96a9XRKlrWtyCLWr0TRCxzftwjzAgMBAAEw +DQYJKoZIhvcNAQELBQADggIBAKc7sPjByTWLp/zPU1ALe0vRa1iytnPy1rdUdSeX +kEivS7ktsmPstD8c1jq9MmmkTQaOMTD4jbyd3YFPyhIH8NynI5OtADTdRTH7N2+M +bkDzW+zuGhxReErS7FsDJ30L3H8Ry2Ep2ZBCUD69KglEk7sJLKt5iQtyVAiuoH4Y +qn9xwGXdyAT1NiibepYSr5NNDeOanbeXTFZsGbDgYdE4QdWbIdlaj8aqmY2cw2LW +cflLjTZw0sIILGhJywjVjF29gb/1HykV8/UVYg344moKjjRaYUPFwFR2R/1vGfEB +O4aNM6e/AeBhY5WZIIxCFGQiG9o6QomzEFryD3sbx4V+qiDN3xd5AJEbpTvxOCts +3dUeacxmlaYfDPvJb3qR17sPgjot6Wxz8EcsHxqO/AvcBpcAZbLRsMszv78JICpT +FYnBJreOL6C9I7MjhQRS6DAhOExrCqgYaWxf8vDUXkY/Y0TFERwNsOhO5uIRPmBL +VroyQkamWvEfkobGRe9RL9pi5kxPp6VxColl7T/C53XTCku7il2avER/yTMG+ceB +E+htQLuPBkZt2vaDsvHaFgageFgmUYTps92Ku6cjBX1UYpAuaRIKRwCHLDd3PM27 +QwgrBIKx2LOuMlLpKF/D3emGqZHty4bWJTpXURMUofviZ6mi2kFaqTcySWCzCEuP +9maR +-----END CERTIFICATE----- diff --git a/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.csr b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.csr new file mode 100644 index 0000000..310d916 --- /dev/null +++ b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICuzCCAaMCAQAwdjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQH +DAJQTDEOMAwGA1UECgwFcmFmYXkxFDASBgNVBAsMC2VuZ2luZWVyaW5nMScwJQYD +VQQDDB4qLmt1YmVjdGwucGVlci5yZWxheS5yYWZheS5kZXYwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDAx45z7f2bXFjh0yPRaPqmiJWhRXDfQ5JiyUlZ +lBfon12mmMx8C/1RY5NROd7xGN2Fokys7yNLq/PFl0NzoQDseJNO6aTgwO1gi7MB +3roiDFe1FRevsSapwu0ngOPovSShiFKsZOaXUeNoDT1d+eCZd9cu2PwiFhxCiblj +u6EckXKAPJGzD2SknLM8P5ky+n1+5801ebNoobP5E2/XSyaiyNSzqoIsLkPgdYU9 +PpBH3CS0YDBSRgmjM1L3R0gO6Oy6KitgUEXXNQ4mCANcJX46bgfjt2iwfjiiywJj +LkRgBkhHUtzFPe19mma1P96a9XRKlrWtyCLWr0TRCxzftwjzAgMBAAGgADANBgkq +hkiG9w0BAQsFAAOCAQEAimAaHYatwlCfq0OtI3tkASb+KkJxk49INZTswzQvyjvr +6mMFVplX+1fQGAYkZGJSjxwdAt2HnlgEyUybZHFFoBgnc/EQWlXfdY6kWIos5bgD +j3PyEEIQ0tCDnXQrABWZ7FSY7WRzEXmSQQwBznq5bprSKM0k5XBxXRo130xV7zCZ +fm2BTIl4hHc9hCc/UWQ6Z6R+pniiZQEaekZ6SpGjxdss5QaWMcvaDhtHSZi0wmRP +X+SyPsY6v3wv6Xp7cnMRsetthmm0iwy/Ctzk6Stl0KTFPqXoAttR/Oi5ynNT7CgY +KrTFCNr3UCY1xdr8vLYV+6RyIU9NdlgwnYrBiBU6gg== +-----END CERTIFICATE REQUEST----- diff --git a/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.key b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.key new file mode 100644 index 0000000..cd5399e --- /dev/null +++ b/components/relay/testdata/certs/star.kubectl.peer.relay.rafay.dev.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwMeOc+39m1xY4dMj0Wj6poiVoUVw30OSYslJWZQX6J9dppjM +fAv9UWOTUTne8RjdhaJMrO8jS6vzxZdDc6EA7HiTTumk4MDtYIuzAd66IgxXtRUX +r7EmqcLtJ4Dj6L0koYhSrGTml1HjaA09XfngmXfXLtj8IhYcQom5Y7uhHJFygDyR +sw9kpJyzPD+ZMvp9fufNNXmzaKGz+RNv10smosjUs6qCLC5D4HWFPT6QR9wktGAw +UkYJozNS90dIDujsuiorYFBF1zUOJggDXCV+Om4H47dosH44ossCYy5EYAZIR1Lc +xT3tfZpmtT/emvV0Spa1rcgi1q9E0Qsc37cI8wIDAQABAoIBAQC3M63r/Aqbv1n0 +WdvbIYG6C2jvVveaJTQTTyi2yWcjOXgdX25MoHQ/CXsxoOZ+htvWKFPnsGcBfcrv +Rv3h/+rJzxhm0OFQeefcbUZTi5+lxccAHqhg3eBZShZoCTQNtNn/7HZswILm4PpU +ouhjy5inH91N9wU9O3flv85/ugq1AtmL5jLp97qKyafWgy3nsQrPf5N+k59qCEXI ++giCKg3wKVgsFZW7bm+dBR6pgSvjfMnu3Yp5bJKNRQ4s/0DRcQJ9rsVvcQn5mWGW +NabBZgzb/nHNAEOFvzypinm96h4Ma4afLZhThvFKMpn5pc9+32rq5/nqbK3rg8sX +uNOOWLPpAoGBAOecyPI7nWcibSXsNFHKrAVdH8r7G+hY7v79+xdPx3FpQfSrlTFC +EBOmPBWTxk1Un80Wby8ObL2oqNcbg37Y78OzmEswyNJkcz24OHY3IVF0uCtPJQVw +JN2Z9cse/XOq/9RkSJzNtQOlSsezuDvid7tuWMmbGl6+OypNt+GOS1pfAoGBANUU +A1PVoN8V18rWJEsKpJrUavUaVFTC4D0XeVyV4+6YydP+mw2KdLEO0+xBA0YTUIsM +OLnY0FDPHby3XpjKoorBgGwm+CPbHVkUuqd5wuNn0b9ZdblFDFh0Ps+bzwEXs4d8 +isnPDOQaJuLlf20zTmIqnpZVGbZEb57oLaUhpgHtAoGAVcLlWWSfrQ+/ULp8yQh9 +1+KaoiOHOuqrXHoZHpGo7SKnjHx4Wuv2XgBA4VB9ria2oU88TjVOKJAqLLSAZVjY +tyNqMfVXdyhAz8rGBWf2i13n0AmXUMMss22N6O+Js+nOnwufI4i8DSSIYxf2ER6T +bTC36bbn9csEx25Nt1RkLIsCgYBUQAKEzm46Zw+ZAwz8ouus0Jqt6Ug474jsfvCc +nCOwvrbm3MAAHaPAMoW234nk1lQ9doqjaUgM+6fda3+62DxmstYNbWcl/khfZaPb +/ngBpJY5m6zBobyc6PHA8vFplC6HPsY9v2Fwe6qugmHdErFGfWWROXreJ7ce1Nzf +cyuJjQKBgQCRFhYBHj2yTaDrZjFpP7ybyGejVS2xGQ3+r54YWh1+MA41A0As97Kd +p1xK5KGxsdHOra6ApfsitGa8JjA7owFaMk+zrpCGyoRdoPAhyz/E9JksK8XW90s3 +zQffnMEuOoPp8GN51GlmsF2N5GpyMjUzuebrjTyYR1gX8qKTmERHaQ== +-----END RSA PRIVATE KEY----- diff --git a/components/relay/testdata/client-cluster1.yaml b/components/relay/testdata/client-cluster1.yaml new file mode 100644 index 0000000..d904833 --- /dev/null +++ b/components/relay/testdata/client-cluster1.yaml @@ -0,0 +1,16 @@ + +dialouts: + kubectl: + proto: https + addr: cluster1.kubectldialin.relay.rafay.dev:443 + relay_service_sni: cluster1.kubectl.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + #use this if address is not avaialable from environment variable + upstream_addr: https://relay.rafay.dev:7443 + upstream_skip_verify: true + upstream_client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + upstream_client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + upstream_rootCA: testdata/certs/rootCA.crt + version: 1 \ No newline at end of file diff --git a/components/relay/testdata/client-cluster2.yaml b/components/relay/testdata/client-cluster2.yaml new file mode 100644 index 0000000..1224f08 --- /dev/null +++ b/components/relay/testdata/client-cluster2.yaml @@ -0,0 +1,16 @@ + +dialouts: + kubectl: + proto: https + addr: cluster2.kubectldialin.relay.rafay.dev:443 + relay_service_sni: cluster1.kubectl.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + #use this if address is not avaialable from environment variable + upstream_addr: https://relay.rafay.dev:7443 + upstream_skip_verify: true + upstream_client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + upstream_client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + upstream_rootCA: testdata/certs/rootCA.crt + version: 1 \ No newline at end of file diff --git a/components/relay/testdata/images/testsetup.png b/components/relay/testdata/images/testsetup.png new file mode 100644 index 0000000000000000000000000000000000000000..69ef09fc229e6693813555e0b41716f03b63c11f GIT binary patch literal 185566 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 =A4x)Bv$^K4UF%Zu`fF_#q{L!r}WzPblfK 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 literal 0 HcmV?d00001 diff --git a/components/relay/testdata/kubectlconfigs/kubeconfig b/components/relay/testdata/kubectlconfigs/kubeconfig new file mode 100644 index 0000000..a9fc18b --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfig @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJWekNCL3FBREFnRUNBZ0VBTUFvR0NDcUdTTTQ5QkFNQ01DTXhJVEFmQmdOVkJBTU1HR3N6Y3kxelpYSjIKWlhJdFkyRkFNVFUzTnpneE1qZ3lOakFlRncweE9URXlNekV4TnpJd01qWmFGdzB5T1RFeU1qZ3hOekl3TWpaYQpNQ014SVRBZkJnTlZCQU1NR0dzemN5MXpaWEoyWlhJdFkyRkFNVFUzTnpneE1qZ3lOakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkNicWNTcklQQ2p5amE3cTg2Zk04SDlkMHJuMGlnYmpZaTRKalQ4c1BobTEKNXVVbm5NZHdVTytlRUY4eVU3cEdEUm04S2FSSER2QWF5SU0rbmloTkFsK2pJekFoTUE0R0ExVWREd0VCL3dRRQpBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFEMCtNT2piSXdxCm1ES3YveVVlSkFXVTVSemp0d3hmSHFmSjUwRnIrckxoMmdJZ0JWSjdMYkRwWXZKNEJDT3B5V0puMlJpRTBwY1IKZ0xZaUF1YlJNcXoxYU1jPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://192.168.56.103:6443 # set to your cluser IP + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +kind: Config +preferences: {} +users: +- name: default + user: + password: 15c80273fd3e8daae8a65ba86d8f6001 + username: admin diff --git a/components/relay/testdata/kubectlconfigs/kubeconfig-c-5 b/components/relay/testdata/kubectlconfigs/kubeconfig-c-5 new file mode 100644 index 0000000..f75e970 --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfig-c-5 @@ -0,0 +1,20 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNWakNDQWYyZ0F3SUJBZ0lSQVBmN1cwSVg5NUlsMVA2MXpmcHpVV2d3Q2dZSUtvWkl6ajBFQXdJd2dZb3gKRERBS0JnTlZCQVlUQTFWVFFURVRNQkVHQTFVRUNCTUtRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCeE1KVTNWdQpibmwyWVd4bE1Sb3dHQVlEVlFRS0V4RlNZV1poZVNCVGVYTjBaVzF6SUVsdVl6RVZNQk1HQTFVRUN4TU1VbUZtCllYa2dVMlZ1ZEhKNU1SNHdIQVlEVlFRREV4VnlZV1poZVMxamIzSmxMWEpsYkdGNUxYVnpaWEl3SGhjTk1qQXcKTkRJNE1ESXdOakEyV2hjTk16QXdOREk0TURJd05qQTJXakNCaWpFTU1Bb0dBMVVFQmhNRFZWTkJNUk13RVFZRApWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaGJHVXhHakFZQmdOVkJBb1RFVkpoClptRjVJRk41YzNSbGJYTWdTVzVqTVJVd0V3WURWUVFMRXd4U1lXWmhlU0JUWlc1MGNua3hIakFjQmdOVkJBTVQKRlhKaFptRjVMV052Y21VdGNtVnNZWGt0ZFhObGNqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQQpCR0MzcHp4NkRNalpvRVowSXJLT1p5cHduSWlEcVhZbkRDc0NUSFBZNHZJS3lWSWU3N3JBUDdhYXBvZVZvNjJPCjN0WW1WS0NuSjdUUUpIZjQyc3pkejhXalFqQkFNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWRCZ05WSFNVRUZqQVUKQmdnckJnRUZCUWNEQWdZSUt3WUJCUVVIQXdFd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBS0JnZ3Foa2pPUFFRRApBZ05IQURCRUFpQmE3ZSthKzlSeXhMaDllZnBOT1ErK0hDcHI1SC95d3QwWUFCUFNqV3ZjN0FJZ01kZ0ZaV0R2ClBIMUtZYjB5VUJwRmNPcEVnRFdsdVRXb1NvUU9EclJETzNjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://4qkolkn.user.relay.rafay.local:10002 + #server: https://4qkolkn.user.relay.rafay.local:443 + name: c-5 +contexts: +- context: + cluster: c-5 + user: rafay-core-debug + name: c-5 +current-context: c-5 +kind: Config +preferences: {} +users: +- name: rafay-core-debug + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ6RENDQVhHZ0F3SUJBZ0lRYWhJQW9CZzJVdkV2ZjR6OEZCZkwvVEFLQmdncWhrak9QUVFEQWpDQmlqRU0KTUFvR0ExVUVCaE1EVlZOQk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhFd2xUZFc1dQplWFpoYkdVeEdqQVlCZ05WQkFvVEVWSmhabUY1SUZONWMzUmxiWE1nU1c1ak1SVXdFd1lEVlFRTEV3eFNZV1poCmVTQlRaVzUwY25reEhqQWNCZ05WQkFNVEZYSmhabUY1TFdOdmNtVXRjbVZzWVhrdGRYTmxjakFlRncweU1EQTAKTWpnd05qVXlOREphRncweU1EQTBNamt3TmpVeU5ESmFNQnN4R1RBWEJnTlZCQU1URUhKaFptRjVMV052Y21VdApaR1ZpZFdjd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSemhBWWsyYnB1MnAvTzRwbE5iMmYyCm5PTFoxM2M5cXlEYnBiOGN6RGpsdVpINVdUR0FMNFRQem5RYkFxaUNTNzNFTU4zR1lkRXRqZ0tGZFkzRGp4aWMKb3ljd0pUQU9CZ05WSFE4QkFmOEVCQU1DQmFBd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3SXdDZ1lJS29aSQp6ajBFQXdJRFNRQXdSZ0loQUplbWY2WHFpRDdQV3BuOWNqNVBMVTFJRUQ1VDg2ZFZPWDMyU1dyQXRqazlBaUVBCmkyQWl3Qkk4ZUxRejdWNGF6YVBLLzZwbkZ2c29hWlRINEhZY0JPMnhDOFk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSVB5Q3IzcjJJdkRMREFiUjR1ajdPbVg3bnJtczlSZ0V3TXptRDQyaU1GY0tvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFYzRRR0pObTZidHFmenVLWlRXOW45cHppMmRkM1Bhc2cyNlcvSE13NDVibVIrVmt4Z0MrRQp6ODUwR3dLb2drdTl4RERkeG1IUkxZNENoWFdOdzQ4WW5BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= diff --git a/components/relay/testdata/kubectlconfigs/kubeconfig-sa b/components/relay/testdata/kubectlconfigs/kubeconfig-sa new file mode 100644 index 0000000..66adf6a --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfig-sa @@ -0,0 +1,19 @@ + +apiVersion: v1 +kind: Config +clusters: +- name: default-cluster + cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJWekNCL3FBREFnRUNBZ0VBTUFvR0NDcUdTTTQ5QkFNQ01DTXhJVEFmQmdOVkJBTU1HR3N6Y3kxelpYSjIKWlhJdFkyRkFNVFUzTnpneE1qZ3lOakFlRncweE9URXlNekV4TnpJd01qWmFGdzB5T1RFeU1qZ3hOekl3TWpaYQpNQ014SVRBZkJnTlZCQU1NR0dzemN5MXpaWEoyWlhJdFkyRkFNVFUzTnpneE1qZ3lOakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkNicWNTcklQQ2p5amE3cTg2Zk04SDlkMHJuMGlnYmpZaTRKalQ4c1BobTEKNXVVbm5NZHdVTytlRUY4eVU3cEdEUm04S2FSSER2QWF5SU0rbmloTkFsK2pJekFoTUE0R0ExVWREd0VCL3dRRQpBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFEMCtNT2piSXdxCm1ES3YveVVlSkFXVTVSemp0d3hmSHFmSjUwRnIrckxoMmdJZ0JWSjdMYkRwWXZKNEJDT3B5V0puMlJpRTBwY1IKZ0xZaUF1YlJNcXoxYU1jPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://192.168.56.103:6443 +contexts: +- name: default-context + context: + cluster: default-cluster + namespace: default + user: default-user +current-context: default-context +users: +- name: default-user + user: + token: eyJhbGciOiJSUzI1NiIsImtpZCI6InBYSV8yNW81MXpaTnN4WGNwcGNYdHoxUUFxeklHenZBRmhyRTAyU211ZHMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6Im5hbWVzcGFjZS1hZG1pbi1zYS10b2tlbi14ajdubSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJuYW1lc3BhY2UtYWRtaW4tc2EiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJjMjNlODc1Yi1kM2EzLTQ2YjEtOThlOS03YjkzODc0MGRkOTAiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpuYW1lc3BhY2UtYWRtaW4tc2EifQ.EdgMATO61V4katN3heaKdhPbyVd0vqc6FhG9ULmwZeXcQqgfZbnDIoOHbkCIPLCoL3CDfci-0JpiVmtG4_N3qxxZcyySsaHnWnyzaYEW0_FNjmtvKW13nwwM0t29iKu_AAaF-qk1dgJwvnQCR_2-nUb1NSjf_Hly25DkstHrsKfDlZM-fYR9HDlMdRXJ8xVX9YiMkAlOifgjqcQpzheoOxRd_yjGUmZVJUHBrjcT-1OdBmvhDjMU48dfdxadWQoymDaGMUbRsNKtPdrfMSrO_HBma5B_7HVJ2PbwQLice7ENltuue6cqR0GiYCKmMrBwDk6m1YweSZvaQIw5jv166A diff --git a/components/relay/testdata/kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local new file mode 100644 index 0000000..ce628de --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Config +clusters: +- name: default-cluster + cluster: + server: https://123abcd.user.relay.rafay.local:443 + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNWakNDQWYyZ0F3SUJBZ0lSQU5yYkV6ZDlsYm4yc0xXb3J1ZEMyZWN3Q2dZSUtvWkl6ajBFQXdJd2dZb3gKRERBS0JnTlZCQVlUQTFWVFFURVRNQkVHQTFVRUNCTUtRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCeE1KVTNWdQpibmwyWVd4bE1Sb3dHQVlEVlFRS0V4RlNZV1poZVNCVGVYTjBaVzF6SUVsdVl6RVZNQk1HQTFVRUN4TU1VbUZtCllYa2dVMlZ1ZEhKNU1SNHdIQVlEVlFRREV4VnlZV1poZVMxamIzSmxMWEpsYkdGNUxYVnpaWEl3SGhjTk1qQXcKTkRJeU1EYzFOakkxV2hjTk16QXdOREl5TURjMU5qSTFXakNCaWpFTU1Bb0dBMVVFQmhNRFZWTkJNUk13RVFZRApWUVFJRXdwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaGJHVXhHakFZQmdOVkJBb1RFVkpoClptRjVJRk41YzNSbGJYTWdTVzVqTVJVd0V3WURWUVFMRXd4U1lXWmhlU0JUWlc1MGNua3hIakFjQmdOVkJBTVQKRlhKaFptRjVMV052Y21VdGNtVnNZWGt0ZFhObGNqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQQpCQnBYL2prbW5sNml2RzVrbndrcE1qa24vYmxuWk5tVjlLQTN6R00rTTZKOE5EVUNPT294QURSUmRIZFZOajltCjdTY0pKdlpJczczMk5kbjVreXFndEJlalFqQkFNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWRCZ05WSFNVRUZqQVUKQmdnckJnRUZCUWNEQWdZSUt3WUJCUVVIQXdFd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBS0JnZ3Foa2pPUFFRRApBZ05IQURCRUFpQkZhWHNtaWlicFpWVzNLZ3RvRm5OODdhQWZuaFZETXgvUmpSeDEzQjJNMEFJZ0FhTzZFUFMvClk4azZMSVhIMWVEMmxOdEsrVi9pMHhFQndEVVA3L0tyV2Y0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +contexts: +- name: default-context + context: + cluster: default-cluster + namespace: default + user: default-user +current-context: default-context +users: +- name: default-user + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUIyRENDQVg2Z0F3SUJBZ0lSQUxFR01TQUErajFLRCtuNllibUdrUW93Q2dZSUtvWkl6ajBFQXdJd2dZb3gKRERBS0JnTlZCQVlUQTFWVFFURVRNQkVHQTFVRUNCTUtRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCeE1KVTNWdQpibmwyWVd4bE1Sb3dHQVlEVlFRS0V4RlNZV1poZVNCVGVYTjBaVzF6SUVsdVl6RVZNQk1HQTFVRUN4TU1VbUZtCllYa2dVMlZ1ZEhKNU1SNHdIQVlEVlFRREV4VnlZV1poZVMxamIzSmxMWEpsYkdGNUxYVnpaWEl3SGhjTk1qQXcKTkRJME1ETXlPREExV2hjTk16QXdOREkwTURNeU9EQTFXakE4TVIwd0d3WURWUVFMRXhSaWNXWjJhR0ZpYVhCMApNMkV5WnpRMk9UZzNaekViTUJrR0ExVUVBeE1TYm1GdFpYTndZV05sTFdGa2JXbHVMWE5oTUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5lWGhyNTh3dzlRam5mSHRyd29yNlB3MlM3R0NrZ0plMlpMSm43bGUKSUxMVkVJcVVzUGVnY0Jvdzd3Q005ZTcrSjR5M1RpOEREa0U4R2NVcXU0UmVHS01TTUJBd0RnWURWUjBQQVFILwpCQVFEQWdXZ01Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRRGVCTmQvcXV2SVprN3pib3Jsb1dPTldkRVA3eDlCCjQ5eEhWMStISzU2ZHN3SWdmTXVVSFA2MVdJS1F5c1ZKdjFkbU1CeDUwdEUzRWJIU3piVlZJdURscTlnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhRYVBBVGZUZW1uNzZ5bEg3TCs5d1VmTlFVYUljQWE4eERndmZyMnhKOTBvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFTmVYaHI1OHd3OVFqbmZIdHJ3b3I2UHcyUzdHQ2tnSmUyWkxKbjdsZUlMTFZFSXFVc1BlZwpjQm93N3dDTTllNytKNHkzVGk4RERrRThHY1VxdTRSZUdBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= \ No newline at end of file diff --git a/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster1 b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster1 new file mode 100644 index 0000000..fe80584 --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster1 @@ -0,0 +1,24 @@ + +apiVersion: v1 +kind: Config +clusters: +- name: default-cluster + cluster: + server: https://123abcd.user.relay.rafay.local:443 + certificate-authority: /var/relayuserroot.crt +# server: https://cluster1.kubectl.relay.rafay.dev:443 +# certificate-authority: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/rootCA.crt +contexts: +- name: default-context + context: + cluster: default-cluster + namespace: default + user: default-user +current-context: default-context +users: +- name: default-user + user: + client-certificate: /var/relayuser.crt + client-key: /var/relayuser.key +# client-certificate: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt +# client-key: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/star.namespace-admin-sa.key diff --git a/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster2 b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster2 new file mode 100644 index 0000000..d3069fa --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeconfigproxy-cluster2 @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://cluster2.kubectl.relay.rafay.dev:443 + certificate-authority: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/rootCA.crt + name: default +contexts: +- context: + cluster: default + user: namespace-admin-sa + name: default +current-context: default +kind: Config +preferences: {} +users: +- name: namespace-admin-sa + user: + client-certificate: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/star.namespace-admin-sa.crt + client-key: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/star.namespace-admin-sa.key + diff --git a/components/relay/testdata/kubectlconfigs/kubeproxyconfig b/components/relay/testdata/kubectlconfigs/kubeproxyconfig new file mode 100644 index 0000000..d9a30bb --- /dev/null +++ b/components/relay/testdata/kubectlconfigs/kubeproxyconfig @@ -0,0 +1,23 @@ +apiVersion: v1 +clusters: +- cluster: + #certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJWekNCL3FBREFnRUNBZ0VBTUFvR0NDcUdTTTQ5QkFNQ01DTXhJVEFmQmdOVkJBTU1HR3N6Y3kxelpYSjIKWlhJdFkyRkFNVFUzTnpneE1qZ3lOakFlRncweE9URXlNekV4TnpJd01qWmFGdzB5T1RFeU1qZ3hOekl3TWpaYQpNQ014SVRBZkJnTlZCQU1NR0dzemN5MXpaWEoyWlhJdFkyRkFNVFUzTnpneE1qZ3lOakJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkNicWNTcklQQ2p5amE3cTg2Zk04SDlkMHJuMGlnYmpZaTRKalQ4c1BobTEKNXVVbm5NZHdVTytlRUY4eVU3cEdEUm04S2FSSER2QWF5SU0rbmloTkFsK2pJekFoTUE0R0ExVWREd0VCL3dRRQpBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFEMCtNT2piSXdxCm1ES3YveVVlSkFXVTVSemp0d3hmSHFmSjUwRnIrckxoMmdJZ0JWSjdMYkRwWXZKNEJDT3B5V0puMlJpRTBwY1IKZ0xZaUF1YlJNcXoxYU1jPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + certificate-authority: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/rootCA.crt + server: https://cluster1.kubectl.relay.rafay.dev:443 + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +kind: Config +preferences: {} +users: +- name: default + user: + client-certificate: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + client-key: /Users/stephanbenny/code/src/github.com/RafaySystems/rafay-relay/testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + #password: 15c80273fd3e8daae8a65ba86d8f6001 + #sername: admin + diff --git a/components/relay/testdata/pod.yaml b/components/relay/testdata/pod.yaml new file mode 100644 index 0000000..1947719 --- /dev/null +++ b/components/relay/testdata/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: logs-inspect + namespace: rafay-system +spec: + containers: + - image: alpine:3.12 + name: logs + command: ["tail", "-f", "/dev/null"] + volumeMounts: + - mountPath: /opt/rafay/audit + name: audit-volume + volumes: + - name: audit-volume + persistentVolumeClaim: + claimName: rafay-relay-audit diff --git a/components/relay/testdata/relay-agent-config.yaml b/components/relay/testdata/relay-agent-config.yaml new file mode 100644 index 0000000..0a595bc --- /dev/null +++ b/components/relay/testdata/relay-agent-config.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +data: + clusterID: q72dg2g + relays: '[{"token":"bqjjlq6g10l2b7fm3v50","addr":"api.sentry.rafay.local:11000","endpoint":"*.core-connector.relay.rafay.local:10002","name":"rafay-core-relay-agent","templateToken":"bqjjln6g10l2lvudmup0"}]' +kind: ConfigMap +metadata: + name: relay-agent-config + namespace: rafay-system diff --git a/components/relay/testdata/role.yaml b/components/relay/testdata/role.yaml new file mode 100644 index 0000000..7192139 --- /dev/null +++ b/components/relay/testdata/role.yaml @@ -0,0 +1,70 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: default + namespace: rafay-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: relay-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- nonResourceURLs: + - '*' + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: relay-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: relay-role +subjects: +- kind: ServiceAccount + name: default + namespace: rafay-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rafay-core-debug + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: core-debug-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' +- nonResourceURLs: + - '*' + verbs: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: core-debug-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: relay-role +subjects: +- kind: ServiceAccount + name: rafay-core-debug + namespace: default diff --git a/components/relay/testdata/sample-kubeconfig.yaml b/components/relay/testdata/sample-kubeconfig.yaml new file mode 100644 index 0000000..d9a9770 --- /dev/null +++ b/components/relay/testdata/sample-kubeconfig.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNWakNDQWZ5Z0F3SUJBZ0lRSGdlWXlLaHlLWXNIY3E2TXNybG9HakFLQmdncWhrak9QUVFEQWpDQmlqRU0KTUFvR0ExVUVCaE1EVlZOQk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhFd2xUZFc1dQplWFpoYkdVeEdqQVlCZ05WQkFvVEVWSmhabUY1SUZONWMzUmxiWE1nU1c1ak1SVXdFd1lEVlFRTEV3eFNZV1poCmVTQlRaVzUwY25reEhqQWNCZ05WQkFNVEZYSmhabUY1TFdOdmNtVXRjbVZzWVhrdGRYTmxjakFlRncweU1EQTAKTWpjeU1EQTBORFJhRncwek1EQTBNamN5TURBME5EUmFNSUdLTVF3d0NnWURWUVFHRXdOVlUwRXhFekFSQmdOVgpCQWdUQ2tOaGJHbG1iM0p1YVdFeEVqQVFCZ05WQkFjVENWTjFibTU1ZG1Gc1pURWFNQmdHQTFVRUNoTVJVbUZtCllYa2dVM2x6ZEdWdGN5QkpibU14RlRBVEJnTlZCQXNUREZKaFptRjVJRk5sYm5SeWVURWVNQndHQTFVRUF4TVYKY21GbVlYa3RZMjl5WlMxeVpXeGhlUzExYzJWeU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRQpqUDh3V3lBNHNGdkh0ZlRXd1E5U3d0MmlzMWZPVi80dXVHY0VJcHhob2hOdVppeWcydElkOWtKNnY3OHpKUStGCkJQNEU0bkc0OFE0M0t2elhnS1JYVktOQ01FQXdEZ1lEVlIwUEFRSC9CQVFEQWdLRU1CMEdBMVVkSlFRV01CUUcKQ0NzR0FRVUZCd01DQmdnckJnRUZCUWNEQVRBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5QkFNQwpBMGdBTUVVQ0lRQy9tTmZiamhUVWN4L1BtbFdLSzYxMk9XejJNaTRqdytqVnN6SXZ2Z3lYWkFJZ2RtQ0wvTjd6CkMvSk1raDVkamx5RFhrMU53cUVUS04xeU52NVZpSjBGVlA0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://q72dg2g.user.relay.rafay.local:10002 + name: c-5 +contexts: +- context: + cluster: c-5 + user: rafay-core-debug + name: c-5 +current-context: c-5 +kind: Config +preferences: {} +users: +- name: rafay-core-debug + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ5ekNDQVhHZ0F3SUJBZ0lRZS8wbHdYYVZLR0pkcXBnSnFFQUNxakFLQmdncWhrak9QUVFEQWpDQmlqRU0KTUFvR0ExVUVCaE1EVlZOQk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhFd2xUZFc1dQplWFpoYkdVeEdqQVlCZ05WQkFvVEVWSmhabUY1SUZONWMzUmxiWE1nU1c1ak1SVXdFd1lEVlFRTEV3eFNZV1poCmVTQlRaVzUwY25reEhqQWNCZ05WQkFNVEZYSmhabUY1TFdOdmNtVXRjbVZzWVhrdGRYTmxjakFlRncweU1EQTAKTWpjeU16QTVNamxhRncweU1EQTBNamd5TXpBNU1qbGFNQnN4R1RBWEJnTlZCQU1URUhKaFptRjVMV052Y21VdApaR1ZpZFdjd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFRT2Z5bmZHR2NyYkgzRS9zSG12YmU2CjBtN1FuKzYyT0orRlhxWFhPREU2SzFuQzZSaVgzaUd1aDJjMlg0aGkyNk5ONk1aM1Uxemx5V3Rnb3dkazhzUDkKb3ljd0pUQU9CZ05WSFE4QkFmOEVCQU1DQmFBd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3SXdDZ1lJS29aSQp6ajBFQXdJRFNBQXdSUUloQUk1MTRLOHNXNE8wWnRqbkM5Nkp3dGRENlFCRUx6d2szMWtCUW8zaEw3MnZBaUEyCkE5YUgxVWFnaGxadnNybXpzM0UzaG1UdHJxSG9xZFZyQnVhMXFQclRWQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUxsSW00Tk52VElVOFV3OTd3TnlKSmRZWjk0TmM2NGw1Y3lsRWRINjFONW9vQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFRG44cDN4aG5LMng5eFA3QjVyMjN1dEp1MEovdXRqaWZoVjZsMXpneE9pdFp3dWtZbDk0aApyb2RuTmwrSVl0dWpUZWpHZDFOYzVjbHJZS01IWlBMRC9RPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= diff --git a/components/relay/testdata/scripts/dialinclient.sh b/components/relay/testdata/scripts/dialinclient.sh new file mode 100644 index 0000000..1e6c7b1 --- /dev/null +++ b/components/relay/testdata/scripts/dialinclient.sh @@ -0,0 +1 @@ +curl -kv --cert ../certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt --key ../certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key https://cluster1.kubectldialin.relay.rafay.dev/ diff --git a/components/relay/testdata/scripts/loop.sh b/components/relay/testdata/scripts/loop.sh new file mode 100755 index 0000000..b2df53a --- /dev/null +++ b/components/relay/testdata/scripts/loop.sh @@ -0,0 +1,4 @@ +for ((n=0;n<1000;n++)) +do +KUBECONFIG=/tmp/kc-test.yaml kubectl get all -A +done \ No newline at end of file diff --git a/components/relay/testdata/scripts/relayclient.sh b/components/relay/testdata/scripts/relayclient.sh new file mode 100644 index 0000000..f5fc701 --- /dev/null +++ b/components/relay/testdata/scripts/relayclient.sh @@ -0,0 +1,16 @@ +#curl -kv --cert ../certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt --key ../certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key https://cluster1.kubectl.relay.rafay.dev/ + + +#curl -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key https://cluster1.kubectl.relay.rafay.dev/api?timeout=32s + + +#curl --http1.1 -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key https://cluster1.kubectl.relay.rafay.dev/apis/rbac.authorization.k8s.io/v1/?timeout=32s + + +#curl --http1.1 -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key https://cluster1.kubectl.relay.rafay.dev/ + +curl --http1.1 -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key POST --data '{"username":"xyz","password":"xyz"}' https://cluster1.kubectl.relay.rafay.dev/apis/rbac.authorization.k8s.io/v1/?timeout=32s + +#curl -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key POST --data '{"username":"xyz","password":"xyz"}' https://cluster1.kubectl.relay.rafay.dev/apis/rbac.authorization.k8s.io/v1?timeout=32s + +#curl --http1.1 -kv --cert ../certs/clientcerts/star.namespace-admin-sa.crt --key ../certs/clientcerts/star.namespace-admin-sa.key POST --data '{"username":"xyz","password":"xyz"}' https://cluster1.kubectl.relay.rafay.dev/api/v1/namespaces/default/pods/myapp1-pod/exec?command=%2Fbin%2Fsh&container=myapp1-container&stdin=true&stdout=true&tty=true \ diff --git a/components/relay/testdata/server.yaml b/components/relay/testdata/server.yaml new file mode 100644 index 0000000..52c418e --- /dev/null +++ b/components/relay/testdata/server.yaml @@ -0,0 +1,33 @@ + + +# sudo ifconfig lo0 alias 127.0.10.1 +relay_listen_addr: 127.0.10.1:443 + +relays: + kubectl: + proto: https + addr: :443 + server_name: kubectl.relay.rafay.dev + dialin_suffix: kubectldialin.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + server_crt: testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt + server_key: testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key + version: 1 + + +dialins: + kubectldialin: + proto: https + addr: :443 + server_name: kubectldialin.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + server_crt: testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt + server_key: testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key + version: 1 + +controller: + addr: + peer_probe_sni: star.probe.relay.rafay.dev:7001 + client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + root_ca: testdata/certs/rootCA.crt \ No newline at end of file diff --git a/components/relay/testdata/server2.yaml b/components/relay/testdata/server2.yaml new file mode 100644 index 0000000..1c2dcae --- /dev/null +++ b/components/relay/testdata/server2.yaml @@ -0,0 +1,33 @@ + + +# sudo ifconfig lo0 alias 127.0.20.1 +relay_listen_ip: 127.0.20.1:443 + +relays: + kubectl: + proto: https + addr: :443 + server_name: kubectl.relay.rafay.dev + dialin_suffix: kubectldialin.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + server_crt: testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.crt + server_key: testdata/certs/servercerts/star-kubectl/star.kubectl.relay.rafay.dev.key + version: 1 + + +dialins: + kubectldialin: + proto: https + addr: :443 + server_name: kubectldialin.relay.rafay.dev + root_ca: testdata/certs/rootCA.crt + server_crt: testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.crt + server_key: testdata/certs/servercerts/star-kubectldialin/star.kubectldialin.relay.rafay.dev.key + version: 1 + +controller: + addr: + peer_probe_sni: star.probe.relay.rafay.dev:7001 + client_crt: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.crt + client_key: testdata/certs/clientcerts/relayserver1-ABCD-123456.relay.rafay.dev.key + root_ca: testdata/certs/rootCA.crt \ No newline at end of file diff --git a/components/relay/testdata/testclients/loadtest.go b/components/relay/testdata/testclients/loadtest.go new file mode 100644 index 0000000..978cdae --- /dev/null +++ b/components/relay/testdata/testclients/loadtest.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "fmt" + "math/rand" + "os/exec" + "time" +) + +var min = 10 +var max = 50 + +func worker1(ctx context.Context, index int) { + var errCnt = 0 + var success = 0 + rand.Seed(time.Now().UnixNano()) + + for { + select { + case <-ctx.Done(): + time.Sleep(10 * time.Second) + fmt.Println("worker1 ", index, " done: success", success, "errcnt ", errCnt) + return + default: + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "../kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local", "get", "pods", "-o", "yaml").Output() + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-5", "get", "all", "-A").Output() + out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-5", "get", "pods").Output() + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-5", "get", "pods", "-o", "yaml").Output() + if err != nil { + errCnt++ + } else { + success++ + } + intvl := time.Duration(rand.Intn(max-min+1)+min) * time.Millisecond + time.Sleep(intvl) + fmt.Printf("worker1[%d][%d][%d]: %s\n", index, success, errCnt, out) + } + } +} + +func worker2(ctx context.Context, index int) { + var errCnt = 0 + var success = 0 + rand.Seed(time.Now().UnixNano()) + + for { + select { + case <-ctx.Done(): + time.Sleep(10 * time.Second) + fmt.Println("worker2 ", index, " done: success", success, "errcnt ", errCnt) + return + default: + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "../kubectlconfigs/kubeconfigproxy-123abcd.user.relay.rafay.local", "get", "pods", "-o", "yaml").Output() + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-6", "get", "all", "-A").Output() + out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-6", "get", "pods").Output() + //out, err := exec.Command("/usr/local/bin/kubectl", "--kubeconfig", "/Users/stephanbenny/WorkSpace/kubeconfig-c-5", "get", "pods", "-o", "yaml").Output() + if err != nil { + errCnt++ + } else { + success++ + } + intvl := time.Duration(rand.Intn(max-min+1)+min) * time.Millisecond + time.Sleep(intvl) + fmt.Printf("worker2[%d][%d][%d]: %s\n", index, success, errCnt, out) + } + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + for i := 1; i <= 10; i++ { + //go worker2(ctx, i) + go worker1(ctx, i) + + } + + time.Sleep(120 * time.Minute) + + cancel() + + time.Sleep(15 * time.Second) + +} diff --git a/components/relay/testdata/testclients/testclient.go b/components/relay/testdata/testclients/testclient.go new file mode 100644 index 0000000..1bb9bc1 --- /dev/null +++ b/components/relay/testdata/testclients/testclient.go @@ -0,0 +1,78 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "os" + "strconv" + "sync" + "testing" + "time" +) + +const ( + //UNIXSOCKET .. + UNIXSOCKET = "/tmp/relay-unix-" +) + +func startClient(wg *sync.WaitGroup, buf []byte) { + defer wg.Done() + + fmt.Print("stat client") + buf3 := make([]byte, 4096) + // connect to this socket + socketPath := UNIXSOCKET + "kubectldialin.relay.rafay.dev" + conn, err := net.DialTimeout("unix", socketPath, 60*time.Second) + if err != nil { + fmt.Println("failed to connect ", err) + return + } + + fmt.Println("Connected to ", socketPath) + + for { + conn.Write(buf) + str := "GET /apis/rbac.authorization.k8s.io/v1?timeout=32s HTTP/1.1\r\nHost: 192.168.56.103:6443\r\nUser-Agent: curl/7.54.0\r\nAccept: */*\r\nX-Rafay-User:namespace-admin-sa\r\nX-Rafay-Namespace: default\r\n\r\n" + conn.Write([]byte(str)) + + conn.SetReadDeadline(time.Now().Add(10 * (time.Second))) + + for { + nr, err := conn.Read(buf3) + if err != nil { + fmt.Println("done reading ", err) + return + } + data := buf3[0:nr] + println("Client got:", string(data), nr) + } + } +} + +func TestRelayClient(t *testing.T) { + var wg sync.WaitGroup + + buf := make([]byte, 1024) + reader := bufio.NewReader(os.Stdin) + fmt.Print("Number of clients: ") + numStr, _ := reader.ReadString('\n') + fmt.Print("Key to send: ") + key, _ := reader.ReadString('\n') + + message := fmt.Sprintf("{\"DialinKey\": \"%s\", \"UserName\": \"namespace-admin-sa\", \"SNI\": \"cluster1.kubectldialin.relay.rafay.dev\"}", key[:len(key)-1]) + copy(buf, message) + + fmt.Print("numStr=", numStr) + num, err := strconv.Atoi(numStr[:len(numStr)-1]) + fmt.Println("num=", num, " err=", err) + for i := 0; i < num; i++ { + wg.Add(1) + go startClient(&wg, buf) + } + + fmt.Print("Waiting for workers") + wg.Wait() + + fmt.Print("Completed") +}