feat: implement new discovery service

This includes new in-memory core, new gRPC API, tests, etc.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
This commit is contained in:
Andrey Smirnov
2021-09-22 23:08:22 +03:00
parent 1a43970826
commit 7174ec1042
38 changed files with 5644 additions and 1234 deletions

View File

@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:22:39Z by kres 907039b.
# Generated on 2021-09-22T21:32:59Z by kres 2a27963-dirty.
codecov:
require_ci_to_pass: false
@ -9,7 +9,7 @@ coverage:
status:
project:
default:
target: 50%
target: 15%
threshold: 0.5%
base: auto
if_ci_failed: success

View File

@ -1,12 +1,16 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:22:39Z by kres 907039b.
# Generated on 2021-09-21T18:51:27Z by kres 7412de4-dirty.
---
policies:
- type: commit
spec:
dco: true
gpg: false
gpg:
required: true
identity:
gitHubOrganization: talos-systems
spellcheck:
locale: US
maximumOfOneCommit: true

View File

@ -1,11 +1,12 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:32:42Z by kres 907039b.
# Generated on 2021-09-22T18:49:05Z by kres 2a27963-dirty.
**
!api
!cmd
!internal
!pkg
!cmd
!go.mod
!go.sum
!.golangci.yml

View File

@ -1,7 +1,7 @@
---
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:23:41Z by kres 907039b.
# Generated on 2021-09-21T13:33:04Z by kres 7412de4-dirty.
kind: pipeline
type: kubernetes
@ -105,11 +105,11 @@ steps:
depends_on:
- unit-tests
- name: kubespan-manager
- name: discovery-service
pull: always
image: autonomy/build-container:latest
commands:
- make kubespan-manager
- make discovery-service
volumes:
- name: outer-docker-socket
path: /var/outer-run
@ -139,11 +139,11 @@ steps:
depends_on:
- base
- name: image-kubespan-manager
- name: image-discovery-service
pull: always
image: autonomy/build-container:latest
commands:
- make image-kubespan-manager
- make image-discovery-service
volumes:
- name: outer-docker-socket
path: /var/outer-run
@ -154,16 +154,16 @@ steps:
- name: ssh
path: /root/.ssh
depends_on:
- kubespan-manager
- discovery-service
- lint
- unit-tests
- name: push-kubespan-manager
- name: push-discovery-service
pull: always
image: autonomy/build-container:latest
commands:
- docker login ghcr.io --username "$${GHCR_USERNAME}" --password "$${GHCR_PASSWORD}"
- make image-kubespan-manager
- make image-discovery-service
environment:
GHCR_PASSWORD:
from_secret: ghcr_token
@ -184,14 +184,14 @@ steps:
exclude:
- pull_request
depends_on:
- image-kubespan-manager
- image-discovery-service
- name: push-kubespan-manager-latest
- name: push-discovery-service-latest
pull: always
image: autonomy/build-container:latest
commands:
- docker login ghcr.io --username "$${GHCR_USERNAME}" --password "$${GHCR_PASSWORD}"
- make image-kubespan-manager TAG=latest
- make image-discovery-service TAG=latest
environment:
GHCR_PASSWORD:
from_secret: ghcr_token
@ -214,7 +214,7 @@ steps:
exclude:
- pull_request
depends_on:
- push-kubespan-manager
- push-discovery-service
- name: release-notes
pull: always
@ -236,8 +236,8 @@ steps:
depends_on:
- unit-tests
- coverage
- kubespan-manager
- image-kubespan-manager
- discovery-service
- image-discovery-service
- lint
- name: release
@ -270,7 +270,7 @@ steps:
services:
- name: docker
image: docker:19.03-dind
image: docker:20.10-dind
entrypoint:
- dockerd
commands:

View File

@ -1,7 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:32:42Z by kres 907039b.
# Generated on 2021-09-21T13:11:46Z by kres 7412de4-dirty.
# options for analysis running
run:
@ -22,7 +21,6 @@ output:
uniq-by-line: true
path-prefix: ""
# all available settings of specific linters
linters-settings:
dogsled:
@ -38,7 +36,7 @@ linters-settings:
lines: 60
statements: 40
gci:
local-prefixes: github.com/talos-systems/kubespan-manager
local-prefixes: github.com/talos-systems/discovery-service
gocognit:
min-complexity: 30
nestif:
@ -60,7 +58,7 @@ linters-settings:
gofmt:
simplify: true
goimports:
local-prefixes: github.com/talos-systems/kubespan-manager
local-prefixes: github.com/talos-systems/discovery-service
golint:
min-confidence: 0.8
gomnd:
@ -116,23 +114,30 @@ linters-settings:
linters:
enable-all: true
disable:
- gas
- typecheck
- gochecknoglobals
- gochecknoinits
- funlen
- godox
- gomnd
- goerr113
- nestif
- wrapcheck
- paralleltest
- exhaustivestruct
- forbidigo
disable-all: false
fast: false
disable:
- exhaustivestruct
- forbidigo
- funlen
- gas
- gochecknoglobals
- gochecknoinits
- godox
- goerr113
- gomnd
- gomoddirectives
- nestif
- paralleltest
- tagliatelle
- thelper
- typecheck
- wrapcheck
# abandoned linters for which golangci shows the warning that the repo is archived by the owner
- interfacer
- maligned
- golint
- scopelint
issues:
exclude: []
@ -141,10 +146,8 @@ issues:
exclude-case-sensitive: false
max-issues-per-linter: 10
max-same-issues: 3
new: false
severity:
default-severity: error
case-sensitive: false

14
.kres.yaml Normal file
View File

@ -0,0 +1,14 @@
---
kind: golang.Protobuf
spec:
vtProtobufEnabled: true
specs:
- source: https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/duration.proto
subdirectory: vendor/google
skipCompile: true
- source: api/v1alpha1/cluster.proto
subdirectory: v1alpha1/pb
---
kind: service.CodeCov
spec:
targetThreshold: 15

View File

@ -2,13 +2,10 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:32:42Z by kres 907039b.
# Generated on 2021-09-22T18:49:05Z by kres 2a27963-dirty.
ARG TOOLCHAIN
# cleaned up specs and compiled versions
FROM scratch AS generate
FROM ghcr.io/talos-systems/ca-certificates:v0.3.0-12-g90722c3 AS image-ca-certificates
FROM ghcr.io/talos-systems/fhs:v0.3.0-12-g90722c3 AS image-fhs
@ -22,6 +19,11 @@ COPY .markdownlint.json .
COPY ./README.md ./README.md
RUN markdownlint --ignore "CHANGELOG.md" --ignore "**/node_modules/**" --ignore '**/hack/chglog/**' --rules /node_modules/sentences-per-line/index.js .
# collects proto specs
FROM scratch AS proto-specs
ADD https://raw.githubusercontent.com/protocolbuffers/protobuf/master/src/google/protobuf/duration.proto /api/vendor/google/
ADD api/v1alpha1/cluster.proto /api/v1alpha1/pb/
# base toolchain image
FROM ${TOOLCHAIN} AS toolchain
RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev
@ -31,12 +33,22 @@ FROM toolchain AS tools
ENV GO111MODULE on
ENV CGO_ENABLED 0
ENV GOPATH /go
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b /bin v1.38.0
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b /bin v1.42.1
ARG GOFUMPT_VERSION
RUN cd $(mktemp -d) \
&& go mod init tmp \
&& go get mvdan.cc/gofumpt/gofumports@${GOFUMPT_VERSION} \
RUN go install mvdan.cc/gofumpt/gofumports@${GOFUMPT_VERSION} \
&& mv /go/bin/gofumports /bin/gofumports
ARG PROTOBUF_GO_VERSION
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOBUF_GO_VERSION}
RUN mv /go/bin/protoc-gen-go /bin
ARG GRPC_GO_VERSION
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GRPC_GO_VERSION}
RUN mv /go/bin/protoc-gen-go-grpc /bin
ARG GRPC_GATEWAY_VERSION
RUN go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v${GRPC_GATEWAY_VERSION}
RUN mv /go/bin/protoc-gen-grpc-gateway /bin
ARG VTPROTOBUF_VERSION
RUN go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@${VTPROTOBUF_VERSION}
RUN mv /go/bin/protoc-gen-go-vtproto /bin
# tools and sources
FROM tools AS base
@ -45,22 +57,23 @@ COPY ./go.mod .
COPY ./go.sum .
RUN --mount=type=cache,target=/go/pkg go mod download
RUN --mount=type=cache,target=/go/pkg go mod verify
COPY ./api ./api
COPY ./cmd ./cmd
COPY ./internal ./internal
COPY ./pkg ./pkg
COPY ./cmd ./cmd
RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null
# builds kubespan-manager-linux-amd64
FROM base AS kubespan-manager-linux-amd64-build
COPY --from=generate / /
WORKDIR /src/cmd/kubespan-manager
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go build -ldflags "-s -w" -o /kubespan-manager-linux-amd64
# runs protobuf compiler
FROM tools AS proto-compile
COPY --from=proto-specs / /
RUN protoc -I/api --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api --go-vtproto_out=paths=source_relative:/api --go-vtproto_opt=features=marshal+unmarshal+size /api/v1alpha1/pb/cluster.proto
RUN rm /api/v1alpha1/pb/cluster.proto
# runs gofumpt
FROM base AS lint-gofumpt
RUN find . -name '*.pb.go' | xargs -r rm
RUN find . -name '*.pb.gw.go' | xargs -r rm
RUN FILES="$(gofumports -l -local github.com/talos-systems/kubespan-manager .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumports -w -local github.com/talos-systems/kubespan-manager .':\n${FILES}"; exit 1)
RUN FILES="$(gofumports -l -local github.com/talos-systems/discovery-service .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumports -w -local github.com/talos-systems/discovery-service .':\n${FILES}"; exit 1)
# runs golangci-lint
FROM base AS lint-golangci-lint
@ -78,19 +91,29 @@ FROM base AS unit-tests-run
ARG TESTPKGS
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS}
FROM scratch AS kubespan-manager-linux-amd64
COPY --from=kubespan-manager-linux-amd64-build /kubespan-manager-linux-amd64 /kubespan-manager-linux-amd64
# cleaned up specs and compiled versions
FROM scratch AS generate
COPY --from=proto-compile /api/ /api/
FROM scratch AS unit-tests
COPY --from=unit-tests-run /src/coverage.txt /coverage.txt
FROM kubespan-manager-linux-${TARGETARCH} AS kubespan-manager
# builds discovery-service-linux-amd64
FROM base AS discovery-service-linux-amd64-build
COPY --from=generate / /
WORKDIR /src/cmd/discovery-service
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go build -ldflags "-s -w" -o /discovery-service-linux-amd64
FROM scratch AS image-kubespan-manager
FROM scratch AS discovery-service-linux-amd64
COPY --from=discovery-service-linux-amd64-build /discovery-service-linux-amd64 /discovery-service-linux-amd64
FROM discovery-service-linux-${TARGETARCH} AS discovery-service
FROM scratch AS image-discovery-service
ARG TARGETARCH
COPY --from=kubespan-manager kubespan-manager-linux-${TARGETARCH} /kubespan-manager
COPY --from=discovery-service discovery-service-linux-${TARGETARCH} /discovery-service
COPY --from=image-fhs / /
COPY --from=image-ca-certificates / /
LABEL org.opencontainers.image.source https://github.com/talos-systems/wglan-manager
ENTRYPOINT ["/kubespan-manager"]
LABEL org.opencontainers.image.source https://github.com/talos-systems/discovery-service
ENTRYPOINT ["/discovery-service"]

View File

@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:32:42Z by kres 907039b.
# Generated on 2021-09-22T14:57:10Z by kres 2a27963-dirty.
# common variables
@ -12,10 +12,11 @@ REGISTRY ?= ghcr.io
USERNAME ?= talos-systems
REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME)
GOFUMPT_VERSION ?= abc0db2c416aca0f60ea33c23c76665f6e7ba0b6
GO_VERSION ?= 1.16
PROTOBUF_GO_VERSION ?= 1.25.0
GO_VERSION ?= 1.17
PROTOBUF_GO_VERSION ?= 1.27.1
GRPC_GO_VERSION ?= 1.1.0
GRPC_GATEWAY_VERSION ?= 2.4.0
VTPROTOBUF_VERSION ?= 81d623a9a700ede8ef765e5ab08b3aa1f5b4d5a8
TESTPKGS ?= ./...
KRES_IMAGE ?= ghcr.io/talos-systems/kres:latest
@ -39,8 +40,9 @@ COMMON_ARGS += --build-arg=GOFUMPT_VERSION=$(GOFUMPT_VERSION)
COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION=$(PROTOBUF_GO_VERSION)
COMMON_ARGS += --build-arg=GRPC_GO_VERSION=$(GRPC_GO_VERSION)
COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION=$(GRPC_GATEWAY_VERSION)
COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION=$(VTPROTOBUF_VERSION)
COMMON_ARGS += --build-arg=TESTPKGS=$(TESTPKGS)
TOOLCHAIN ?= docker.io/golang:1.16-alpine
TOOLCHAIN ?= docker.io/golang:1.17-alpine
# help menu
@ -75,7 +77,7 @@ respectively.
endef
all: unit-tests kubespan-manager image-kubespan-manager lint
all: unit-tests discovery-service image-discovery-service lint
.PHONY: clean
clean: ## Cleans up all artifacts.
@ -97,8 +99,11 @@ lint-gofumpt: ## Runs gofumpt linter.
fmt: ## Formats the source code
@docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \
bash -c "export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \
cd /tmp && go mod init tmp && go get mvdan.cc/gofumpt/gofumports@$(GOFUMPT_VERSION) && \
cd - && gofumports -w -local github.com/talos-systems/kubespan-manager ."
go install mvdan.cc/gofumpt/gofumports@$(GOFUMPT_VERSION) && \
gofumports -w -local github.com/talos-systems/discovery-service ."
generate: ## Generate .proto definitions.
@$(MAKE) local-$@ DEST=./
.PHONY: base
base: ## Prepare base toolchain
@ -116,15 +121,15 @@ unit-tests-race: ## Performs unit tests with race detection enabled.
coverage: ## Upload coverage data to codecov.io.
bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/coverage.txt -X fix"
.PHONY: $(ARTIFACTS)/kubespan-manager-linux-amd64
$(ARTIFACTS)/kubespan-manager-linux-amd64:
@$(MAKE) local-kubespan-manager-linux-amd64 DEST=$(ARTIFACTS)
.PHONY: $(ARTIFACTS)/discovery-service-linux-amd64
$(ARTIFACTS)/discovery-service-linux-amd64:
@$(MAKE) local-discovery-service-linux-amd64 DEST=$(ARTIFACTS)
.PHONY: kubespan-manager-linux-amd64
kubespan-manager-linux-amd64: $(ARTIFACTS)/kubespan-manager-linux-amd64 ## Builds executable for kubespan-manager-linux-amd64.
.PHONY: discovery-service-linux-amd64
discovery-service-linux-amd64: $(ARTIFACTS)/discovery-service-linux-amd64 ## Builds executable for discovery-service-linux-amd64.
.PHONY: kubespan-manager
kubespan-manager: kubespan-manager-linux-amd64
.PHONY: discovery-service
discovery-service: discovery-service-linux-amd64 ## Builds executables for discovery-service.
.PHONY: lint-markdown
lint-markdown: ## Runs markdownlint.
@ -133,9 +138,9 @@ lint-markdown: ## Runs markdownlint.
.PHONY: lint
lint: lint-golangci-lint lint-gofumpt lint-markdown ## Run all linters for the project.
.PHONY: image-kubespan-manager
image-kubespan-manager: ## Builds image for kubespan-manager.
@$(MAKE) target-$@ TARGET_ARGS="--tag=$(REGISTRY)/$(USERNAME)/kubespan-manager:$(TAG)"
.PHONY: image-discovery-service
image-discovery-service: ## Builds image for discovery-service.
@$(MAKE) target-$@ TARGET_ARGS="--tag=$(REGISTRY)/$(USERNAME)/discovery-service:$(TAG)"
.PHONY: rekres
rekres:

View File

@ -1,3 +1,14 @@
# wglan-manager
# Talos Discovery Service (for KubeSpan)
A trivial implementation of a Wireguard LAN manager for Talos.
Discovery Service provides cluster membership and KubeSpan peer information for Talos clusters.
## Overview
Discovery Service provides centralized service for Talos nodes to exchange information about nodes of the cluster.
Talos runs "official" instance of the service, and anyone can run their own instance on-prem or in the cloud.
Discovery service doesn't store any data, all data is ephemeral and is kept only in memory.
Node information is expired (if not updated) after 30 minutes.
Discovery service doesn't see actual node information, it only stores and updates encrypted blobs.
Discovery data should be submitted encrypted by the client, and service doesn't have the encryption key.

114
api/v1alpha1/cluster.proto Normal file
View File

@ -0,0 +1,114 @@
syntax = "proto3";
package sidero.discovery;
option go_package = "github.com/talos-systems/api/v1alpha1/pb";
import "vendor/google/duration.proto";
service Cluster {
// Hello is the first request sent by the client.
//
// Server might redirect the client to a different instance.
rpc Hello(HelloRequest) returns (HelloResponse);
// AffiliateUpdate updates (or creates) affiliate in the cluster.
rpc AffiliateUpdate(AffiliateUpdateRequest) returns (AffiliateUpdateResponse);
// AffiliateDelete deletes affiliate from the cluster.
rpc AffiliateDelete(AffiliateDeleteRequest) returns (AffiliateDeleteResponse);
// List affiliates in the cluster.
rpc List(ListRequest) returns (ListResponse);
// Watch affiliate updates in the cluster.
rpc Watch(WatchRequest) returns (stream WatchResponse);
}
// Common definitions.
message Affiliate {
// Affiliate ID.
string id = 1;
// Affiliate data (encrypted).
bytes data = 2;
// Affiliate endpoints (encrypted)/
repeated bytes endpoints = 3;
}
// Hello RPC
message HelloRequest {
// ClusterID of the client.
string cluster_id = 1;
// Client version.
string client_version = 2;
}
message RedirectMessage {
// URL of the service endpoint to connect to.
string endpoint_url = 1;
}
message HelloResponse {
// If redirect is present, client should immediately reconnect to a different endpoint.
optional RedirectMessage redirect = 1;
// Client IP as seen by the server.
bytes client_ip = 2;
}
// AffiliateUpdate RPC
message AffiliateUpdateRequest {
// Client ClusterID.
string cluster_id = 1;
// Affiliate ID to update.
string affiliate_id = 2;
// Encrypted affiliate data.
//
// If missing, affiliate data is not updated.
optional bytes affiliate_data = 3;
// Encrypted list of affiliate endpoints.
//
// Endpoints are merged with the existing list of endpoints.
repeated bytes affiliate_endpoints = 4;
// TTL for the new data submitted.
google.protobuf.Duration ttl = 5;
}
message AffiliateUpdateResponse {
}
// AffiliateDelete RPC
message AffiliateDeleteRequest {
// Client ClusterID.
string cluster_id = 1;
// Affiliate ID to delete.
string affiliate_id = 2;
}
message AffiliateDeleteResponse {
}
// List RPC
message ListRequest {
// Client ClusterID.
string cluster_id = 1;
}
message ListResponse {
// List of cluster affiliates.
repeated Affiliate affiliates = 1;
}
// Watch RPC
message WatchRequest {
// Client ClusterID.
string cluster_id = 1;
}
message WatchResponse {
// List of cluster affiliates.
Affiliate affiliate = 1;
// Flag that affiliate was deleted, only ID field is valid.
bool deleted = 2;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,292 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ClusterClient is the client API for Cluster service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ClusterClient interface {
// Hello is the first request sent by the client.
//
// Server might redirect the client to a different instance.
Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
// AffiliateUpdate updates (or creates) affiliate in the cluster.
AffiliateUpdate(ctx context.Context, in *AffiliateUpdateRequest, opts ...grpc.CallOption) (*AffiliateUpdateResponse, error)
// AffiliateDelete deletes affiliate from the cluster.
AffiliateDelete(ctx context.Context, in *AffiliateDeleteRequest, opts ...grpc.CallOption) (*AffiliateDeleteResponse, error)
// List affiliates in the cluster.
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
// Watch affiliate updates in the cluster.
Watch(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (Cluster_WatchClient, error)
}
type clusterClient struct {
cc grpc.ClientConnInterface
}
func NewClusterClient(cc grpc.ClientConnInterface) ClusterClient {
return &clusterClient{cc}
}
func (c *clusterClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
out := new(HelloResponse)
err := c.cc.Invoke(ctx, "/sidero.discovery.Cluster/Hello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) AffiliateUpdate(ctx context.Context, in *AffiliateUpdateRequest, opts ...grpc.CallOption) (*AffiliateUpdateResponse, error) {
out := new(AffiliateUpdateResponse)
err := c.cc.Invoke(ctx, "/sidero.discovery.Cluster/AffiliateUpdate", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) AffiliateDelete(ctx context.Context, in *AffiliateDeleteRequest, opts ...grpc.CallOption) (*AffiliateDeleteResponse, error) {
out := new(AffiliateDeleteResponse)
err := c.cc.Invoke(ctx, "/sidero.discovery.Cluster/AffiliateDelete", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
out := new(ListResponse)
err := c.cc.Invoke(ctx, "/sidero.discovery.Cluster/List", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) Watch(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (Cluster_WatchClient, error) {
stream, err := c.cc.NewStream(ctx, &Cluster_ServiceDesc.Streams[0], "/sidero.discovery.Cluster/Watch", opts...)
if err != nil {
return nil, err
}
x := &clusterWatchClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Cluster_WatchClient interface {
Recv() (*WatchResponse, error)
grpc.ClientStream
}
type clusterWatchClient struct {
grpc.ClientStream
}
func (x *clusterWatchClient) Recv() (*WatchResponse, error) {
m := new(WatchResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ClusterServer is the server API for Cluster service.
// All implementations must embed UnimplementedClusterServer
// for forward compatibility
type ClusterServer interface {
// Hello is the first request sent by the client.
//
// Server might redirect the client to a different instance.
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
// AffiliateUpdate updates (or creates) affiliate in the cluster.
AffiliateUpdate(context.Context, *AffiliateUpdateRequest) (*AffiliateUpdateResponse, error)
// AffiliateDelete deletes affiliate from the cluster.
AffiliateDelete(context.Context, *AffiliateDeleteRequest) (*AffiliateDeleteResponse, error)
// List affiliates in the cluster.
List(context.Context, *ListRequest) (*ListResponse, error)
// Watch affiliate updates in the cluster.
Watch(*WatchRequest, Cluster_WatchServer) error
mustEmbedUnimplementedClusterServer()
}
// UnimplementedClusterServer must be embedded to have forward compatible implementations.
type UnimplementedClusterServer struct {
}
func (UnimplementedClusterServer) Hello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func (UnimplementedClusterServer) AffiliateUpdate(context.Context, *AffiliateUpdateRequest) (*AffiliateUpdateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AffiliateUpdate not implemented")
}
func (UnimplementedClusterServer) AffiliateDelete(context.Context, *AffiliateDeleteRequest) (*AffiliateDeleteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AffiliateDelete not implemented")
}
func (UnimplementedClusterServer) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedClusterServer) Watch(*WatchRequest, Cluster_WatchServer) error {
return status.Errorf(codes.Unimplemented, "method Watch not implemented")
}
func (UnimplementedClusterServer) mustEmbedUnimplementedClusterServer() {}
// UnsafeClusterServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ClusterServer will
// result in compilation errors.
type UnsafeClusterServer interface {
mustEmbedUnimplementedClusterServer()
}
func RegisterClusterServer(s grpc.ServiceRegistrar, srv ClusterServer) {
s.RegisterService(&Cluster_ServiceDesc, srv)
}
func _Cluster_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).Hello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/sidero.discovery.Cluster/Hello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).Hello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_AffiliateUpdate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AffiliateUpdateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).AffiliateUpdate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/sidero.discovery.Cluster/AffiliateUpdate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).AffiliateUpdate(ctx, req.(*AffiliateUpdateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_AffiliateDelete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AffiliateDeleteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).AffiliateDelete(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/sidero.discovery.Cluster/AffiliateDelete",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).AffiliateDelete(ctx, req.(*AffiliateDeleteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/sidero.discovery.Cluster/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).List(ctx, req.(*ListRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_Watch_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(WatchRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ClusterServer).Watch(m, &clusterWatchServer{stream})
}
type Cluster_WatchServer interface {
Send(*WatchResponse) error
grpc.ServerStream
}
type clusterWatchServer struct {
grpc.ServerStream
}
func (x *clusterWatchServer) Send(m *WatchResponse) error {
return x.ServerStream.SendMsg(m)
}
// Cluster_ServiceDesc is the grpc.ServiceDesc for Cluster service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Cluster_ServiceDesc = grpc.ServiceDesc{
ServiceName: "sidero.discovery.Cluster",
HandlerType: (*ClusterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Hello",
Handler: _Cluster_Hello_Handler,
},
{
MethodName: "AffiliateUpdate",
Handler: _Cluster_AffiliateUpdate_Handler,
},
{
MethodName: "AffiliateDelete",
Handler: _Cluster_AffiliateDelete_Handler,
},
{
MethodName: "List",
Handler: _Cluster_List_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "Watch",
Handler: _Cluster_Watch_Handler,
ServerStreams: true,
},
},
Metadata: "v1alpha1/pb/cluster.proto",
}

File diff suppressed because it is too large Load Diff

116
api/vendor/google/duration.proto vendored Normal file
View File

@ -0,0 +1,116 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
syntax = "proto3";
package google.protobuf;
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
option cc_enable_arenas = true;
option go_package = "google.golang.org/protobuf/types/known/durationpb";
option java_package = "com.google.protobuf";
option java_outer_classname = "DurationProto";
option java_multiple_files = true;
option objc_class_prefix = "GPB";
// A Duration represents a signed, fixed-length span of time represented
// as a count of seconds and fractions of seconds at nanosecond
// resolution. It is independent of any calendar and concepts like "day"
// or "month". It is related to Timestamp in that the difference between
// two Timestamp values is a Duration and it can be added or subtracted
// from a Timestamp. Range is approximately +-10,000 years.
//
// # Examples
//
// Example 1: Compute Duration from two Timestamps in pseudo code.
//
// Timestamp start = ...;
// Timestamp end = ...;
// Duration duration = ...;
//
// duration.seconds = end.seconds - start.seconds;
// duration.nanos = end.nanos - start.nanos;
//
// if (duration.seconds < 0 && duration.nanos > 0) {
// duration.seconds += 1;
// duration.nanos -= 1000000000;
// } else if (duration.seconds > 0 && duration.nanos < 0) {
// duration.seconds -= 1;
// duration.nanos += 1000000000;
// }
//
// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code.
//
// Timestamp start = ...;
// Duration duration = ...;
// Timestamp end = ...;
//
// end.seconds = start.seconds + duration.seconds;
// end.nanos = start.nanos + duration.nanos;
//
// if (end.nanos < 0) {
// end.seconds -= 1;
// end.nanos += 1000000000;
// } else if (end.nanos >= 1000000000) {
// end.seconds += 1;
// end.nanos -= 1000000000;
// }
//
// Example 3: Compute Duration from datetime.timedelta in Python.
//
// td = datetime.timedelta(days=3, minutes=10)
// duration = Duration()
// duration.FromTimedelta(td)
//
// # JSON Mapping
//
// In JSON format, the Duration type is encoded as a string rather than an
// object, where the string ends in the suffix "s" (indicating seconds) and
// is preceded by the number of seconds, with nanoseconds expressed as
// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
// microsecond should be expressed in JSON format as "3.000001s".
//
//
message Duration {
// Signed seconds of the span of time. Must be from -315,576,000,000
// to +315,576,000,000 inclusive. Note: these bounds are computed from:
// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
int64 seconds = 1;
// Signed fractions of a second at nanosecond resolution of the span
// of time. Durations less than one second are represented with a 0
// `seconds` field and a positive or negative `nanos` field. For durations
// of one second or more, a non-zero value for the `nanos` field must be
// of the same sign as the `seconds` field. Must be from -999,999,999
// to +999,999,999 inclusive.
int32 nanos = 2;
}

View File

@ -0,0 +1,185 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/talos-systems/discovery-service/api/v1alpha1/pb"
"github.com/talos-systems/discovery-service/internal/state"
"github.com/talos-systems/discovery-service/pkg/server"
)
var (
listenAddr = ":3000"
metricsAddr = ":2122"
devMode bool
gcInterval time.Duration
)
func init() {
flag.StringVar(&listenAddr, "addr", ":3000", "addr on which to listen")
flag.StringVar(&metricsAddr, "metrics-addr", ":2122", "prometheus metrics listen addr")
flag.BoolVar(&devMode, "debug", false, "enable debug mode")
flag.DurationVar(&gcInterval, "gc-interval", time.Minute, "garbage collection interval")
}
func main() {
flag.Parse()
logger, err := zap.NewProduction()
if err != nil {
log.Fatalln("failed to initialize logger:", err)
}
if os.Getenv("MODE") == "dev" {
devMode = true
}
if devMode {
logger, err = zap.NewDevelopment()
if err != nil {
log.Fatalln("failed to initialize development logger:", err)
}
}
if err = signalHandler(context.Background(), logger, run); err != nil {
logger.Error("service failed", zap.Error(err))
os.Exit(1)
}
}
func signalHandler(ctx context.Context, logger *zap.Logger, f func(ctx context.Context, logger *zap.Logger) error) error {
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
return f(ctx, logger)
}
func recoveryHandler(logger *zap.Logger) grpc_recovery.RecoveryHandlerFunc {
return func(p interface{}) error {
if logger != nil {
logger.Error("grpc panic", zap.Any("panic", p), zap.Stack("stack"))
}
return status.Errorf(codes.Internal, "%v", p)
}
}
func run(ctx context.Context, logger *zap.Logger) error {
logger.Info("service starting")
defer logger.Info("service shut down")
// Recovery is installed as the the first middleware in the chain to handle panics (via defer and recover()) in all subsequent middlewares.
// Logging is installed as the first middleware (even before recovery middleware) in the chain
// so that request in the form it was received and status sent on the wire is logged (error/success).
// It also tracks the whole duration of the request, including other middleware overhead.
grpc_zap.ReplaceGrpcLoggerV2(logger)
recoveryOpt := grpc_recovery.WithRecoveryHandler(recoveryHandler(logger))
serverOptions := []grpc.ServerOption{
grpc_middleware.WithUnaryServerChain(
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_zap.UnaryServerInterceptor(logger),
grpc_prometheus.UnaryServerInterceptor,
grpc_recovery.UnaryServerInterceptor(recoveryOpt),
),
grpc_middleware.WithStreamServerChain(
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_zap.StreamServerInterceptor(logger),
grpc_prometheus.StreamServerInterceptor,
grpc_recovery.StreamServerInterceptor(recoveryOpt),
),
}
state := state.NewState()
lis, err := net.Listen("tcp", listenAddr)
if err != nil {
return fmt.Errorf("failed to listen: %w", err)
}
s := grpc.NewServer(serverOptions...)
pb.RegisterClusterServer(s, server.NewClusterServer(state))
grpc_prometheus.Register(s)
var metricsMux http.ServeMux
metricsMux.Handle("/metrics", promhttp.Handler())
metricsServer := http.Server{
Addr: metricsAddr,
Handler: &metricsMux,
}
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
logger.Info("gRPC server starting", zap.Stringer("address", lis.Addr()))
if err := s.Serve(lis); err != nil {
return fmt.Errorf("failed to serve: %w", err)
}
return nil
})
eg.Go(func() error {
logger.Info("metrics starting", zap.String("address", metricsServer.Addr))
if err := metricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
eg.Go(func() error {
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
s.GracefulStop()
metricsServer.Shutdown(shutdownCtx) //nolint:errcheck
return nil
})
eg.Go(func() error {
state.RunGC(ctx, logger, gcInterval)
return nil
})
return eg.Wait()
}

View File

@ -1,314 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/talos-systems/kubespan-manager/internal/db"
"github.com/talos-systems/kubespan-manager/pkg/types"
)
var (
listenAddr = ":3000"
devMode bool
nodeDB db.DB
)
func init() {
flag.StringVar(&listenAddr, "addr", ":3000", "addr on which to listen")
flag.BoolVar(&devMode, "debug", false, "enable debug mode")
}
//nolint:gocognit,gocyclo,cyclop
func main() {
flag.Parse()
logger, err := zap.NewProduction()
if err != nil {
log.Fatalln("failed to initialize logger:", err)
}
if os.Getenv("MODE") == "dev" {
devMode = true
logger, err = zap.NewDevelopment()
if err != nil {
log.Fatalln("failed to initialize development logger:", err)
}
}
if os.Getenv("REDIS_ADDR") != "" {
nodeDB, err = db.NewRedis(os.Getenv("REDIS_ADDR"), logger)
if err != nil {
log.Fatalln("failed to connect to redis: %w", err)
}
} else {
nodeDB = db.New(logger)
}
app := fiber.New()
app.Get("/:cluster", func(c *fiber.Ctx) error {
cluster := c.Params("cluster")
if cluster == "" {
logger.Error("empty cluster for node list")
return c.SendStatus(http.StatusBadRequest)
}
if e := validateClusterID(cluster); e != nil {
logger.Error("bad cluster ID",
zap.String("cluster", c.Params("cluster", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
list, e := nodeDB.List(c.Context(), cluster)
if e != nil {
if errors.Is(e, db.ErrNotFound) {
logger.Warn("cluster not found",
zap.String("cluster", cluster),
zap.Error(e),
)
return c.SendStatus(http.StatusNotFound)
}
return c.SendStatus(http.StatusInternalServerError)
}
logger.Info("listing cluster nodes",
zap.String("cluster", c.Params("cluster", "")),
zap.Int("count", len(list)),
)
return c.JSON(list)
})
app.Get("/:cluster/:node", func(c *fiber.Ctx) error {
cluster := c.Params("cluster", "")
if cluster == "" {
logger.Error("empty cluster for node get")
return c.SendStatus(http.StatusBadRequest)
}
if e := validateClusterID(cluster); e != nil {
logger.Error("bad cluster ID",
zap.String("cluster", c.Params("cluster", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
if e := validatePublicKey(c.Params("node")); e != nil {
logger.Error("bad node ID",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", c.Params("node", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
node := c.Params("node", "")
if node == "" {
logger.Error("empty node for node get",
zap.String("cluster", c.Params("cluster", "")),
)
return c.SendStatus(http.StatusBadRequest)
}
n, e := nodeDB.Get(c.Context(), cluster, node)
if e != nil {
if errors.Is(e, db.ErrNotFound) {
logger.Warn("node not found",
zap.String("cluster", cluster),
zap.String("node", node),
zap.Error(err),
)
return c.SendStatus(http.StatusNotFound)
}
return c.SendStatus(http.StatusInternalServerError)
}
logger.Info("returning cluster node",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", n.ID),
zap.String("ip", n.IP.String()),
zap.Strings("addresses", addressToString(n.Addresses)),
zap.Error(err),
)
return c.JSON(n)
})
// PUT addresses to a Node
app.Put("/:cluster/:node", func(c *fiber.Ctx) error {
var addresses []*types.Address
if e := validateClusterID(c.Params("cluster")); e != nil {
logger.Error("bad cluster ID",
zap.String("cluster", c.Params("cluster", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
if e := validatePublicKey(c.Params("node")); e != nil {
logger.Error("bad node ID",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", c.Params("node", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
if e := c.BodyParser(&addresses); e != nil {
logger.Error("failed to parse node PUT",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", c.Params("node", "")),
zap.Error(e),
)
return c.SendStatus(http.StatusBadRequest)
}
node := c.Params("node", "")
if node == "" {
logger.Error("invalid node key",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", c.Params("node", "")),
)
}
if err := nodeDB.AddAddresses(c.Context(), c.Params("cluster", ""), node, addresses...); err != nil {
logger.Error("failed to add known endpoints",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", node),
zap.Strings("addresses", addressToString(addresses)),
zap.Error(err),
)
return c.SendStatus(http.StatusInternalServerError)
}
return c.SendStatus(http.StatusNoContent)
})
app.Post("/:cluster", func(c *fiber.Ctx) error {
n := new(types.Node)
if err := validateClusterID(c.Params("cluster")); err != nil {
logger.Error("bad cluster ID",
zap.String("cluster", c.Params("cluster", "")),
zap.Error(err),
)
return c.SendStatus(http.StatusBadRequest)
}
if err := c.BodyParser(n); err != nil {
logger.Error("failed to parse node POST",
zap.String("cluster", c.Params("cluster", "")),
zap.Error(err),
)
return c.SendStatus(http.StatusBadRequest)
}
if err := validatePublicKey(n.ID); err != nil {
logger.Error("bad node ID",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", n.ID),
zap.Error(err),
)
return c.SendStatus(http.StatusBadRequest)
}
if err := nodeDB.Add(c.Context(), c.Params("cluster", ""), n); err != nil {
logger.Error("failed to add/update node",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", n.ID),
zap.String("ip", n.IP.String()),
zap.Strings("addresses", addressToString(n.Addresses)),
zap.Error(err),
)
return c.SendStatus(http.StatusInternalServerError)
}
logger.Info("add/update node",
zap.String("cluster", c.Params("cluster", "")),
zap.String("node", n.ID),
zap.String("ip", n.IP.String()),
zap.Strings("addresses", addressToString(n.Addresses)),
)
return c.SendStatus(http.StatusNoContent)
})
go func() {
for {
time.Sleep(time.Hour)
nodeDB.Clean()
}
}()
logger.Fatal("listen exited",
zap.Error(app.Listen(listenAddr)),
)
}
func addressToString(addresses []*types.Address) (out []string) {
for _, a := range addresses {
if !a.IP.IsZero() {
out = append(out, a.IP.String())
continue
}
out = append(out, a.Name)
}
return out
}
func validateClusterID(cluster string) error {
if _, err := uuid.Parse(cluster); err != nil {
return fmt.Errorf("cluster ID is not a valid UUID: %w", err)
}
return nil
}
func validatePublicKey(key string) error {
if _, err := wgtypes.ParseKey(key); err != nil {
return fmt.Errorf("node ID is not a valid wireguard key")
}
return nil
}

38
go.mod
View File

@ -1,12 +1,36 @@
module github.com/talos-systems/kubespan-manager
module github.com/talos-systems/discovery-service
go 1.16
go 1.17
require (
github.com/go-redis/redis/v8 v8.11.2
github.com/gofiber/fiber/v2 v2.8.0
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/prometheus/client_golang v1.11.0
github.com/stretchr/testify v1.5.1
go.uber.org/zap v1.16.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
inet.af/netaddr v0.0.0-20210525141459-c0eff8545de6
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
google.golang.org/grpc v1.40.0
google.golang.org/protobuf v1.26.0-rc.1
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e
)
require (
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/golang/protobuf v1.4.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
go.uber.org/atomic v1.6.0 // indirect
go.uber.org/multierr v1.5.0 // indirect
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)

294
go.sum
View File

@ -1,101 +1,154 @@
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=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis/v8 v8.11.2 h1:WqlSpAwz8mxDSMCvbyz1Mkiqe0LE5OY4j3lgkvu1Ts0=
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
github.com/gofiber/fiber/v2 v2.8.0 h1:BdWvZmg/WY/Vjtjm38aXOp1Lks1BhuyS2b7lSWSPAzk=
github.com/gofiber/fiber/v2 v2.8.0/go.mod h1:Ah3IJikrKNRepl/HuVawppS25X7FWohwfCSRn7kJG28=
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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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.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 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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.5.2/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
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 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
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.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
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.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
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.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
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.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 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.23.0 h1:0ufwSD9BhWa6f8HWdmdq4FHQ23peRo3Ng/Qs8m5NcFs=
github.com/valyala/fasthttp v1.23.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
@ -103,83 +156,77 @@ go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/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 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/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-20201216054612-986b41b23924/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-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/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-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -187,27 +234,50 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
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=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
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.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/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.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.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
inet.af/netaddr v0.0.0-20210525141459-c0eff8545de6 h1:Bf++HvcnBFYGQjFx1qhW/3uTMQbv+bfKB6gdEN7JvvM=
inet.af/netaddr v0.0.0-20210525141459-c0eff8545de6/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e h1:tvgqez5ZQoBBiBAGNU/fmJy247yB/7++kcLOEoMYup0=
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=

View File

@ -1,26 +0,0 @@
<!-- THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. -->
<!-- -->
<!-- Generated on 2021-08-13T12:22:39Z by kres 907039b. -->
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }})
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
* {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}

View File

@ -1,32 +0,0 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2021-08-13T12:22:39Z by kres 907039b.
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://github.com/talos-systems/wglan-manager
options:
commits:
# filters:
# Type:
# - feat
# - fix
# - perf
# - refactor
commit_groups:
# title_maps:
# feat: Features
# fix: Bug Fixes
# perf: Performance Improvements
# refactor: Code Refactoring
header:
pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
pattern_maps:
- Type
- Scope
- Subject
notes:
keywords:
- BREAKING CHANGE

View File

@ -6,8 +6,8 @@
# commit to be tagged for the new release
commit = "HEAD"
project_name = "wglan-manager"
github_repo = "talos-systems/wglan-manager"
project_name = "discovery-service"
github_repo = "talos-systems/discovery-service"
match_deps = "^github.com/(talos-systems/[a-zA-Z0-9-]+)$"
# previous = -

View File

@ -1,170 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package db contains state storage logic.
package db
import (
"context"
"errors"
"fmt"
"sync"
"time"
"go.uber.org/zap"
"github.com/talos-systems/kubespan-manager/pkg/types"
)
// ErrNotFound means that record is not found in DB.
var ErrNotFound = errors.New("not found")
// AddressExpirationTimeout is the amount of time after which addresses of a node should be expired.
const AddressExpirationTimeout = 10 * time.Minute
// DB manager state persistent storage interface.
type DB interface {
// Add adds a set of known Endpoints to a node, creating the node, if it does not exist.
Add(ctx context.Context, cluster string, n *types.Node) error
// AddAddresses adds a set of addresses for a node.
AddAddresses(ctx context.Context, cluster, id string, ep ...*types.Address) error
// Clean executes a database cleanup routine.
Clean()
// Get returns the details of the node.
Get(ctx context.Context, cluster, id string) (*types.Node, error)
// List returns the set of Nodes for the given Cluster.
List(ctx context.Context, cluster string) ([]*types.Node, error)
}
type ramDB struct {
logger *zap.Logger
db map[string]map[string]*types.Node
mu sync.RWMutex
}
// New returns a new database.
func New(logger *zap.Logger) DB {
return &ramDB{
logger: logger,
db: make(map[string]map[string]*types.Node),
}
}
// Add implements DB.
func (d *ramDB) Add(ctx context.Context, cluster string, n *types.Node) error {
d.mu.Lock()
defer d.mu.Unlock()
c, ok := d.db[cluster]
if !ok {
c = make(map[string]*types.Node)
d.db[cluster] = c
}
if existing, ok := c[n.ID]; ok {
existing.AddAddresses(n.Addresses...)
return nil
}
c[n.ID] = n
return nil
}
func (d *ramDB) AddAddresses(ctx context.Context, cluster, id string, addresses ...*types.Address) error {
d.mu.Lock()
defer d.mu.Unlock()
c, ok := d.db[cluster]
if !ok {
return fmt.Errorf("cluster does not exist")
}
n, ok := c[id]
if !ok {
return fmt.Errorf("node does not exist")
}
n.AddAddresses(addresses...)
return nil
}
// List implements DB.
func (d *ramDB) List(ctx context.Context, cluster string) (list []*types.Node, err error) {
c, ok := d.db[cluster]
if !ok {
return nil, fmt.Errorf("cluster %q not found", cluster)
}
for _, n := range c {
n.ExpireAddressesOlderThan(AddressExpirationTimeout)
if len(n.Addresses) > 0 {
list = append(list, n)
}
}
if len(list) == 0 {
return nil, ErrNotFound
}
return list, nil
}
// Get implements DB.
func (d *ramDB) Get(ctx context.Context, cluster, id string) (*types.Node, error) {
d.mu.RLock()
defer d.mu.Unlock()
c, ok := d.db[cluster]
if !ok {
return nil, fmt.Errorf("cluster %q not found", cluster)
}
n, ok := c[id]
if !ok {
return nil, ErrNotFound
}
return n, nil
}
// Clean runs the database cleanup routine.
func (d *ramDB) Clean() {
d.mu.Lock()
defer d.mu.Unlock()
var clusterDeleteList []string
for clusterID, c := range d.db {
var nodeDeleteList []string
for id, n := range c {
n.ExpireAddressesOlderThan(AddressExpirationTimeout)
if len(n.Addresses) < 1 {
nodeDeleteList = append(nodeDeleteList, id)
}
}
for _, id := range nodeDeleteList {
c[id] = nil
delete(c, id)
}
if len(c) == 0 {
clusterDeleteList = append(clusterDeleteList, clusterID)
}
}
for _, id := range clusterDeleteList {
delete(d.db, id)
}
}

View File

@ -1,175 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package db
import (
"context"
"errors"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
"github.com/talos-systems/kubespan-manager/pkg/types"
)
const redisTTL = 12 * time.Minute
type redisDB struct {
logger *zap.Logger
rc *redis.Client
}
// NewRedis creates new redis DB.
func NewRedis(addr string, logger *zap.Logger) (DB, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rc := redis.NewClient(&redis.Options{
Addr: addr,
})
if err := rc.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
return &redisDB{
rc: rc,
logger: logger,
}, nil
}
func (d *redisDB) clusterNodesKey(cluster string) string {
return fmt.Sprintf("cluster:%s:nodelist", cluster)
}
func (d *redisDB) clusterNodeKey(cluster, id string) string {
return fmt.Sprintf("cluster:%s:node:%s", cluster, id)
}
func (d *redisDB) clusterAddressKey(cluster string, addr *types.Address) string {
if !addr.IP.IsZero() {
return fmt.Sprintf("cluster:%s:address:%s", cluster, addr.IP.String())
}
return fmt.Sprintf("cluster:%s:address:%s", cluster, addr.Name)
}
// Add implements db.DB.
func (d *redisDB) Add(ctx context.Context, cluster string, n *types.Node) error {
tx := d.rc.TxPipeline()
// Store the node data
tx.Set(ctx, d.clusterNodeKey(cluster, n.ID), n, redisTTL)
// Add the node to the cluster
if err := tx.SAdd(ctx, d.clusterNodesKey(cluster), n.ID).Err(); err != nil {
return fmt.Errorf("failed to add node %s to cluster %q: %w", n.Name, cluster, err)
}
tx.Expire(ctx, d.clusterNodesKey(cluster), redisTTL)
// Update the address assignments
for _, addr := range n.Addresses {
tx.Set(ctx, d.clusterAddressKey(cluster, addr), n.ID, redisTTL)
}
_, err := tx.Exec(ctx)
return err
}
// AddAddresses implements db.DB.
func (d *redisDB) AddAddresses(ctx context.Context, cluster, id string, ep ...*types.Address) error {
n, err := d.Get(ctx, cluster, id)
if err != nil {
return fmt.Errorf("failed to retrieve node %q from cluster %q: %w", id, cluster, err)
}
n.AddAddresses(ep...)
return d.Add(ctx, cluster, n)
}
// Clean implements db.DB.
func (d *redisDB) Clean() {} // no-op
// Get implements db.DB.
func (d *redisDB) Get(ctx context.Context, cluster, id string) (*types.Node, error) {
n := new(types.Node)
if err := d.rc.Get(ctx, d.clusterNodeKey(cluster, id)).Scan(n); err != nil {
if errors.Is(redis.Nil, err) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to parse node %q of cluster %q: %w", id, cluster, err)
}
var validAddresses []*types.Address
for _, a := range n.Addresses {
owner, err := d.rc.Get(ctx, d.clusterAddressKey(cluster, a)).Result()
if err == nil && owner == id {
validAddresses = append(validAddresses, a)
}
}
n.Addresses = validAddresses
return n, nil
}
// List implements db.DB.
func (d *redisDB) List(ctx context.Context, cluster string) ([]*types.Node, error) {
nodeList, err := d.rc.SMembers(ctx, d.clusterNodesKey(cluster)).Result()
if err != nil {
if errors.Is(redis.Nil, err) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get members of cluster %q: %w", cluster, err)
}
ret := make([]*types.Node, 0, len(nodeList))
for _, id := range nodeList {
n, err := d.Get(ctx, cluster, id)
if err != nil {
if errors.Is(redis.Nil, err) {
d.logger.Debug("removing stale node from cluster",
zap.String("node", id),
zap.String("cluster", cluster),
)
if err = d.rc.SRem(ctx, d.clusterNodesKey(cluster), id).Err(); err != nil {
d.logger.Warn("failed to remove node from cluster set which did not exist",
zap.String("node", id),
zap.String("cluster", cluster),
zap.Error(err),
)
}
} else {
d.logger.Error("failed to get node listen in nodeList",
zap.String("node", id),
zap.String("cluster", cluster),
zap.Error(err),
)
}
continue
}
ret = append(ret, n)
}
if len(ret) == 0 {
return nil, ErrNotFound
}
return ret, nil
}

106
internal/state/affiliate.go Normal file
View File

@ -0,0 +1,106 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state
import (
"bytes"
"time"
)
// Affiliate represents cluster affiliate state.
type Affiliate struct {
id string
expiration time.Time
data []byte
endpoints []Endpoint
changed bool
}
// Endpoint is a combination of endpoint itself and its TTL.
type Endpoint struct {
expiration time.Time
data []byte
}
// NewAffiliate constructs new (empty) Affiliate.
func NewAffiliate(id string) *Affiliate {
return &Affiliate{
id: id,
}
}
// ClearChanged clears the changed flag.
func (affiliate *Affiliate) ClearChanged() {
affiliate.changed = false
}
// IsChanged returns changed flag.
func (affiliate *Affiliate) IsChanged() bool {
return affiliate.changed
}
// Update affiliate data and expiration.
func (affiliate *Affiliate) Update(data []byte, expiration time.Time) {
affiliate.data = data
affiliate.expiration = expiration
affiliate.changed = true
}
// MergeEndpoints and potentially update expiration for endpoints.
func (affiliate *Affiliate) MergeEndpoints(endpoints [][]byte, expiration time.Time) {
for _, endpoint := range endpoints {
found := false
for i := range affiliate.endpoints {
if bytes.Equal(affiliate.endpoints[i].data, endpoint) {
found = true
if affiliate.endpoints[i].expiration.Before(expiration) {
affiliate.endpoints[i].expiration = expiration
}
break
}
}
if !found {
affiliate.endpoints = append(affiliate.endpoints, Endpoint{
expiration: expiration,
data: endpoint,
})
affiliate.changed = true
}
}
if affiliate.expiration.Before(expiration) {
affiliate.expiration = expiration
}
}
// GarbageCollect affiliate data.
//
// Endpoints are removed independent of the affiliate data.
func (affiliate *Affiliate) GarbageCollect(now time.Time) (remove, changed bool) {
if affiliate.expiration.Before(now) {
return true, true
}
n := 0
for _, endpoint := range affiliate.endpoints {
if endpoint.expiration.After(now) {
affiliate.endpoints[n] = endpoint
n++
} else {
changed = true
}
}
affiliate.endpoints = affiliate.endpoints[:n]
return false, changed
}

View File

@ -0,0 +1,98 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/discovery-service/internal/state"
)
func TestAffiliateMutations(t *testing.T) {
now := time.Now()
affiliate := state.NewAffiliate("id1")
affiliate.ClearChanged()
assert.False(t, affiliate.IsChanged())
affiliate.Update([]byte("data"), now.Add(time.Minute))
assert.Equal(t, &state.AffiliateExport{
ID: "id1",
Data: []byte("data"),
Endpoints: [][]byte{},
}, affiliate.Export())
assert.True(t, affiliate.IsChanged())
affiliate.ClearChanged()
affiliate.Update([]byte("data1"), now.Add(time.Minute))
assert.Equal(t, &state.AffiliateExport{
ID: "id1",
Data: []byte("data1"),
Endpoints: [][]byte{},
}, affiliate.Export())
assert.True(t, affiliate.IsChanged())
affiliate.ClearChanged()
affiliate.MergeEndpoints([][]byte{
[]byte("e1"),
[]byte("e2"),
}, now.Add(time.Minute))
assert.Equal(t, &state.AffiliateExport{
ID: "id1",
Data: []byte("data1"),
Endpoints: [][]byte{[]byte("e1"), []byte("e2")},
}, affiliate.Export())
assert.True(t, affiliate.IsChanged())
affiliate.ClearChanged()
affiliate.MergeEndpoints([][]byte{
[]byte("e1"),
}, now.Add(time.Minute))
assert.False(t, affiliate.IsChanged())
affiliate.MergeEndpoints([][]byte{
[]byte("e1"),
[]byte("e3"),
}, now.Add(3*time.Minute))
assert.Equal(t, &state.AffiliateExport{
ID: "id1",
Data: []byte("data1"),
Endpoints: [][]byte{[]byte("e1"), []byte("e2"), []byte("e3")},
}, affiliate.Export())
assert.True(t, affiliate.IsChanged())
remove, changed := affiliate.GarbageCollect(now)
assert.False(t, remove)
assert.False(t, changed)
remove, changed = affiliate.GarbageCollect(now.Add(2 * time.Minute))
assert.False(t, remove)
assert.True(t, changed)
assert.Equal(t, &state.AffiliateExport{
ID: "id1",
Data: []byte("data1"),
Endpoints: [][]byte{[]byte("e1"), []byte("e3")},
}, affiliate.Export())
remove, changed = affiliate.GarbageCollect(now.Add(4 * time.Minute))
assert.True(t, remove)
assert.True(t, changed)
}

167
internal/state/cluster.go Normal file
View File

@ -0,0 +1,167 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state
import (
"sync"
"time"
)
// Cluster is a collection of affiliates.
//
// Cluster is gc'ed as all its affiliates are gone.
type Cluster struct {
affiliates map[string]*Affiliate
id string
subscriptions []*Subscription
affiliatesMu sync.Mutex
subscriptionsMu sync.Mutex
}
// NewCluster creates new cluster with specified ID.
func NewCluster(id string) *Cluster {
return &Cluster{
id: id,
affiliates: map[string]*Affiliate{},
}
}
// WithAffiliate runs a function against the affiliate.
//
// Cluster state is locked while the function is running.
func (cluster *Cluster) WithAffiliate(id string, f func(affiliate *Affiliate)) {
cluster.affiliatesMu.Lock()
defer cluster.affiliatesMu.Unlock()
if affiliate, ok := cluster.affiliates[id]; ok {
affiliate.ClearChanged()
f(affiliate)
if affiliate.IsChanged() {
cluster.notify(&Notification{
AffiliateID: id,
Affiliate: affiliate.Export(),
})
}
return
}
affiliate := NewAffiliate(id)
f(affiliate)
cluster.affiliates[id] = affiliate
cluster.notify(&Notification{
AffiliateID: id,
Affiliate: affiliate.Export(),
})
}
// DeleteAffiliate deletes affiliate from the cluster.
func (cluster *Cluster) DeleteAffiliate(id string) {
cluster.affiliatesMu.Lock()
defer cluster.affiliatesMu.Unlock()
if _, ok := cluster.affiliates[id]; ok {
delete(cluster.affiliates, id)
cluster.notify(&Notification{
AffiliateID: id,
})
}
}
// List the affiliates.
//
// List provides a snapshot of the affiliates.
func (cluster *Cluster) List() []*AffiliateExport {
cluster.affiliatesMu.Lock()
defer cluster.affiliatesMu.Unlock()
result := make([]*AffiliateExport, 0, len(cluster.affiliates))
for _, affiliate := range cluster.affiliates {
result = append(result, affiliate.Export())
}
return result
}
// Subscribe to the affiliate updates.
//
// Subscribe returns a snapshot of current list of affiliates and creates new Subscription.
func (cluster *Cluster) Subscribe(ch chan<- *Notification) ([]*AffiliateExport, *Subscription) {
cluster.affiliatesMu.Lock()
defer cluster.affiliatesMu.Unlock()
cluster.subscriptionsMu.Lock()
defer cluster.subscriptionsMu.Unlock()
snapshot := make([]*AffiliateExport, 0, len(cluster.affiliates))
for _, affiliate := range cluster.affiliates {
snapshot = append(snapshot, affiliate.Export())
}
subscription := &Subscription{
cluster: cluster,
errCh: make(chan error, 1),
ch: ch,
}
cluster.subscriptions = append(cluster.subscriptions, subscription)
return snapshot, subscription
}
func (cluster *Cluster) unsubscribe(subscription *Subscription) {
cluster.subscriptionsMu.Lock()
defer cluster.subscriptionsMu.Unlock()
for i := range cluster.subscriptions {
if cluster.subscriptions[i] == subscription {
cluster.subscriptions[i] = cluster.subscriptions[len(cluster.subscriptions)-1]
cluster.subscriptions[len(cluster.subscriptions)-1] = nil
cluster.subscriptions = cluster.subscriptions[:len(cluster.subscriptions)-1]
return
}
}
}
// GarbageCollect the cluster.
func (cluster *Cluster) GarbageCollect(now time.Time) (empty bool) {
cluster.affiliatesMu.Lock()
defer cluster.affiliatesMu.Unlock()
for id, affiliate := range cluster.affiliates {
remove, changed := affiliate.GarbageCollect(now)
if remove {
delete(cluster.affiliates, id)
}
if changed {
cluster.notify(&Notification{
AffiliateID: id,
})
}
}
return len(cluster.affiliates) == 0
}
func (cluster *Cluster) notify(notifications ...*Notification) {
cluster.subscriptionsMu.Lock()
subscriptions := append([]*Subscription(nil), cluster.subscriptions...)
cluster.subscriptionsMu.Unlock()
for _, notification := range notifications {
for _, subscription := range subscriptions {
subscription.notify(notification)
}
}
}

View File

@ -0,0 +1,243 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state_test
import (
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/discovery-service/internal/state"
)
func TestClusterMutations(t *testing.T) {
now := time.Now()
cluster := state.NewCluster("cluster1")
remove := cluster.GarbageCollect(now)
assert.True(t, remove)
assert.Len(t, cluster.List(), 0)
cluster.WithAffiliate("af1", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data"), now.Add(time.Minute))
})
assert.Len(t, cluster.List(), 1)
updates := make(chan *state.Notification, 1)
snapshot, subscription := cluster.Subscribe(updates)
defer subscription.Close()
assert.Len(t, snapshot, 1)
cluster.WithAffiliate("af1", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data1"), now.Add(time.Minute))
})
assert.Len(t, cluster.List(), 1)
assert.Equal(t, []*state.AffiliateExport{
{
ID: "af1",
Data: []byte("data1"),
Endpoints: [][]byte{},
},
}, cluster.List())
select {
case notification := <-updates:
assert.Equal(t, "af1", notification.AffiliateID)
assert.Equal(t, &state.AffiliateExport{
ID: "af1",
Data: []byte("data1"),
Endpoints: [][]byte{},
}, notification.Affiliate)
case <-time.After(time.Second):
assert.Fail(t, "no notification")
}
cluster.WithAffiliate("af2", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data2"), now.Add(time.Minute))
})
assert.Len(t, cluster.List(), 2)
list := cluster.List()
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
assert.Equal(t, []*state.AffiliateExport{
{
ID: "af1",
Data: []byte("data1"),
Endpoints: [][]byte{},
},
{
ID: "af2",
Data: []byte("data2"),
Endpoints: [][]byte{},
},
}, list)
select {
case notification := <-updates:
assert.Equal(t, "af2", notification.AffiliateID)
assert.Equal(t, &state.AffiliateExport{
ID: "af2",
Data: []byte("data2"),
Endpoints: [][]byte{},
}, notification.Affiliate)
case <-time.After(time.Second):
assert.Fail(t, "no notification")
}
cluster.DeleteAffiliate("af1")
assert.Len(t, cluster.List(), 1)
assert.Equal(t, []*state.AffiliateExport{
{
ID: "af2",
Data: []byte("data2"),
Endpoints: [][]byte{},
},
}, cluster.List())
select {
case notification := <-updates:
assert.Equal(t, "af1", notification.AffiliateID)
assert.Nil(t, notification.Affiliate)
case <-time.After(time.Second):
assert.Fail(t, "no notification")
}
empty := cluster.GarbageCollect(now)
assert.False(t, empty)
empty = cluster.GarbageCollect(now.Add(2 * time.Minute))
assert.True(t, empty)
select {
case notification := <-updates:
assert.Equal(t, "af2", notification.AffiliateID)
assert.Nil(t, notification.Affiliate)
case <-time.After(time.Second):
assert.Fail(t, "no notification")
}
select {
case err := <-subscription.ErrCh():
assert.NoError(t, err)
default:
}
}
func TestClusterSubscriptions(t *testing.T) {
t.Parallel()
now := time.Now()
cluster := state.NewCluster("cluster2")
// create live and dead subscribers
liveSubscribers := make([]*state.Subscription, 5)
deadSubscribers := make([]*state.Subscription, 2)
channels := make([]chan *state.Notification, len(liveSubscribers))
for i := range liveSubscribers {
var snapshot []*state.AffiliateExport
channels[i] = make(chan *state.Notification, 16)
snapshot, liveSubscribers[i] = cluster.Subscribe(channels[i])
assert.Empty(t, snapshot)
defer liveSubscribers[i].Close()
}
for i := range deadSubscribers {
var snapshot []*state.AffiliateExport
snapshot, deadSubscribers[i] = cluster.Subscribe(make(chan<- *state.Notification, 1))
assert.Empty(t, snapshot)
}
cluster.WithAffiliate("af1", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data1"), now.Add(time.Minute))
})
cluster.WithAffiliate("af2", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data2"), now.Add(time.Minute))
})
cluster.WithAffiliate("af2", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data2_1"), now.Add(time.Minute))
})
cluster.DeleteAffiliate("af2")
// dead subscribers should have errored out
for i := range deadSubscribers {
select {
case err := <-deadSubscribers[i].ErrCh():
assert.Error(t, err)
default:
assert.Fail(t, "error is expected")
}
}
// live subscribers should have no error
for i := range liveSubscribers {
select {
case <-liveSubscribers[i].ErrCh():
assert.Fail(t, "error is not expected")
default:
}
}
assertNotification := func(ch <-chan *state.Notification, expected *state.Notification) {
select {
case notification := <-ch:
assert.Equal(t, expected, notification)
default:
assert.Fail(t, "no message")
}
}
for _, ch := range channels {
assertNotification(ch, &state.Notification{
AffiliateID: "af1",
Affiliate: &state.AffiliateExport{
ID: "af1",
Data: []byte("data1"),
Endpoints: [][]byte{},
},
})
assertNotification(ch, &state.Notification{
AffiliateID: "af2",
Affiliate: &state.AffiliateExport{
ID: "af2",
Data: []byte("data2"),
Endpoints: [][]byte{},
},
})
assertNotification(ch, &state.Notification{
AffiliateID: "af2",
Affiliate: &state.AffiliateExport{
ID: "af2",
Data: []byte("data2_1"),
Endpoints: [][]byte{},
},
})
assertNotification(ch, &state.Notification{
AffiliateID: "af2",
})
}
}

28
internal/state/export.go Normal file
View File

@ -0,0 +1,28 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state
// AffiliateExport represents a read-only copy of affiliate.
type AffiliateExport struct {
ID string
Data []byte
Endpoints [][]byte
}
// Export the affiliate into AffiliateExport.
func (affiliate *Affiliate) Export() *AffiliateExport {
result := &AffiliateExport{
ID: affiliate.id,
Data: affiliate.data,
}
result.Endpoints = make([][]byte, 0, len(affiliate.endpoints))
for _, endpoint := range affiliate.endpoints {
result.Endpoints = append(result.Endpoints, endpoint.data)
}
return result
}

71
internal/state/state.go Normal file
View File

@ -0,0 +1,71 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package state implements server state with clusters, affiliates, subscriptions, etc.
package state
import (
"context"
"sync"
"time"
"go.uber.org/zap"
)
// State keeps the discovery service state.
type State struct {
clusters sync.Map
}
// NewState create new instance of State.
func NewState() *State {
return &State{}
}
// GetCluster returns (or creates) new cluster by ID.
func (state *State) GetCluster(id string) *Cluster {
if v, ok := state.clusters.Load(id); ok {
return v.(*Cluster)
}
v, _ := state.clusters.LoadOrStore(id, NewCluster(id))
return v.(*Cluster)
}
// GarbageCollect recursively each cluster, and remove empty clusters.
func (state *State) GarbageCollect(now time.Time) (removedClusters int) {
state.clusters.Range(func(key, value interface{}) bool {
cluster := value.(*Cluster) //nolint:errcheck,forcetypeassert
if cluster.GarbageCollect(now) {
state.clusters.Delete(key)
removedClusters++
}
return true
})
return removedClusters
}
// RunGC runs the garbage collection on interval.
func (state *State) RunGC(ctx context.Context, logger *zap.Logger, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for ctx.Err() == nil {
removedClusters := state.GarbageCollect(time.Now())
if removedClusters > 0 {
logger.Info("garbage collection run", zap.Int("removed_clusters", removedClusters))
}
select {
case <-ctx.Done():
case <-ticker.C:
}
}
}

View File

@ -0,0 +1,37 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/discovery-service/internal/state"
)
func TestState(t *testing.T) {
now := time.Now()
st := state.NewState()
deletedClusters := st.GarbageCollect(now)
assert.Equal(t, 0, deletedClusters)
st.GetCluster("id1")
st.GetCluster("id2").WithAffiliate("af1", func(affiliate *state.Affiliate) {
affiliate.Update([]byte("data1"), now.Add(time.Minute))
})
deletedClusters = st.GarbageCollect(now)
assert.Equal(t, 1, deletedClusters)
deletedClusters = st.GarbageCollect(now.Add(2 * time.Minute))
assert.Equal(t, 1, deletedClusters)
deletedClusters = st.GarbageCollect(now)
assert.Equal(t, 0, deletedClusters)
}

View File

@ -0,0 +1,45 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package state
import "fmt"
// Notification about affiliate update.
//
// Affiliate is nil, then affiliate was deleted.
type Notification struct {
Affiliate *AffiliateExport
AffiliateID string
}
// Subscription is a handle returned to the subscriber.
type Subscription struct {
cluster *Cluster
errCh chan error
ch chan<- *Notification
}
// ErrCh returns error channel, whenever there's error on the channel, subscription is invalid.
func (subscription *Subscription) ErrCh() <-chan error {
return subscription.errCh
}
// Close subscription (unsubscribe).
func (subscription *Subscription) Close() {
subscription.cluster.unsubscribe(subscription)
}
func (subscription *Subscription) notify(notification *Notification) {
select {
case subscription.ch <- notification:
return
default:
}
subscription.errCh <- fmt.Errorf("lost update")
subscription.Close()
}

View File

@ -1,110 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package client implements http client for the manager API.
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/talos-systems/kubespan-manager/pkg/types"
)
// Add adds a Node to the Cluster database, updating it if it already exists.
func Add(rootURL, clusterID string, n *types.Node) error {
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(n); err != nil {
return fmt.Errorf("failed to encode Node information: %w", err)
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, fmt.Sprintf("%s/%s", rootURL, clusterID), body)
if err != nil {
return fmt.Errorf("failed to post Node information: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to post Node information: %w", err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode > 299 {
return fmt.Errorf("server rejected Node information: %s", resp.Status)
}
return nil
}
// AddAddresses adds a list of addresses to a node.
func AddAddresses(rootURL, clusterID, id string, epList ...*types.Address) error {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(epList); err != nil {
return fmt.Errorf("failed to encode known endpoints: %w", err)
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPut, fmt.Sprintf("%s/%s/%s", rootURL, clusterID, id), buf)
if err != nil {
return fmt.Errorf("failed to make PUT request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to add endpoints to node %q/%q: %w", clusterID, id, err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode > 299 {
return fmt.Errorf("server rejected new endpoints for node %q/%q: %s", clusterID, id, resp.Status)
}
return nil
}
// Get returns the Node defined by the given public key, if and only if it exists within the given Cluster ID.
func Get(rootURL, clusterID, publicKey string) (*types.Node, error) {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, fmt.Sprintf("%s/%s/%s", rootURL, clusterID, publicKey), nil)
if err != nil {
return nil, fmt.Errorf("failed to request node %q/%q from server %q: %w", clusterID, publicKey, rootURL, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to request node %q/%q from server %q: %w", clusterID, publicKey, rootURL, err)
}
defer resp.Body.Close() //nolint:errcheck
node := new(types.Node)
if err = json.NewDecoder(resp.Body).Decode(node); err != nil {
return nil, fmt.Errorf("failed to decode response from server: %w", err)
}
return node, nil
}
// List returns the set of Nodes associated with the given Cluster ID.
func List(rootURL, clusterID string) ([]*types.Node, error) {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, fmt.Sprintf("%s/%s", rootURL, clusterID), nil)
if err != nil {
return nil, fmt.Errorf("failed to request list from server %q: %w", rootURL, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to request list from server %q: %w", rootURL, err)
}
defer resp.Body.Close() //nolint:errcheck
var list []*types.Node
if err = json.NewDecoder(resp.Body).Decode(&list); err != nil {
return nil, fmt.Errorf("failed to decode response from server: %w", err)
}
return list, nil
}

18
pkg/server/metrics.go Normal file
View File

@ -0,0 +1,18 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package server
import (
prom "github.com/prometheus/client_golang/prometheus"
)
var metricVersionGauge = prom.NewGaugeVec(prom.GaugeOpts{
Name: "discovery_hello_requests",
Help: "Number of hello requests split by client version.",
}, []string{"client_version"})
func init() {
prom.MustRegister(metricVersionGauge)
}

183
pkg/server/server.go Normal file
View File

@ -0,0 +1,183 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package server implements server-side part of gRPC API.
package server
import (
"context"
"net"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"inet.af/netaddr"
"github.com/talos-systems/discovery-service/api/v1alpha1/pb"
"github.com/talos-systems/discovery-service/internal/state"
)
const updateBuffer = 32
// ClusterServer implements discovery cluster gRPC API.
type ClusterServer struct {
pb.UnimplementedClusterServer
state *state.State
}
// NewClusterServer builds new ClusterServer.
func NewClusterServer(state *state.State) *ClusterServer {
return &ClusterServer{
state: state,
}
}
// Hello implements cluster API.
func (srv *ClusterServer) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
metricVersionGauge.WithLabelValues(req.ClientVersion).Inc()
if err := validateClusterID(req.ClusterId); err != nil {
return nil, err
}
resp := &pb.HelloResponse{}
if peer, ok := peer.FromContext(ctx); ok {
if addr, ok := peer.Addr.(*net.TCPAddr); ok {
if ip, ok := netaddr.FromStdIP(addr.IP); ok {
var err error
resp.ClientIp, err = ip.MarshalBinary()
if err != nil {
return nil, err
}
}
}
}
return resp, nil
}
// AffiliateUpdate implements cluster API.
func (srv *ClusterServer) AffiliateUpdate(ctx context.Context, req *pb.AffiliateUpdateRequest) (*pb.AffiliateUpdateResponse, error) {
if err := validateClusterID(req.ClusterId); err != nil {
return nil, err
}
if err := validateAffiliateID(req.AffiliateId); err != nil {
return nil, err
}
srv.state.GetCluster(req.ClusterId).WithAffiliate(req.AffiliateId, func(affiliate *state.Affiliate) {
expiration := time.Now().Add(req.Ttl.AsDuration())
if len(req.AffiliateData) > 0 {
affiliate.Update(req.AffiliateData, expiration)
}
affiliate.MergeEndpoints(req.AffiliateEndpoints, expiration)
})
return &pb.AffiliateUpdateResponse{}, nil
}
// AffiliateDelete implements cluster API.
func (srv *ClusterServer) AffiliateDelete(ctx context.Context, req *pb.AffiliateDeleteRequest) (*pb.AffiliateDeleteResponse, error) {
if err := validateClusterID(req.ClusterId); err != nil {
return nil, err
}
if err := validateAffiliateID(req.AffiliateId); err != nil {
return nil, err
}
srv.state.GetCluster(req.ClusterId).DeleteAffiliate(req.AffiliateId)
return &pb.AffiliateDeleteResponse{}, nil
}
// List implements cluster API.
func (srv *ClusterServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) {
if err := validateClusterID(req.ClusterId); err != nil {
return nil, err
}
affiliates := srv.state.GetCluster(req.ClusterId).List()
resp := &pb.ListResponse{
Affiliates: make([]*pb.Affiliate, 0, len(affiliates)),
}
for _, affiliate := range affiliates {
resp.Affiliates = append(resp.Affiliates, &pb.Affiliate{
Id: affiliate.ID,
Data: affiliate.Data,
Endpoints: affiliate.Endpoints,
})
}
return resp, nil
}
// Watch implements cluster API.
func (srv *ClusterServer) Watch(req *pb.WatchRequest, server pb.Cluster_WatchServer) error {
if err := validateClusterID(req.ClusterId); err != nil {
return err
}
// make enough room to handle connection issues
updates := make(chan *state.Notification, updateBuffer)
snapshot, subscription := srv.state.GetCluster(req.ClusterId).Subscribe(updates)
defer subscription.Close()
for _, affiliate := range snapshot {
if err := server.Send(&pb.WatchResponse{
Affiliate: &pb.Affiliate{
Id: affiliate.ID,
Data: affiliate.Data,
Endpoints: affiliate.Endpoints,
},
}); err != nil {
if status.Code(err) == codes.Canceled {
return nil
}
return err
}
}
for {
select {
case <-server.Context().Done():
return nil
case err := <-subscription.ErrCh():
return status.Errorf(codes.Aborted, "subscription canceled: %s", err)
case notification := <-updates:
resp := &pb.WatchResponse{}
if notification.Affiliate == nil {
resp.Deleted = true
resp.Affiliate = &pb.Affiliate{
Id: notification.AffiliateID,
}
} else {
resp.Affiliate = &pb.Affiliate{
Id: notification.Affiliate.ID,
Data: notification.Affiliate.Data,
Endpoints: notification.Affiliate.Endpoints,
}
}
if err := server.Send(resp); err != nil {
if status.Code(err) == codes.Canceled {
return nil
}
return err
}
}
}
}

324
pkg/server/server_test.go Normal file
View File

@ -0,0 +1,324 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package server_test
import (
"context"
"net"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/talos-systems/discovery-service/api/v1alpha1/pb"
"github.com/talos-systems/discovery-service/internal/state"
"github.com/talos-systems/discovery-service/pkg/server"
)
func setupServer(t *testing.T) (address string) {
t.Helper()
lis, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
s := grpc.NewServer()
pb.RegisterClusterServer(s, server.NewClusterServer(state.NewState()))
go func() {
require.NoError(t, s.Serve(lis))
}()
t.Cleanup(s.Stop)
return lis.Addr().String()
}
func TestServerAPI(t *testing.T) {
t.Parallel()
addr := setupServer(t)
conn, err := grpc.Dial(addr, grpc.WithInsecure())
require.NoError(t, err)
client := pb.NewClusterClient(conn)
t.Run("Hello", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resp, err := client.Hello(ctx, &pb.HelloRequest{
ClusterId: "fake",
ClientVersion: "v0.12.0",
})
require.NoError(t, err)
assert.Equal(t, []byte{0x7f, 0x0, 0x0, 0x1}, resp.ClientIp) // 127.0.0.1
})
t.Run("AffiliateUpdate", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake1",
AffiliateId: "af1",
AffiliateData: []byte("data1"),
AffiliateEndpoints: [][]byte{
[]byte("e1"),
[]byte("e2"),
},
Ttl: durationpb.New(time.Minute),
})
require.NoError(t, err)
resp, err := client.List(ctx, &pb.ListRequest{
ClusterId: "fake1",
})
require.NoError(t, err)
require.Len(t, resp.Affiliates, 1)
assert.Equal(t, "af1", resp.Affiliates[0].Id)
assert.Equal(t, []byte("data1"), resp.Affiliates[0].Data)
assert.Equal(t, [][]byte{[]byte("e1"), []byte("e2")}, resp.Affiliates[0].Endpoints)
// add more endpoints
_, err = client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake1",
AffiliateId: "af1",
AffiliateEndpoints: [][]byte{
[]byte("e3"),
[]byte("e2"),
},
Ttl: durationpb.New(time.Minute),
})
require.NoError(t, err)
resp, err = client.List(ctx, &pb.ListRequest{
ClusterId: "fake1",
})
require.NoError(t, err)
require.Len(t, resp.Affiliates, 1)
assert.Equal(t, "af1", resp.Affiliates[0].Id)
assert.Equal(t, []byte("data1"), resp.Affiliates[0].Data)
assert.Equal(t, [][]byte{[]byte("e1"), []byte("e2"), []byte("e3")}, resp.Affiliates[0].Endpoints)
})
t.Run("AffiliateDelete", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{
ClusterId: "fake2",
AffiliateId: "af1",
})
require.NoError(t, err)
_, err = client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake2",
AffiliateId: "af1",
AffiliateData: []byte("data1"),
})
require.NoError(t, err)
_, err = client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{
ClusterId: "fake2",
AffiliateId: "af1",
})
require.NoError(t, err)
resp, err := client.List(ctx, &pb.ListRequest{
ClusterId: "fake2",
})
require.NoError(t, err)
assert.Len(t, resp.Affiliates, 0)
})
t.Run("Watch", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake3",
AffiliateId: "af1",
AffiliateData: []byte("data1"),
})
require.NoError(t, err)
cli, err := client.Watch(ctx, &pb.WatchRequest{
ClusterId: "fake3",
})
require.NoError(t, err)
msg, err := cli.Recv()
require.NoError(t, err)
assert.True(t, proto.Equal(&pb.WatchResponse{
Deleted: false,
Affiliate: &pb.Affiliate{
Id: "af1",
Data: []byte("data1"),
Endpoints: [][]byte{},
},
}, msg))
_, err = client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake3",
AffiliateId: "af2",
AffiliateData: []byte("data2"),
})
require.NoError(t, err)
msg, err = cli.Recv()
require.NoError(t, err)
assert.True(t, proto.Equal(&pb.WatchResponse{
Deleted: false,
Affiliate: &pb.Affiliate{
Id: "af2",
Data: []byte("data2"),
Endpoints: [][]byte{},
},
}, msg))
_, err = client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{
ClusterId: "fake3",
AffiliateId: "af1",
})
require.NoError(t, err)
msg, err = cli.Recv()
require.NoError(t, err)
assert.True(t, proto.Equal(&pb.WatchResponse{
Deleted: true,
Affiliate: &pb.Affiliate{
Id: "af1",
},
}, msg))
})
}
func TestValidation(t *testing.T) {
t.Parallel()
addr := setupServer(t)
conn, err := grpc.Dial(addr, grpc.WithInsecure())
require.NoError(t, err)
client := pb.NewClusterClient(conn)
ctx := context.Background()
t.Run("Hello", func(t *testing.T) {
t.Parallel()
_, err := client.Hello(ctx, &pb.HelloRequest{})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.Hello(ctx, &pb.HelloRequest{
ClusterId: strings.Repeat("A", 1024),
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
})
t.Run("AffiliateUpdate", func(t *testing.T) {
t.Parallel()
_, err := client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: strings.Repeat("A", 1024),
AffiliateId: "fake",
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.AffiliateUpdate(ctx, &pb.AffiliateUpdateRequest{
ClusterId: "fake",
AffiliateId: strings.Repeat("A", 1024),
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
})
t.Run("AffiliateDelete", func(t *testing.T) {
t.Parallel()
_, err := client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{
ClusterId: strings.Repeat("A", 1024),
AffiliateId: "fake",
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.AffiliateDelete(ctx, &pb.AffiliateDeleteRequest{
ClusterId: "fake",
AffiliateId: strings.Repeat("A", 1024),
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
})
t.Run("List", func(t *testing.T) {
t.Parallel()
_, err := client.List(ctx, &pb.ListRequest{})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
_, err = client.List(ctx, &pb.ListRequest{
ClusterId: strings.Repeat("A", 1024),
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
})
t.Run("Watch", func(t *testing.T) {
t.Parallel()
cli, err := client.Watch(ctx, &pb.WatchRequest{})
require.NoError(t, err)
_, err = cli.Recv()
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
cli, err = client.Watch(ctx, &pb.WatchRequest{
ClusterId: strings.Repeat("A", 1024),
})
require.NoError(t, err)
_, err = cli.Recv()
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
})
}

39
pkg/server/validate.go Normal file
View File

@ -0,0 +1,39 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package server
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
clusterIDMax = 256
affiliateIDMax = 256
)
func validateClusterID(id string) error {
if len(id) < 1 {
return status.Errorf(codes.InvalidArgument, "cluster ID can't be empty")
}
if len(id) > clusterIDMax {
return status.Errorf(codes.InvalidArgument, "cluster ID is too long")
}
return nil
}
func validateAffiliateID(id string) error {
if len(id) < 1 {
return status.Errorf(codes.InvalidArgument, "affiliate ID can't be empty")
}
if len(id) > affiliateIDMax {
return status.Errorf(codes.InvalidArgument, "affiliate ID is too long")
}
return nil
}

View File

@ -1,154 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package types contains all manager data types.
package types
import (
"encoding/json"
"fmt"
"net"
"sync"
"time"
"inet.af/netaddr"
)
// Address describes an IP or DNS address with optional Port.
type Address struct {
// LastReported indicates the time at which this address was last reported.
LastReported time.Time `json:"lastReported"`
// IP is the IP address of this NodeAddress, if known.
IP netaddr.IP `json:"ip,omitempty"`
// Name is the DNS name of this NodeAddress, if known.
Name string `json:"name,omitempty"`
// Port is the port number for this NodeAddress, if known.
Port uint16 `json:"port,omitempty"`
}
// EqualHost indicates whether two addresses have the same host portion, ignoring the ports.
func (a *Address) EqualHost(other *Address) bool {
if !a.IP.IsZero() || !other.IP.IsZero() {
return a.IP == other.IP
}
return a.Name == other.Name
}
// Equal indicates whether two addresses are equal.
func (a *Address) Equal(other *Address) bool {
if !a.EqualHost(other) {
return false
}
return a.Port == other.Port
}
// Endpoint returns a UDP endpoint address for the Address, using the defaultPort if none is known.
func (a *Address) Endpoint(defaultPort uint16) (*net.UDPAddr, error) {
proto := "udp"
addr := a.Name
port := a.Port
if !a.IP.IsZero() {
addr = a.IP.String()
if a.IP.Is6() {
proto = "udp6"
addr = "[" + addr + "]"
} else {
proto = "udp4"
}
}
if port == 0 {
port = defaultPort
}
return net.ResolveUDPAddr(proto, fmt.Sprintf("%s:%d", addr, port))
}
// Node describes a Wireguard Peer.
type Node struct {
// Name is the human-readable identifier of this Node.
// Usually, this is the kubernetes Node name.
// It *should* generally be unique, but it is not required to be so.
Name string `json:"name,omitempty"`
// ID is the unique identifier for this Node.
// Usually, this is the Wireguard Public Key of the Node.
ID string `json:"id,omitempty"`
// IP is the IP address of the Wireguard interface on this Node.
IP netaddr.IP `json:"ip,omitempty"`
// Addresses is a list of addresses for the Node.
Addresses []*Address `json:"selfIPs,omitempty"`
mu sync.Mutex
}
// AddAddresses adds a set of addresses to a Node.
func (n *Node) AddAddresses(addresses ...*Address) {
n.mu.Lock()
defer n.mu.Unlock()
for _, a := range addresses {
var found bool
if a.LastReported.IsZero() {
a.LastReported = time.Now()
}
for _, existing := range n.Addresses {
if a.EqualHost(existing) {
found = true
if a.Port > 0 {
existing.Port = a.Port
}
existing.LastReported = a.LastReported
break
}
}
if !found {
n.Addresses = append(n.Addresses, a)
}
}
}
// ExpireAddressesOlderThan removes addresses from the Node which have not been reported within the given timeframe.
func (n *Node) ExpireAddressesOlderThan(maxAge time.Duration) {
n.mu.Lock()
defer n.mu.Unlock()
i := 0
for _, a := range n.Addresses {
if time.Since(a.LastReported) < maxAge {
n.Addresses[i] = a
i++
continue
}
}
n.Addresses = n.Addresses[:i]
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (n *Node) MarshalBinary() ([]byte, error) {
return json.Marshal(n)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (n *Node) UnmarshalBinary(data []byte) error {
*n = Node{}
return json.Unmarshal(data, n)
}

View File

@ -1,48 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types_test
import (
"testing"
"time"
"inet.af/netaddr"
"github.com/talos-systems/kubespan-manager/pkg/types"
)
func TestEncoderDecoder(t *testing.T) {
n := &types.Node{
Name: "tester",
ID: "IHOPEfmiUG1kE832FAxm77J5WP0O1ZHp9OwqbGowL1E=",
IP: netaddr.MustParseIP("2001:db8:1001::1"),
Addresses: []*types.Address{
{
Name: "mynode.mydomain.com",
Port: 51512,
LastReported: time.Now(),
},
{
IP: netaddr.MustParseIP("2001:db8:2002::2"),
Port: 52522,
LastReported: time.Now(),
},
},
}
data, err := n.MarshalBinary()
if err != nil {
t.Errorf("failed to marshal node: %w", err)
}
n2 := new(types.Node)
if err = n2.UnmarshalBinary(data); err != nil {
t.Errorf("failed to unmarshal node: %w", err)
}
if n.ID != n2.ID {
t.Errorf("IDs do not match: %s != %s", n.ID, n2.ID)
}
}