From 583532e66508b7f4a8f49ca42e0289a6c0f68320 Mon Sep 17 00:00:00 2001 From: Nikita Vaniasin Date: Mon, 15 Aug 2022 10:23:25 +0200 Subject: [PATCH] [Feature] New gRPC and HTTP listeners (#1036) --- CHANGELOG.md | 1 + Makefile | 33 ++- chart/kube-arangodb/templates/service.yaml | 8 + cmd/cmd.go | 52 +++- docs/design/README.md | 1 + docs/design/api.md | 30 +++ go.mod | 11 +- go.sum | 18 +- pkg/api/api.go | 130 ++++++++++ pkg/api/auth.go | 82 +++++++ pkg/api/grpc.go | 39 +++ pkg/api/http.go | 79 +++++++ pkg/api/jwt.go | 95 ++++++++ pkg/api/server/operator.pb.go | 261 +++++++++++++++++++++ pkg/api/server/operator.proto | 39 +++ pkg/api/server/operator_grpc.pb.go | 105 +++++++++ pkg/api/tls.go | 91 +++++++ pkg/generated/timezones/timezones.go | 2 +- 18 files changed, 1067 insertions(+), 10 deletions(-) create mode 100644 docs/design/api.md create mode 100644 pkg/api/api.go create mode 100644 pkg/api/auth.go create mode 100644 pkg/api/grpc.go create mode 100644 pkg/api/http.go create mode 100644 pkg/api/jwt.go create mode 100644 pkg/api/server/operator.pb.go create mode 100644 pkg/api/server/operator.proto create mode 100644 pkg/api/server/operator_grpc.pb.go create mode 100644 pkg/api/tls.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e221ace..c979a4659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ - (Bugfix) Infinite loop fix in ArangoD AsyncClient - (Bugfix) Add Panic Handler - (Bugfix) Unify yaml packages +- (Feature) Add new GRPC and HTTP API ## [1.2.13](https://github.com/arangodb/kube-arangodb/tree/1.2.13) (2022-06-07) - (Bugfix) Fix arangosync members state inspection diff --git a/Makefile b/Makefile index ba761275b..b29cb857e 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,14 @@ else COMPILE_DEBUG_FLAGS := endif +PROTOC_VERSION := 21.1 +ifeq ($(shell uname),Darwin) + PROTOC_ARCHIVE_SUFFIX := osx-universal_binary +else + PROTOC_ARCHIVE_SUFFIX := linux-x86_64 +endif +PROTOC_URL := https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-${PROTOC_ARCHIVE_SUFFIX}.zip + ifeq ($(MANIFESTSUFFIX),-) # Release setting MANIFESTSUFFIX := @@ -157,7 +165,7 @@ endif EXCLUDE_DIRS := vendor .gobuild deps tools pkg/generated/clientset pkg/generated/informers pkg/generated/listers EXCLUDE_FILES := *generated.deepcopy.go -SOURCES_QUERY := find ./ -type f -name '*.go' $(foreach EXCLUDE_DIR,$(EXCLUDE_DIRS), ! -path "*/$(EXCLUDE_DIR)/*") $(foreach EXCLUDE_FILE,$(EXCLUDE_FILES), ! -path "*/$(EXCLUDE_FILE)") +SOURCES_QUERY := find ./ -type f -name '*.go' ! -name '*.pb.go' $(foreach EXCLUDE_DIR,$(EXCLUDE_DIRS), ! -path "*/$(EXCLUDE_DIR)/*") $(foreach EXCLUDE_FILE,$(EXCLUDE_FILES), ! -path "*/$(EXCLUDE_FILE)") SOURCES := $(shell $(SOURCES_QUERY)) DASHBOARDSOURCES := $(shell find $(DASHBOARDDIR)/src -name '*.js') $(DASHBOARDDIR)/package.json LINT_EXCLUDES:= @@ -167,6 +175,8 @@ else LINT_EXCLUDES+=.*\.enterprise\.go$$ endif +PROTOSOURCES := $(shell find ./ -type f -name '*.proto' $(foreach EXCLUDE_DIR,$(EXCLUDE_DIRS), ! -path "*/$(EXCLUDE_DIR)/*") | sort) + .DEFAULT_GOAL := all .PHONY: all all: check-vars verify-generated build @@ -185,7 +195,7 @@ allall: all .PHONY: license-verify license-verify: @echo ">> Verify license of files" - @$(GOPATH)/bin/addlicense -f "./tools/codegen/license-header.txt" -check $(SOURCES) + @$(GOPATH)/bin/addlicense -f "./tools/codegen/license-header.txt" -check $(SOURCES) $(PROTOSOURCES) .PHONY: fmt fmt: @@ -196,7 +206,7 @@ fmt: .PHONY: license license: @echo ">> Ensuring license of files" - @$(GOPATH)/bin/addlicense -f "./tools/codegen/license-header.txt" $(SOURCES) + @$(GOPATH)/bin/addlicense -f "./tools/codegen/license-header.txt" $(SOURCES) $(PROTOSOURCES) .PHONY: fmt-verify fmt-verify: license-verify @@ -464,6 +474,13 @@ tools: update-vendor @GOBIN=$(GOPATH)/bin go install github.com/jessevdk/go-assets-builder@b8483521738fd2198ecfc378067a4e8a6079f8e5 @echo ">> Fetching gci" @GOBIN=$(GOPATH)/bin go install github.com/daixiang0/gci@v0.3.0 + @echo ">> Downloading protobuf compiler..." + @curl -L ${PROTOC_URL} -o $(GOPATH)/protoc.zip + @echo ">> Unzipping protobuf compiler..." + @unzip -o $(GOPATH)/protoc.zip -d $(GOPATH)/ + @echo ">> Fetching protoc go plugins..." + @GOBIN=$(GOPATH)/bin go install github.com/golang/protobuf/protoc-gen-go@v1.5.2 + @GOBIN=$(GOPATH)/bin go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 .PHONY: vendor vendor: @@ -532,5 +549,13 @@ check-community: _check: @$(MAKE) fmt license-verify linter run-unit-tests bin +generate: generate-internal generate-proto fmt + generate-internal: - ROOT=$(ROOT) go test --count=1 "$(REPOPATH)/internal/..." \ No newline at end of file + ROOT=$(ROOT) go test --count=1 "$(REPOPATH)/internal/..." + +generate-proto: + PATH=$(PATH):$(GOBUILDDIR)/bin $(GOBUILDDIR)/bin/protoc -I.:$(GOBUILDDIR)/include/ \ + --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + $(PROTOSOURCES) \ No newline at end of file diff --git a/chart/kube-arangodb/templates/service.yaml b/chart/kube-arangodb/templates/service.yaml index ac5832229..2005e2f07 100644 --- a/chart/kube-arangodb/templates/service.yaml +++ b/chart/kube-arangodb/templates/service.yaml @@ -19,6 +19,14 @@ spec: port: 8528 protocol: TCP targetPort: 8528 + - name: http-api + port: 8628 + protocol: TCP + targetPort: 8628 + - name: grpc-api + port: 8728 + protocol: TCP + targetPort: 8728 selector: app.kubernetes.io/name: {{ template "kube-arangodb.name" . }} app.kubernetes.io/managed-by: {{ .Release.Service }} diff --git a/cmd/cmd.go b/cmd/cmd.go index 5288e6317..9df3b7613 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -45,6 +45,7 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/klog" + "github.com/arangodb/kube-arangodb/pkg/api" deploymentApi "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/crd" "github.com/arangodb/kube-arangodb/pkg/deployment/features" @@ -68,8 +69,12 @@ import ( const ( defaultServerHost = "0.0.0.0" defaultServerPort = 8528 + defaultAPIHTTPPort = 8628 + defaultAPIGRPCPort = 8728 defaultLogLevel = "debug" defaultAdminSecretName = "arangodb-operator-dashboard" + defaultAPIJWTSecretName = "arangodb-operator-api-jwt" + defaultAPIJWTKeySecretName = "arangodb-operator-api-jwt-key" defaultAlpineImage = "alpine:3.7" defaultMetricsExporterImage = "arangodb/arangodb-exporter:0.1.6" defaultArangoImage = "arangodb/arangodb:latest" @@ -98,6 +103,14 @@ var ( adminSecretName string // Name of basic authentication secret containing the admin username+password of the dashboard allowAnonymous bool // If set, anonymous access to dashboard is allowed } + apiOptions struct { + enabled bool + httpPort int + grpcPort int + jwtSecretName string + jwtKeySecretName string + tlsSecretName string + } operatorOptions struct { enableDeployment bool // Run deployment operator enableDeploymentReplication bool // Run deployment-replication operator @@ -158,6 +171,12 @@ func init() { f.StringVar(&serverOptions.adminSecretName, "server.admin-secret-name", defaultAdminSecretName, "Name of secret containing username + password for login to the dashboard") f.BoolVar(&serverOptions.allowAnonymous, "server.allow-anonymous-access", false, "Allow anonymous access to the dashboard") f.StringArrayVar(&logLevels, "log.level", []string{defaultLogLevel}, fmt.Sprintf("Set log levels in format or =. Possible loggers: %s", strings.Join(logging.Global().Names(), ", "))) + f.BoolVar(&apiOptions.enabled, "api.enabled", true, "Enable operator HTTP and gRPC API") + f.IntVar(&apiOptions.httpPort, "api.http-port", defaultAPIHTTPPort, "HTTP API port to listen on") + f.IntVar(&apiOptions.grpcPort, "api.grpc-port", defaultAPIGRPCPort, "gRPC API port to listen on") + f.StringVar(&apiOptions.tlsSecretName, "api.tls-secret-name", "", "Name of secret containing tls.crt & tls.key for HTTPS API (if empty, self-signed certificate is used)") + f.StringVar(&apiOptions.jwtSecretName, "api.jwt-secret-name", defaultAPIJWTSecretName, "Name of secret which will contain JWT to authenticate API requests.") + f.StringVar(&apiOptions.jwtKeySecretName, "api.jwt-key-secret-name", defaultAPIJWTKeySecretName, "Name of secret containing key used to sign JWT. If there is no such secret present, value will be saved here") f.BoolVar(&operatorOptions.enableDeployment, "operator.deployment", false, "Enable to run the ArangoDeployment operator") f.BoolVar(&operatorOptions.enableDeploymentReplication, "operator.deployment-replication", false, "Enable to run the ArangoDeploymentReplication operator") f.BoolVar(&operatorOptions.enableStorage, "operator.storage", false, "Enable to run the ArangoLocalStorage operator") @@ -306,6 +325,37 @@ func executeMain(cmd *cobra.Command, args []string) { logger.Err(err).Fatal("Failed to create operator") } + if apiOptions.enabled { + apiServerCfg := api.ServerConfig{ + Namespace: namespace, + ServerName: name, + ServerAltNames: []string{ip}, + HTTPAddress: net.JoinHostPort("0.0.0.0", strconv.Itoa(apiOptions.httpPort)), + GRPCAddress: net.JoinHostPort("0.0.0.0", strconv.Itoa(apiOptions.grpcPort)), + TLSSecretName: apiOptions.tlsSecretName, + JWTSecretName: apiOptions.jwtSecretName, + JWTKeySecretName: apiOptions.jwtKeySecretName, + LivelinessProbe: &livenessProbe, + ProbeDeployment: api.ReadinessProbeConfig{ + Enabled: cfg.EnableDeployment, + Probe: &deploymentProbe, + }, + ProbeDeploymentReplication: api.ReadinessProbeConfig{ + Enabled: cfg.EnableDeploymentReplication, + Probe: &deploymentReplicationProbe, + }, + ProbeStorage: api.ReadinessProbeConfig{ + Enabled: cfg.EnableStorage, + Probe: &storageProbe, + }, + } + apiServer, err := api.NewServer(client.Kubernetes().CoreV1(), apiServerCfg) + if err != nil { + logger.Err(err).Fatal("Failed to create API server") + } + go utilsError.LogError(logger, "while running API server", apiServer.Run) + } + listenAddr := net.JoinHostPort(serverOptions.host, strconv.Itoa(serverOptions.port)) if svr, err := server.NewServer(client.Kubernetes().CoreV1(), server.Config{ Namespace: namespace, @@ -348,7 +398,7 @@ func executeMain(cmd *cobra.Command, args []string) { }); err != nil { logger.Err(err).Fatal("Failed to create HTTP server") } else { - go utilsError.LogError(logger, "error while starting service", svr.Run) + go utilsError.LogError(logger, "error while starting server", svr.Run) } // startChaos(context.Background(), cfg.KubeCli, cfg.Namespace, chaosLevel) diff --git a/docs/design/README.md b/docs/design/README.md index 415ddc8db..302f65585 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -13,3 +13,4 @@ - [Additional configuration](./additional_configuration.md) - [Topology awareness](./topology_awareness.md) - [Configuring timezone](./configuring_tz.md) +- [Operator API](./api.md) diff --git a/docs/design/api.md b/docs/design/api.md new file mode 100644 index 000000000..832dfdb0d --- /dev/null +++ b/docs/design/api.md @@ -0,0 +1,30 @@ +# Operator API + +A running operator exposes HTTP and gRPC API listeners to allow retrieving and setting some configuration values programmatically. +Both listeners require a secured connection to be established. It is possible to provide TLS certificate via k8s secret +using command line option `--api.tls-secret-name`. If secret name is not provided, operator will use self-signed certificate. + +Some HTTP endpoints require the authorization to work with. All gRPC endpoints require the authorization. +The authorization can be accomplished by providing JWT token in 'Authorization' header, e.g. `Authorization: Bearer ` +The JWT token can be fetched from k8s secret (by default `arangodb-operator-api-jwt`). The token is generated automatically +on operator startup using the signing key specified in `arangodb-operator-api-jwt-key` secret. If it is empty or not exists, +the signing key will be auto-generated and saved into secret. You can specify other signing key using `--api.jwt-key-secret-name` CLI option. + +## HTTP + +The HTTP API is running at endpoint specified by operator command line options `--api.http-port` (8628 by default). + +The HTTP API exposes endpoints used to get operator health and readiness status, operator version, and prometheus-compatible metrics. + +For now only `/metrics` endpoint require authorization. + + +## gRPC + +The gRPC API is running at endpoint specified by operator command line options `--api.grpc-port` (8728 by default). + +The gRPC API is exposed to allow programmatic access to some operator features and status. + +gRPC protobuf definitions and go-client can be found at `github.com/kube-arangodb/pkg/api/server` package. + +All gRPC requests require per-RPC metadata set to contain a valid Authorization header. diff --git a/go.mod b/go.mod index 7f1671182..b5b5af695 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,9 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c + google.golang.org/grpc v1.47.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.21.10 k8s.io/apiextensions-apiserver v0.18.3 @@ -54,6 +56,11 @@ require ( k8s.io/klog v1.0.0 ) +require ( + github.com/arangodb/rebalancer v0.1.1 // indirect + google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect +) + require ( github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -68,7 +75,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.5 // indirect + github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.1.2 // indirect github.com/googleapis/gnostic v0.4.1 // indirect @@ -95,7 +102,7 @@ require ( golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.28.0 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.9.0 // indirect diff --git a/go.sum b/go.sum index 646a760a0..51c0c861e 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -132,6 +136,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -226,8 +231,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 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/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -488,6 +494,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -612,6 +619,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -822,6 +831,8 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -842,6 +853,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 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= @@ -854,8 +867,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 000000000..1fca3421a --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,130 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "net" + "net/http" + "time" + + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + typedCore "k8s.io/client-go/kubernetes/typed/core/v1" + + pb "github.com/arangodb/kube-arangodb/pkg/api/server" + "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/util/probe" +) + +var apiLogger = logging.Global().RegisterAndGetLogger("api-server", logging.Info) + +type Server struct { + httpServer *http.Server + grpcServer *grpc.Server + grpcAddress string + + pb.UnimplementedOperatorServer +} + +type ReadinessProbeConfig struct { + Enabled bool + Probe *probe.ReadyProbe +} + +// ServerConfig settings for the Server +type ServerConfig struct { + Namespace string + ServerName string + ServerAltNames []string + HTTPAddress string + GRPCAddress string + TLSSecretName string + JWTSecretName string + JWTKeySecretName string + LivelinessProbe *probe.LivenessProbe + ProbeDeployment ReadinessProbeConfig + ProbeDeploymentReplication ReadinessProbeConfig + ProbeStorage ReadinessProbeConfig +} + +// NewServer creates and configure a new Server +func NewServer(cli typedCore.CoreV1Interface, cfg ServerConfig) (*Server, error) { + jwtSigningKey, err := ensureJWT(cli, cfg) + if err != nil { + return nil, err + } + + tlsConfig, err := prepareTLSConfig(cli, cfg) + if err != nil { + return nil, err + } + + auth := &authorization{jwtSigningKey: jwtSigningKey} + + s := &Server{ + httpServer: &http.Server{ + Addr: cfg.HTTPAddress, + ReadTimeout: time.Second * 30, + ReadHeaderTimeout: time.Second * 15, + WriteTimeout: time.Second * 30, + TLSConfig: tlsConfig, + }, + grpcServer: grpc.NewServer( + grpc.UnaryInterceptor(auth.ensureGRPCAuth), + grpc.Creds(credentials.NewTLS(tlsConfig)), + ), + grpcAddress: cfg.GRPCAddress, + } + handler, err := buildHTTPHandler(cfg, auth) + if err != nil { + return nil, err + } + s.httpServer.Handler = handler + + pb.RegisterOperatorServer(s.grpcServer, s) + return s, nil +} + +func (s *Server) Run() error { + g := errgroup.Group{} + g.Go(func() error { + apiLogger.Info("Serving HTTP API on %s", s.httpServer.Addr) + if err := s.httpServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + return err + } + return nil + }) + g.Go(func() error { + apiLogger.Info("Serving GRPC API on %s", s.grpcAddress) + ln, err := net.Listen("tcp", s.grpcAddress) + if err != nil { + return err + } + defer ln.Close() + + if err := s.grpcServer.Serve(ln); err != nil && err != grpc.ErrServerStopped { + return err + } + return nil + }) + return g.Wait() +} diff --git a/pkg/api/auth.go b/pkg/api/auth.go new file mode 100644 index 000000000..8ec1ac95c --- /dev/null +++ b/pkg/api/auth.go @@ -0,0 +1,82 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "context" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + jg "github.com/golang-jwt/jwt" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type authorization struct { + jwtSigningKey string +} + +func (a *authorization) isValid(token string) bool { + t, err := jg.Parse(token, func(_ *jg.Token) (interface{}, error) { + return []byte(a.jwtSigningKey), nil + }) + if err != nil { + apiLogger.Err(err).Info("invalid JWT: %s", token) + return false + } + return t.Valid +} + +// ensureHTTPAuth ensure a valid token exists within HTTP request header +func (a *authorization) ensureHTTPAuth(c *gin.Context) { + h := c.Request.Header.Values("Authorization") + bearerToken := extractBearerToken(h) + if !a.isValid(bearerToken) { + c.AbortWithStatus(http.StatusUnauthorized) + } +} + +// ensureGRPCAuth ensures a valid token exists within a GRPC request's metadata +func (a *authorization) ensureGRPCAuth(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "missing metadata") + } + + // The keys within metadata.MD are normalized to lowercase. + // See: https://godoc.org/google.golang.org/grpc/metadata#New + bearerToken := extractBearerToken(md["authorization"]) + if !a.isValid(bearerToken) { + return nil, status.Errorf(codes.Unauthenticated, "invalid token") + } + // Continue execution of handler after ensuring a valid token. + return handler(ctx, req) +} + +func extractBearerToken(authorization []string) string { + if len(authorization) < 1 { + return "" + } + return strings.TrimPrefix(authorization[0], "Bearer ") +} diff --git a/pkg/api/grpc.go b/pkg/api/grpc.go new file mode 100644 index 000000000..645c63f6f --- /dev/null +++ b/pkg/api/grpc.go @@ -0,0 +1,39 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "context" + + pb "github.com/arangodb/kube-arangodb/pkg/api/server" + "github.com/arangodb/kube-arangodb/pkg/version" +) + +func (s *Server) GetVersion(ctx context.Context, _ *pb.Empty) (*pb.Version, error) { + v := version.GetVersionV1() + return &pb.Version{ + Version: string(v.Version), + Build: v.Build, + Edition: string(v.Edition), + GoVersion: v.GoVersion, + BuildDate: v.BuildDate, + }, nil +} diff --git a/pkg/api/http.go b/pkg/api/http.go new file mode 100644 index 000000000..731afcfcd --- /dev/null +++ b/pkg/api/http.go @@ -0,0 +1,79 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + prometheus "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" + operatorHTTP "github.com/arangodb/kube-arangodb/pkg/util/http" + "github.com/arangodb/kube-arangodb/pkg/util/probe" + "github.com/arangodb/kube-arangodb/pkg/version" +) + +func buildHTTPHandler(cfg ServerConfig, auth *authorization) (http.Handler, error) { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Use(gin.Recovery()) + + versionV1Responder, err := operatorHTTP.NewSimpleJSONResponse(version.GetVersionV1()) + if err != nil { + return nil, errors.WithStack(err) + } + r.GET("/_api/version", gin.WrapF(versionV1Responder.ServeHTTP)) + r.GET("/api/v1/version", gin.WrapF(versionV1Responder.ServeHTTP)) + r.GET("/health", gin.WrapF(cfg.LivelinessProbe.LivenessHandler)) + + var readyProbes []*probe.ReadyProbe + if cfg.ProbeDeployment.Enabled { + r.GET("/ready/deployment", gin.WrapF(cfg.ProbeDeployment.Probe.ReadyHandler)) + readyProbes = append(readyProbes, cfg.ProbeDeployment.Probe) + } + if cfg.ProbeDeploymentReplication.Enabled { + r.GET("/ready/deployment-replication", gin.WrapF(cfg.ProbeDeploymentReplication.Probe.ReadyHandler)) + readyProbes = append(readyProbes, cfg.ProbeDeploymentReplication.Probe) + } + if cfg.ProbeStorage.Enabled { + r.GET("/ready/storage", gin.WrapF(cfg.ProbeStorage.Probe.ReadyHandler)) + readyProbes = append(readyProbes, cfg.ProbeStorage.Probe) + } + r.GET("/ready", gin.WrapF(handleGetReady(readyProbes...))) + + r.GET("/metrics", auth.ensureHTTPAuth, gin.WrapH(prometheus.Handler())) + + return r, nil +} + +func handleGetReady(probes ...*probe.ReadyProbe) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + for _, probe := range probes { + if !probe.IsReady() { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusOK) + } +} diff --git a/pkg/api/jwt.go b/pkg/api/jwt.go new file mode 100644 index 000000000..e2e042be7 --- /dev/null +++ b/pkg/api/jwt.go @@ -0,0 +1,95 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "context" + "crypto/rand" + "fmt" + "time" + + jg "github.com/golang-jwt/jwt" + typedCore "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/globals" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + secret "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret/v1" +) + +// ensureJWT ensure that JWT signing key exists or creates a new one. +// It also saves new token into secret if it is not present. +// Returns JWT signing key. +func ensureJWT(cli typedCore.CoreV1Interface, cfg ServerConfig) (string, error) { + secrets := cli.Secrets(cfg.Namespace) + + signingKey, err := k8sutil.GetTokenSecret(context.Background(), secrets, cfg.JWTKeySecretName) + if err != nil && k8sutil.IsNotFound(err) || signingKey == "" { + signingKey, err = createSigningKey(secrets, cfg.JWTKeySecretName) + if err != nil { + return "", err + } + } else if err != nil { + return "", errors.WithStack(err) + } + + _, err = k8sutil.GetTokenSecret(context.Background(), secrets, cfg.JWTSecretName) + if err != nil && k8sutil.IsNotFound(err) { + err = generateAndSaveJWT(secrets, cfg) + if err != nil { + return "", err + } + } else if err != nil { + return "", errors.WithStack(err) + } + return signingKey, nil +} + +// generateAndSaveJWT tries to generate new JWT using signing key retrieved from secret. +// If it is not present, it creates a new key. +// The resulting JWT is stored in secrets. +func generateAndSaveJWT(secrets secret.Interface, cfg ServerConfig) error { + claims := jg.MapClaims{ + "iss": fmt.Sprintf("kube-arangodb/%s", cfg.ServerName), + "iat": time.Now().Unix(), + } + err := k8sutil.CreateJWTFromSecret(context.Background(), secrets, secrets, cfg.JWTSecretName, cfg.JWTKeySecretName, claims, nil) + if err != nil { + return errors.WithStack(err) + } + return err +} + +func createSigningKey(secrets secret.ModInterface, keySecretName string) (string, error) { + signingKey := make([]byte, 32) + _, err := rand.Read(signingKey) + if err != nil { + return "", errors.WithStack(err) + } + + err = globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(context.Background(), func(ctxChild context.Context) error { + return k8sutil.CreateTokenSecret(ctxChild, secrets, keySecretName, string(signingKey), nil) + }) + if err != nil { + return "", errors.WithStack(err) + } + return string(signingKey), nil +} diff --git a/pkg/api/server/operator.pb.go b/pkg/api/server/operator.pb.go new file mode 100644 index 000000000..34d5c0e86 --- /dev/null +++ b/pkg/api/server/operator.pb.go @@ -0,0 +1,261 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.21.1 +// source: pkg/api/server/operator.proto + +package server + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_api_server_operator_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_pkg_api_server_operator_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_pkg_api_server_operator_proto_rawDescGZIP(), []int{0} +} + +type Version struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Build string `protobuf:"bytes,2,opt,name=build,proto3" json:"build,omitempty"` + Edition string `protobuf:"bytes,3,opt,name=edition,proto3" json:"edition,omitempty"` + GoVersion string `protobuf:"bytes,4,opt,name=go_version,json=goVersion,proto3" json:"go_version,omitempty"` + BuildDate string `protobuf:"bytes,5,opt,name=build_date,json=buildDate,proto3" json:"build_date,omitempty"` +} + +func (x *Version) Reset() { + *x = Version{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_api_server_operator_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Version) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Version) ProtoMessage() {} + +func (x *Version) ProtoReflect() protoreflect.Message { + mi := &file_pkg_api_server_operator_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Version.ProtoReflect.Descriptor instead. +func (*Version) Descriptor() ([]byte, []int) { + return file_pkg_api_server_operator_proto_rawDescGZIP(), []int{1} +} + +func (x *Version) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Version) GetBuild() string { + if x != nil { + return x.Build + } + return "" +} + +func (x *Version) GetEdition() string { + if x != nil { + return x.Edition + } + return "" +} + +func (x *Version) GetGoVersion() string { + if x != nil { + return x.GoVersion + } + return "" +} + +func (x *Version) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + +var File_pkg_api_server_operator_proto protoreflect.FileDescriptor + +var file_pkg_api_server_operator_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x91, 0x01, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x65, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x6f, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x6f, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x44, 0x61, 0x74, 0x65, 0x32, 0x3a, 0x0a, 0x08, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, + 0x12, 0x2e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0d, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x00, + 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, + 0x72, 0x61, 0x6e, 0x67, 0x6f, 0x64, 0x62, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x2d, 0x61, 0x72, 0x61, + 0x6e, 0x67, 0x6f, 0x64, 0x62, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_api_server_operator_proto_rawDescOnce sync.Once + file_pkg_api_server_operator_proto_rawDescData = file_pkg_api_server_operator_proto_rawDesc +) + +func file_pkg_api_server_operator_proto_rawDescGZIP() []byte { + file_pkg_api_server_operator_proto_rawDescOnce.Do(func() { + file_pkg_api_server_operator_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_api_server_operator_proto_rawDescData) + }) + return file_pkg_api_server_operator_proto_rawDescData +} + +var file_pkg_api_server_operator_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_api_server_operator_proto_goTypes = []interface{}{ + (*Empty)(nil), // 0: server.Empty + (*Version)(nil), // 1: server.Version +} +var file_pkg_api_server_operator_proto_depIdxs = []int32{ + 0, // 0: server.Operator.GetVersion:input_type -> server.Empty + 1, // 1: server.Operator.GetVersion:output_type -> server.Version + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_pkg_api_server_operator_proto_init() } +func file_pkg_api_server_operator_proto_init() { + if File_pkg_api_server_operator_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_api_server_operator_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Empty); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_api_server_operator_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Version); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_api_server_operator_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pkg_api_server_operator_proto_goTypes, + DependencyIndexes: file_pkg_api_server_operator_proto_depIdxs, + MessageInfos: file_pkg_api_server_operator_proto_msgTypes, + }.Build() + File_pkg_api_server_operator_proto = out.File + file_pkg_api_server_operator_proto_rawDesc = nil + file_pkg_api_server_operator_proto_goTypes = nil + file_pkg_api_server_operator_proto_depIdxs = nil +} diff --git a/pkg/api/server/operator.proto b/pkg/api/server/operator.proto new file mode 100644 index 000000000..27ef48a45 --- /dev/null +++ b/pkg/api/server/operator.proto @@ -0,0 +1,39 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +syntax = "proto3"; + +option go_package = "github.com/arangodb/kube-arangodb/pkg/api/server"; + +package server; + +service Operator { + rpc GetVersion (Empty) returns (Version) {} +} + +message Empty {} + +message Version { + string version = 1; + string build = 2; + string edition = 3; + string go_version = 4; + string build_date = 5; +} diff --git a/pkg/api/server/operator_grpc.pb.go b/pkg/api/server/operator_grpc.pb.go new file mode 100644 index 000000000..69bcd2d0b --- /dev/null +++ b/pkg/api/server/operator_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.21.1 +// source: pkg/api/server/operator.proto + +package server + +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 + +// OperatorClient is the client API for Operator 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 OperatorClient interface { + GetVersion(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Version, error) +} + +type operatorClient struct { + cc grpc.ClientConnInterface +} + +func NewOperatorClient(cc grpc.ClientConnInterface) OperatorClient { + return &operatorClient{cc} +} + +func (c *operatorClient) GetVersion(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Version, error) { + out := new(Version) + err := c.cc.Invoke(ctx, "/server.Operator/GetVersion", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// OperatorServer is the server API for Operator service. +// All implementations must embed UnimplementedOperatorServer +// for forward compatibility +type OperatorServer interface { + GetVersion(context.Context, *Empty) (*Version, error) + mustEmbedUnimplementedOperatorServer() +} + +// UnimplementedOperatorServer must be embedded to have forward compatible implementations. +type UnimplementedOperatorServer struct { +} + +func (UnimplementedOperatorServer) GetVersion(context.Context, *Empty) (*Version, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented") +} +func (UnimplementedOperatorServer) mustEmbedUnimplementedOperatorServer() {} + +// UnsafeOperatorServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to OperatorServer will +// result in compilation errors. +type UnsafeOperatorServer interface { + mustEmbedUnimplementedOperatorServer() +} + +func RegisterOperatorServer(s grpc.ServiceRegistrar, srv OperatorServer) { + s.RegisterService(&Operator_ServiceDesc, srv) +} + +func _Operator_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OperatorServer).GetVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/server.Operator/GetVersion", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OperatorServer).GetVersion(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// Operator_ServiceDesc is the grpc.ServiceDesc for Operator service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Operator_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "server.Operator", + HandlerType: (*OperatorServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetVersion", + Handler: _Operator_GetVersion_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "pkg/api/server/operator.proto", +} diff --git a/pkg/api/tls.go b/pkg/api/tls.go new file mode 100644 index 000000000..3eca61f65 --- /dev/null +++ b/pkg/api/tls.go @@ -0,0 +1,91 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package api + +import ( + "context" + "crypto/tls" + "time" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + typedCore "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/arangodb-helper/go-certificates" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +func prepareTLSConfig(cli typedCore.CoreV1Interface, cfg ServerConfig) (*tls.Config, error) { + cert, key, err := loadOrSelfSignCertificate(cli, cfg) + if err != nil { + return nil, errors.WithStack(err) + } + tlsConfig, err := createTLSConfig(cert, key) + if err != nil { + return nil, errors.WithStack(err) + } + tlsConfig.BuildNameToCertificate() + return tlsConfig, nil +} + +// loadOrSelfSignCertificate loads TLS certificate from secret or creates a new one +func loadOrSelfSignCertificate(cli typedCore.CoreV1Interface, cfg ServerConfig) (string, string, error) { + if cfg.TLSSecretName != "" { + // Load TLS certificate from secret + s, err := cli.Secrets(cfg.Namespace).Get(context.Background(), cfg.TLSSecretName, meta.GetOptions{}) + if err != nil { + return "", "", err + } + certBytes, found := s.Data[core.TLSCertKey] + if !found { + return "", "", errors.Newf("No %s found in secret %s", core.TLSCertKey, cfg.TLSSecretName) + } + keyBytes, found := s.Data[core.TLSPrivateKeyKey] + if !found { + return "", "", errors.Newf("No %s found in secret %s", core.TLSPrivateKeyKey, cfg.TLSSecretName) + } + return string(certBytes), string(keyBytes), nil + } + // Secret not specified, create our own TLS certificate + options := certificates.CreateCertificateOptions{ + CommonName: cfg.ServerName, + Hosts: append([]string{cfg.ServerName}, cfg.ServerAltNames...), + ValidFrom: time.Now(), + ValidFor: time.Hour * 24 * 365 * 10, + IsCA: false, + ECDSACurve: "P256", + } + return certificates.CreateCertificate(options, nil) +} + +// createTLSConfig creates a TLS config based on given config +func createTLSConfig(cert, key string) (*tls.Config, error) { + var result *tls.Config + c, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return nil, errors.WithStack(err) + } + result = &tls.Config{ + Certificates: []tls.Certificate{c}, + } + return result, nil +} diff --git a/pkg/generated/timezones/timezones.go b/pkg/generated/timezones/timezones.go index 0a49f70e9..d6d2955bc 100644 --- a/pkg/generated/timezones/timezones.go +++ b/pkg/generated/timezones/timezones.go @@ -34,7 +34,7 @@ type Timezone struct { } func (t Timezone) GetData() ([]byte, bool) { - if d, ok := timezonesData[t.Parent]; ok { + if d, ok := timezonesData[t.Name]; ok { if d, err := base64.StdEncoding.DecodeString(d); err == nil { return d, true }