1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-14 11:57:37 +00:00

[Feature] [Gateway] SNI and Authz support (#1714)

This commit is contained in:
Adam Janikowski 2024-09-02 15:37:36 +02:00 committed by GitHub
parent efbbc79439
commit db5406e868
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 623 additions and 280 deletions

View file

@ -21,6 +21,7 @@
- (Bugfix) Fix race condition in ArangoBackup - (Bugfix) Fix race condition in ArangoBackup
- (Feature) Improve Gateway Config gen - (Feature) Improve Gateway Config gen
- (Feature) Integration Service TLS - (Feature) Integration Service TLS
- (Feature) (Gateway) SNI and Authz support
## [1.2.42](https://github.com/arangodb/kube-arangodb/tree/1.2.42) (2024-07-23) ## [1.2.42](https://github.com/arangodb/kube-arangodb/tree/1.2.42) (2024-07-23)
- (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries - (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries

View file

@ -7,7 +7,9 @@ FROM ${ENVOY_IMAGE} AS envoy
FROM ${IMAGE} AS base FROM ${IMAGE} AS base
RUN apt-get update && apt-get upgrade -y && apt-get clean ARG BUILD_SKIP_UPDATE=false
ENV BUILD_SKIP_UPDATE=${BUILD_SKIP_UPDATE}
RUN if [ X"${BUILD_SKIP_UPDATE}" = X"true" ]; then echo "Update skipped!"; else apt-get update && apt-get upgrade -y && apt-get clean; fi
FROM base FROM base

View file

@ -121,6 +121,8 @@ ifndef LOCALONLY
PUSHIMAGES := 1 PUSHIMAGES := 1
endif endif
BUILD_SKIP_UPDATE ?= false
ifdef IMAGETAG ifdef IMAGETAG
IMAGESUFFIX := :$(IMAGETAG) IMAGESUFFIX := :$(IMAGETAG)
else else
@ -272,7 +274,7 @@ NON_EE_SOURCES := $(shell $(NON_EE_SOURCES_QUERY))
YAML_EXCLUDE_DIRS := vendor .gobuild deps tools pkg/generated/clientset pkg/generated/informers pkg/generated/listers \ YAML_EXCLUDE_DIRS := vendor .gobuild deps tools pkg/generated/clientset pkg/generated/informers pkg/generated/listers \
chart/kube-arangodb/templates chart/kube-arangodb-arm64/templates chart/kube-arangodb-enterprise/templates chart/kube-arangodb-enterprise-arm64/templates \ chart/kube-arangodb/templates chart/kube-arangodb-arm64/templates chart/kube-arangodb-enterprise/templates chart/kube-arangodb-enterprise-arm64/templates \
chart/kube-arangodb-crd/templates chart/arangodb-ingress-proxy/templates chart/kube-arangodb-crd/templates
YAML_EXCLUDE_FILES := YAML_EXCLUDE_FILES :=
YAML_QUERY := find ./ -type f -name '*.yaml' $(foreach EXCLUDE_DIR,$(YAML_EXCLUDE_DIRS), ! -path "*/$(EXCLUDE_DIR)/*") $(foreach EXCLUDE_FILE,$(YAML_EXCLUDE_FILES), ! -path "*/$(EXCLUDE_FILE)") YAML_QUERY := find ./ -type f -name '*.yaml' $(foreach EXCLUDE_DIR,$(YAML_EXCLUDE_DIRS), ! -path "*/$(EXCLUDE_DIR)/*") $(foreach EXCLUDE_FILE,$(YAML_EXCLUDE_FILES), ! -path "*/$(EXCLUDE_FILE)")
YAMLS := $(shell $(YAML_QUERY)) YAMLS := $(shell $(YAML_QUERY))
@ -478,11 +480,11 @@ $(BIN): $(VBIN_LINUX_AMD64) $(VBIN_OPS_LINUX_AMD64) $(VBIN_INT_LINUX_AMD64)
docker: clean check-vars $(VBIN_LINUX_AMD64) $(VBIN_LINUX_ARM64) docker: clean check-vars $(VBIN_LINUX_AMD64) $(VBIN_LINUX_ARM64)
ifdef PUSHIMAGES ifdef PUSHIMAGES
docker buildx build --no-cache -f $(DOCKERFILE) --build-arg GOVERSION=$(GOVERSION) --build-arg DISTRIBUTION=$(DISTRIBUTION) \ docker buildx build --no-cache -f $(DOCKERFILE) --build-arg GOVERSION=$(GOVERSION) --build-arg DISTRIBUTION=$(DISTRIBUTION) \
--build-arg "VERSION=${VERSION_MAJOR_MINOR_PATCH}" --build-arg "RELEASE_MODE=$(RELEASE_MODE)" \ --build-arg "VERSION=${VERSION_MAJOR_MINOR_PATCH}" --build-arg "RELEASE_MODE=$(RELEASE_MODE)" --build-arg "BUILD_SKIP_UPDATE=${BUILD_SKIP_UPDATE}" \
--platform linux/amd64,linux/arm64 --push -t $(OPERATORIMAGE) . --platform linux/amd64,linux/arm64 --push -t $(OPERATORIMAGE) .
else else
docker buildx build --no-cache -f $(DOCKERFILE) --build-arg GOVERSION=$(GOVERSION) --build-arg DISTRIBUTION=$(DISTRIBUTION) \ docker buildx build --no-cache -f $(DOCKERFILE) --build-arg GOVERSION=$(GOVERSION) --build-arg DISTRIBUTION=$(DISTRIBUTION) \
--build-arg "VERSION=${VERSION_MAJOR_MINOR_PATCH}" --build-arg "RELEASE_MODE=$(RELEASE_MODE)" \ --build-arg "VERSION=${VERSION_MAJOR_MINOR_PATCH}" --build-arg "RELEASE_MODE=$(RELEASE_MODE)" --build-arg "BUILD_SKIP_UPDATE=${BUILD_SKIP_UPDATE}" \
--platform linux/amd64,linux/arm64 -t $(OPERATORIMAGE) . --platform linux/amd64,linux/arm64 -t $(OPERATORIMAGE) .
endif endif
@ -802,6 +804,7 @@ set-typed-api-version/%:
@grep -rHn "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/$*/v[A-Za-z0-9]\+" \ @grep -rHn "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/$*/v[A-Za-z0-9]\+" \
"$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/deployment/" \
"$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/replication/" \
"$(ROOT)/pkg/integrations/" \
"$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operator/" \
"$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/operatorV2/" \
"$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/server/" \
@ -818,6 +821,7 @@ set-api-version/%:
@grep -rHn "github.com/arangodb/kube-arangodb/pkg/apis/$*/v[A-Za-z0-9]\+" \ @grep -rHn "github.com/arangodb/kube-arangodb/pkg/apis/$*/v[A-Za-z0-9]\+" \
"$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/deployment/" \
"$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/replication/" \
"$(ROOT)/pkg/integrations/" \
"$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operator/" \
"$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/operatorV2/" \
"$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/server/" \
@ -831,6 +835,7 @@ set-api-version/%:
@grep -rHn "DatabaseV[A-Za-z0-9]\+()" \ @grep -rHn "DatabaseV[A-Za-z0-9]\+()" \
"$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/deployment/" \
"$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/replication/" \
"$(ROOT)/pkg/integrations/" \
"$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operator/" \
"$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/operatorV2/" \
"$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/server/" \
@ -844,6 +849,7 @@ set-api-version/%:
@grep -rHn "ReplicationV[A-Za-z0-9]\+()" \ @grep -rHn "ReplicationV[A-Za-z0-9]\+()" \
"$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/deployment/" \
"$(ROOT)/pkg/replication/" \ "$(ROOT)/pkg/replication/" \
"$(ROOT)/pkg/integrations/" \
"$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operator/" \
"$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/operatorV2/" \
"$(ROOT)/pkg/server/" \ "$(ROOT)/pkg/server/" \

View file

@ -182,7 +182,7 @@ Flags:
--kubernetes.max-batch-size int Size of batch during objects read (default 256) --kubernetes.max-batch-size int Size of batch during objects read (default 256)
--kubernetes.qps float32 Number of queries per second for k8s API (default 15) --kubernetes.qps float32 Number of queries per second for k8s API (default 15)
--log.format string Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used (default "pretty") --log.format string Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used (default "pretty")
--log.level stringArray Set log levels in format <level> or <logger>=<level>. Possible loggers: action, agency, api-server, assertion, backup-operator, chaos-monkey, crd, deployment, deployment-ci, deployment-reconcile, deployment-replication, deployment-resilience, deployment-resources, deployment-storage, deployment-storage-pc, deployment-storage-service, http, inspector, integration-config-v1, integrations, k8s-client, kubernetes-informer, monitor, networking-route-operator, operator, operator-arangojob-handler, operator-v2, operator-v2-event, operator-v2-worker, panics, pod_compare, root, root-event-recorder, server, server-authentication (default [info]) --log.level stringArray Set log levels in format <level> or <logger>=<level>. Possible loggers: action, agency, api-server, assertion, backup-operator, chaos-monkey, crd, deployment, deployment-ci, deployment-reconcile, deployment-replication, deployment-resilience, deployment-resources, deployment-storage, deployment-storage-pc, deployment-storage-service, http, inspector, integration-config-v1, integration-envoy-auth-v3, integrations, k8s-client, kubernetes-informer, monitor, networking-route-operator, operator, operator-arangojob-handler, operator-v2, operator-v2-event, operator-v2-worker, panics, pod_compare, root, root-event-recorder, server, server-authentication (default [info])
--log.sampling If true, operator will try to minimize duplication of logging events (default true) --log.sampling If true, operator will try to minimize duplication of logging events (default true)
--memory-limit uint Define memory limit for hard shutdown and the dump of goroutines. Used for testing --memory-limit uint Define memory limit for hard shutdown and the dump of goroutines. Used for testing
--metrics.excluded-prefixes stringArray List of the excluded metrics prefixes --metrics.excluded-prefixes stringArray List of the excluded metrics prefixes

View file

@ -1,4 +0,0 @@
description: ArangoDB Ingress Proxy
name: arangodb-ingress-proxy
tillerVersion: '>2.7'
version: 1.0.0

View file

@ -1,15 +0,0 @@
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

View file

@ -1,20 +0,0 @@
# Introduction
Kubernetes ArangoDB Ingress for custom certificates.
ArangoDB supports more than only HTTP protocol, so simple Ingress is not enough.
## Before
Before Ingress proxy will be installed certificate secret needs to be created:
```
kubectl -n <deployment namespace> create secret tls <secret name> --cert <path to cert> --key <path to key>
```
## Installation
To install Ingress:
```
helm install --name <my ingress name> --namespace <deployment namespace> <path to kube-arangodb repository>/chart/arangodb-ingress-proxy --set replicas=2 --set tls=TLS Secret name> --set deployment=<ArangoDeployment name>
```

View file

@ -1,3 +0,0 @@
Your LB is ready!
Get LoadBalancer IP using `kubectl --namespace "{{ .Release.Namespace }}" get svc "{{ template "arangodb-ingress-proxy.name" . }}" -o jsonpath="{.status.loadBalancer.ingress[0].ip}"`

View file

@ -1,15 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "arangodb-ingress-proxy.name" -}}
{{- printf "%s" .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Expand the name of the release.
*/}}
{{- define "arangodb-ingress-proxy.releaseName" -}}
{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

View file

@ -1,46 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "arangodb-ingress-proxy.name" . }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
data:
config: |
user nginx;
worker_processes 1;
error_log /dev/stdout info;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
stream {
log_format basic '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
access_log /dev/stdout basic;
server {
listen 8529 ssl;
proxy_pass {{ required "Arango Deployment name needs to be provided!" .Values.deployment }}:8529;
proxy_ssl on;
ssl_certificate /etc/nginx/local-tls/tls.crt;
ssl_certificate_key /etc/nginx/local-tls/tls.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_timeout 4h;
ssl_handshake_timeout 30s;
proxy_timeout 6h;
}
}

View file

@ -1,68 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "arangodb-ingress-proxy.name" . }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- amd64
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: "kubernetes.io/hostname"
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- {{ template "arangodb-ingress-proxy.name" . }}
containers:
- name: nginx
imagePullPolicy: {{ .Values.imagePullPolicy }}
image: {{ .Values.image }}
ports:
- name: nginx
containerPort: 8529
volumeMounts:
- mountPath: /etc/nginx/nginx.conf
name: config
subPath: config
- mountPath: /etc/nginx/local-tls
name: tls
volumes:
- name: config
configMap:
name: {{ template "arangodb-ingress-proxy.name" . }}
- name: tls
secret:
secretName: {{ required "TLS certificate need to be provided!" .Values.tls }}

View file

@ -1,23 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "arangodb-ingress-proxy.name" . }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
spec:
ports:
- name: server
port: 8529
protocol: TCP
targetPort: 8529
selector:
app.kubernetes.io/name: {{ template "arangodb-ingress-proxy.name" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
release: {{ .Release.Name }}
type: LoadBalancer

View file

@ -1,3 +0,0 @@
replicas: 2
imagePullPolicy: Always
image: nginx:1.16.1-alpine

View file

@ -80,7 +80,7 @@ Flags:
--kubernetes.max-batch-size int Size of batch during objects read (default 256) --kubernetes.max-batch-size int Size of batch during objects read (default 256)
--kubernetes.qps float32 Number of queries per second for k8s API (default 15) --kubernetes.qps float32 Number of queries per second for k8s API (default 15)
--log.format string Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used (default "pretty") --log.format string Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used (default "pretty")
--log.level stringArray Set log levels in format <level> or <logger>=<level>. Possible loggers: action, agency, api-server, assertion, backup-operator, chaos-monkey, crd, deployment, deployment-ci, deployment-reconcile, deployment-replication, deployment-resilience, deployment-resources, deployment-storage, deployment-storage-pc, deployment-storage-service, http, inspector, integration-config-v1, integrations, k8s-client, kubernetes-informer, monitor, networking-route-operator, operator, operator-arangojob-handler, operator-v2, operator-v2-event, operator-v2-worker, panics, pod_compare, root, root-event-recorder, server, server-authentication (default [info]) --log.level stringArray Set log levels in format <level> or <logger>=<level>. Possible loggers: action, agency, api-server, assertion, backup-operator, chaos-monkey, crd, deployment, deployment-ci, deployment-reconcile, deployment-replication, deployment-resilience, deployment-resources, deployment-storage, deployment-storage-pc, deployment-storage-service, http, inspector, integration-config-v1, integration-envoy-auth-v3, integrations, k8s-client, kubernetes-informer, monitor, networking-route-operator, operator, operator-arangojob-handler, operator-v2, operator-v2-event, operator-v2-worker, panics, pod_compare, root, root-event-recorder, server, server-authentication (default [info])
--log.sampling If true, operator will try to minimize duplication of logging events (default true) --log.sampling If true, operator will try to minimize duplication of logging events (default true)
--memory-limit uint Define memory limit for hard shutdown and the dump of goroutines. Used for testing --memory-limit uint Define memory limit for hard shutdown and the dump of goroutines. Used for testing
--metrics.excluded-prefixes stringArray List of the excluded metrics prefixes --metrics.excluded-prefixes stringArray List of the excluded metrics prefixes

View file

@ -53,5 +53,10 @@ func (i *impl) Register(registrar *grpc.Server) {
} }
func (i *impl) Check(ctx context.Context, request *pbEnvoyAuthV3.CheckRequest) (*pbEnvoyAuthV3.CheckResponse, error) { func (i *impl) Check(ctx context.Context, request *pbEnvoyAuthV3.CheckRequest) (*pbEnvoyAuthV3.CheckResponse, error) {
return &pbEnvoyAuthV3.CheckResponse{}, nil logger.Info("Request Received")
return &pbEnvoyAuthV3.CheckResponse{
HttpResponse: &pbEnvoyAuthV3.CheckResponse_OkResponse{
OkResponse: &pbEnvoyAuthV3.OkHttpResponse{},
},
}, nil
} }

View file

@ -0,0 +1,25 @@
//
// DISCLAIMER
//
// Copyright 2024 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 v3
import "github.com/arangodb/kube-arangodb/pkg/logging"
var logger = logging.Global().RegisterAndGetLogger("integration-envoy-auth-v3", logging.Info)

View file

@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/arangodb/kube-arangodb/pkg/util/svc" "github.com/arangodb/kube-arangodb/pkg/util/svc"
"github.com/arangodb/kube-arangodb/pkg/util/tests"
"github.com/arangodb/kube-arangodb/pkg/util/tests/tgrpc" "github.com/arangodb/kube-arangodb/pkg/util/tests/tgrpc"
) )
@ -53,6 +54,7 @@ func Test_AllowAll(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, resp.Validate()) require.NoError(t, resp.Validate())
require.Nil(t, resp.Status) require.Nil(t, resp.Status)
require.Nil(t, resp.HttpResponse) require.NotNil(t, resp.HttpResponse)
require.NotNil(t, tests.CastAs[*pbEnvoyAuthV3.CheckResponse_OkResponse](t, resp.GetHttpResponse()).OkResponse)
require.Nil(t, resp.DynamicMetadata) require.Nil(t, resp.DynamicMetadata)
} }

View file

@ -180,19 +180,23 @@ func ValidateRequiredInterfacePath[T ValidateInterface](path string, in T) error
} }
// ValidateList validates all elements on the list // ValidateList validates all elements on the list
func ValidateList[T any](in []T, validator func(T) error) error { func ValidateList[T any](in []T, validator func(T) error, checks ...func(in []T) error) error {
errors := make([]error, len(in)) errors := make([]error, len(in)+len(checks))
for id := range in { for id := range in {
errors[id] = PrefixResourceError(fmt.Sprintf("[%d]", id), validator(in[id])) errors[id] = PrefixResourceError(fmt.Sprintf("[%d]", id), validator(in[id]))
} }
for id, c := range checks {
errors[len(in)+id] = c(in)
}
return WithErrors(errors...) return WithErrors(errors...)
} }
// ValidateMap validates all elements on the list // ValidateMap validates all elements on the list
func ValidateMap[T any](in map[string]T, validator func(string, T) error) error { func ValidateMap[T any](in map[string]T, validator func(string, T) error, checks ...func(in map[string]T) error) error {
errors := make([]error, 0, len(in)) errors := make([]error, 0, len(in)+len(checks))
for id := range in { for id := range in {
if err := PrefixResourceError(fmt.Sprintf("`%s`", id), validator(id, in[id])); err != nil { if err := PrefixResourceError(fmt.Sprintf("`%s`", id), validator(id, in[id])); err != nil {
@ -200,6 +204,10 @@ func ValidateMap[T any](in map[string]T, validator func(string, T) error) error
} }
} }
for id, c := range checks {
errors[len(in)+id] = c(in)
}
return WithErrors(errors...) return WithErrors(errors...)
} }

View file

@ -20,6 +20,8 @@
package features package features
import api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
func init() { func init() {
registerFeature(gateway) registerFeature(gateway)
} }
@ -35,3 +37,7 @@ var gateway = &feature{
func Gateway() Feature { func Gateway() Feature {
return gateway return gateway
} }
func IsGatewayEnabled(spec api.DeploymentSpec) bool {
return Gateway().Enabled() && spec.IsGatewayEnabled()
}

View file

@ -0,0 +1,117 @@
//
// DISCLAIMER
//
// Copyright 2016-2024 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 pod
import (
"fmt"
core "k8s.io/api/core/v1"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
"github.com/arangodb/kube-arangodb/pkg/deployment/features"
"github.com/arangodb/kube-arangodb/pkg/util"
"github.com/arangodb/kube-arangodb/pkg/util/constants"
"github.com/arangodb/kube-arangodb/pkg/util/errors"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces"
)
func SNIGateway() Builder {
return sniGateway{}
}
type sniGateway struct{}
func (s sniGateway) Envs(i Input) []core.EnvVar {
return nil
}
func (s sniGateway) isSupported(i Input) bool {
if !i.Deployment.TLS.IsSecure() {
return false
}
if !features.Gateway().Supported(i.Version, i.Enterprise) {
// We need 3.7.0+ and Enterprise to support this
return false
}
return GroupSNISupported(i.Deployment, i.Group)
}
func (s sniGateway) Verify(i Input, cachedStatus interfaces.Inspector) error {
if !s.isSupported(i) {
return nil
}
for _, secret := range util.SortKeys(i.Deployment.TLS.GetSNI().Mapping) {
kubeSecret, exists := cachedStatus.Secret().V1().GetSimple(secret)
if !exists {
return errors.Errorf("SNI Secret not found %s", secret)
}
_, ok := kubeSecret.Data[constants.SecretTLSKeyfile]
if !ok {
return errors.Errorf("Unable to find secret key %s/%s for SNI", secret, constants.SecretTLSKeyfile)
}
}
return nil
}
func (s sniGateway) Volumes(i Input) ([]core.Volume, []core.VolumeMount) {
if !s.isSupported(i) {
return nil, nil
}
sni := i.Deployment.TLS.GetSNI()
volumes := make([]core.Volume, 0, len(sni.Mapping))
volumeMounts := make([]core.VolumeMount, 0, len(sni.Mapping))
for _, secret := range util.SortKeys(sni.Mapping) {
secretNameSha := util.SHA256FromString(secret)
secretNameSha = fmt.Sprintf("sni-%s", secretNameSha[:48])
vol := core.Volume{
Name: secretNameSha,
VolumeSource: core.VolumeSource{
Secret: &core.SecretVolumeSource{
SecretName: secret,
},
},
}
volMount := core.VolumeMount{
Name: secretNameSha,
MountPath: fmt.Sprintf("%s/%s", shared.TLSSNIKeyfileVolumeMountDir, secret),
ReadOnly: true,
}
volumes = append(volumes, vol)
volumeMounts = append(volumeMounts, volMount)
}
return volumes, volumeMounts
}
func (s sniGateway) Args(i Input) k8sutil.OptionPairs {
return nil
}

View file

@ -21,7 +21,6 @@
package pod package pod
import ( import (
"crypto/sha256"
"fmt" "fmt"
core "k8s.io/api/core/v1" core "k8s.io/api/core/v1"
@ -36,12 +35,18 @@ import (
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces"
) )
func GroupSNISupported(mode api.DeploymentMode, group api.ServerGroup) bool { func GroupSNISupported(spec api.DeploymentSpec, group api.ServerGroup) bool {
switch mode { switch spec.Mode.Get() {
case api.DeploymentModeCluster: case api.DeploymentModeCluster:
if features.IsGatewayEnabled(spec) {
return group == api.ServerGroupGateways
}
return group == api.ServerGroupCoordinators return group == api.ServerGroupCoordinators
case api.DeploymentModeSingle: case api.DeploymentModeSingle:
if features.IsGatewayEnabled(spec) {
return group == api.ServerGroupGateways
}
fallthrough fallthrough
case api.DeploymentModeActiveFailover: case api.DeploymentModeActiveFailover:
return group == api.ServerGroupSingle return group == api.ServerGroupSingle
@ -70,7 +75,7 @@ func (s sni) isSupported(i Input) bool {
return false return false
} }
return GroupSNISupported(i.Deployment.Mode.Get(), i.Group) return GroupSNISupported(i.Deployment, i.Group)
} }
func (s sni) Verify(i Input, cachedStatus interfaces.Inspector) error { func (s sni) Verify(i Input, cachedStatus interfaces.Inspector) error {
@ -102,7 +107,7 @@ func (s sni) Volumes(i Input) ([]core.Volume, []core.VolumeMount) {
volumeMounts := make([]core.VolumeMount, 0, len(sni.Mapping)) volumeMounts := make([]core.VolumeMount, 0, len(sni.Mapping))
for _, secret := range util.SortKeys(sni.Mapping) { for _, secret := range util.SortKeys(sni.Mapping) {
secretNameSha := fmt.Sprintf("%0x", sha256.Sum256([]byte(secret))) secretNameSha := util.SHA256FromString(secret)
secretNameSha = fmt.Sprintf("sni-%s", secretNameSha[:48]) secretNameSha = fmt.Sprintf("sni-%s", secretNameSha[:48])

View file

@ -45,7 +45,7 @@ func (r *Reconciler) createRebalancerV2GeneratePlan(spec api.DeploymentSpec, sta
r.metrics.Rebalancer.SetEnabled(true) r.metrics.Rebalancer.SetEnabled(true)
if !status.Members.AllMembersReady(spec.Mode.Get(), spec.Sync.IsEnabled(), features.Gateway().Enabled() && spec.IsGatewayEnabled()) { if !status.Members.AllMembersReady(spec.Mode.Get(), spec.Sync.IsEnabled(), features.IsGatewayEnabled(spec)) {
return nil return nil
} }

View file

@ -70,7 +70,7 @@ func (r *Reconciler) createScaleMemberPlan(ctx context.Context, apiObject k8suti
plan = append(plan, r.createScalePlan(status, status.Members.SyncWorkers, api.ServerGroupSyncWorkers, 0, context)...) plan = append(plan, r.createScalePlan(status, status.Members.SyncWorkers, api.ServerGroupSyncWorkers, 0, context)...)
} }
} }
if features.Gateway().Enabled() && spec.IsGatewayEnabled() { if features.IsGatewayEnabled(spec) {
plan = append(plan, r.createScalePlan(status, status.Members.Gateways, api.ServerGroupGateways, spec.Gateways.GetCount(), context)...) plan = append(plan, r.createScalePlan(status, status.Members.Gateways, api.ServerGroupGateways, spec.Gateways.GetCount(), context)...)
} else { } else {
plan = append(plan, r.createScalePlan(status, status.Members.Gateways, api.ServerGroupGateways, 0, context)...) plan = append(plan, r.createScalePlan(status, status.Members.Gateways, api.ServerGroupGateways, 0, context)...)

View file

@ -1,7 +1,7 @@
// //
// DISCLAIMER // DISCLAIMER
// //
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany // Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -57,7 +57,11 @@ func (r *Reconciler) createRotateTLSServerSNIPlan(ctx context.Context, apiObject
var plan api.Plan var plan api.Plan
for _, group := range api.AllServerGroups { for _, group := range api.AllServerGroups {
if !pod.GroupSNISupported(spec.Mode.Get(), group) { if group == api.ServerGroupGateways {
// Gateways are managed differently
continue
}
if !pod.GroupSNISupported(spec, group) {
continue continue
} }
for _, m := range status.Members.MembersOfGroup(group) { for _, m := range status.Members.MembersOfGroup(group) {

View file

@ -23,6 +23,7 @@ package resources
import ( import (
"context" "context"
"fmt" "fmt"
"path"
"path/filepath" "path/filepath"
core "k8s.io/api/core/v1" core "k8s.io/api/core/v1"
@ -66,6 +67,7 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec
}, },
Data: map[string]string{ Data: map[string]string{
GatewayConfigFileName: string(gatewayCfgYaml), GatewayConfigFileName: string(gatewayCfgYaml),
GatewayConfigChecksumFileName: gatewayCfgChecksum,
}, },
} }
@ -85,11 +87,12 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec
return errors.Reconcile() return errors.Reconcile()
} else { } else {
// CM Exists, checks checksum - if key is not in the map we return empty string // CM Exists, checks checksum - if key is not in the map we return empty string
if existingSha := util.SHA256FromString(cm.Data[GatewayConfigFileName]); existingSha != gatewayCfgChecksum { if existingSha, existingChecksumSha := util.SHA256FromString(cm.Data[GatewayConfigFileName]), cm.Data[GatewayConfigChecksumFileName]; existingSha != gatewayCfgChecksum || existingChecksumSha != gatewayCfgChecksum {
// We need to do the update // We need to do the update
if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{},
patcher.PatchConfigMapData(map[string]string{ patcher.PatchConfigMapData(map[string]string{
GatewayConfigFileName: string(gatewayCfgYaml), GatewayConfigFileName: string(gatewayCfgYaml),
GatewayConfigChecksumFileName: gatewayCfgChecksum,
})); err != nil { })); err != nil {
log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap") log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap")
return errors.WithStack(err) return errors.WithStack(err)
@ -116,6 +119,11 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto
var cfg gateway.Config var cfg gateway.Config
cfg.IntegrationSidecar = &gateway.ConfigDestinationTarget{
Host: "127.0.0.1",
Port: int32(r.context.GetSpec().Gateway.GetSidecar().GetListenPort()),
}
cfg.DefaultDestination = gateway.ConfigDestination{ cfg.DefaultDestination = gateway.ConfigDestination{
Targets: []gateway.ConfigDestinationTarget{ Targets: []gateway.ConfigDestinationTarget{
{ {
@ -133,6 +141,25 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto
PrivateKeyPath: keyPath, PrivateKeyPath: keyPath,
} }
cfg.DefaultDestination.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS) cfg.DefaultDestination.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS)
// Check SNI
if sni := spec.TLS.GetSNI().Mapping; len(sni) > 0 {
for _, volume := range util.SortKeys(sni) {
servers, ok := sni[volume]
if !ok {
continue
}
var s gateway.ConfigSNI
f := path.Join(shared.TLSSNIKeyfileVolumeMountDir, volume, constants.SecretTLSKeyfile)
s.ConfigTLS = gateway.ConfigTLS{
CertificatePath: f,
PrivateKeyPath: f,
}
s.ServerNames = servers
cfg.SNI = append(cfg.SNI, s)
}
}
} }
// Check ArangoRoutes // Check ArangoRoutes

View file

@ -49,7 +49,7 @@ func (r *Resources) EnsureConfigMaps(ctx context.Context, cachedStatus inspector
reconcileRequired := k8sutil.NewReconcile(cachedStatus) reconcileRequired := k8sutil.NewReconcile(cachedStatus)
if features.Gateway().Enabled() && spec.IsGatewayEnabled() { if features.IsGatewayEnabled(spec) {
counterMetric.Inc() counterMetric.Inc()
if err := reconcileRequired.WithError(r.ensureGatewayConfig(ctx, cachedStatus, configMaps)); err != nil { if err := reconcileRequired.WithError(r.ensureGatewayConfig(ctx, cachedStatus, configMaps)); err != nil {
return errors.Section(err, "Gateway ConfigMap") return errors.Section(err, "Gateway ConfigMap")

View file

@ -0,0 +1,25 @@
//
// DISCLAIMER
//
// Copyright 2024 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 gateway
const (
IntegrationSidecarFilterName = "envoy.filters.http.ext_authz"
)

View file

@ -23,16 +23,23 @@ package gateway
import ( import (
"fmt" "fmt"
"sort" "sort"
"time"
bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
listenerAPI "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" listenerAPI "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
httpFilterAuthzApi "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
routerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" routerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
tlsInspectorApi "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
upstreamHttpApi "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
"github.com/golang/protobuf/ptypes/any"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
@ -46,12 +53,18 @@ type Config struct {
Destinations ConfigDestinations `json:"destinations,omitempty"` Destinations ConfigDestinations `json:"destinations,omitempty"`
DefaultTLS *ConfigTLS `json:"defaultTLS,omitempty"` DefaultTLS *ConfigTLS `json:"defaultTLS,omitempty"`
IntegrationSidecar *ConfigDestinationTarget `json:"integrationSidecar,omitempty"`
SNI ConfigSNIList `json:"sni,omitempty"`
} }
func (c Config) Validate() error { func (c Config) Validate() error {
return errors.Errors( return errors.Errors(
shared.PrefixResourceErrors("defaultDestination", c.DefaultDestination.Validate()), shared.PrefixResourceErrors("defaultDestination", c.DefaultDestination.Validate()),
shared.PrefixResourceErrors("integrationSidecar", c.IntegrationSidecar.Validate()),
shared.PrefixResourceErrors("destinations", c.Destinations.Validate()), shared.PrefixResourceErrors("destinations", c.Destinations.Validate()),
shared.PrefixResourceErrors("sni", c.SNI.Validate()),
) )
} }
@ -116,6 +129,41 @@ func (c Config) RenderClusters() ([]*clusterAPI.Cluster, error) {
def, def,
} }
if i := c.IntegrationSidecar; i != nil {
hpo, err := anypb.New(&upstreamHttpApi.HttpProtocolOptions{
UpstreamProtocolOptions: &upstreamHttpApi.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &upstreamHttpApi.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &upstreamHttpApi.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{
Http2ProtocolOptions: &coreAPI.Http2ProtocolOptions{},
},
},
},
})
if err != nil {
return nil, err
}
cluster := &clusterAPI.Cluster{
Name: "integration_sidecar",
ConnectTimeout: durationpb.New(time.Second),
LbPolicy: clusterAPI.Cluster_ROUND_ROBIN,
LoadAssignment: &endpointAPI.ClusterLoadAssignment{
ClusterName: "integration_sidecar",
Endpoints: []*endpointAPI.LocalityLbEndpoints{
{
LbEndpoints: []*endpointAPI.LbEndpoint{
i.RenderEndpoint(),
},
},
},
},
TypedExtensionProtocolOptions: map[string]*any.Any{
"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": hpo,
},
}
clusters = append(clusters, cluster)
}
for k, v := range c.Destinations { for k, v := range c.Destinations {
name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k))
c, err := v.RenderCluster(name) c, err := v.RenderCluster(name)
@ -159,6 +207,33 @@ func (c Config) RenderRoutes() ([]*routeAPI.Route, error) {
return routes, nil return routes, nil
} }
func (c Config) RenderIntegrationSidecarFilter() (*httpConnectionManagerAPI.HttpFilter, error) {
e, err := anypb.New(&httpFilterAuthzApi.ExtAuthz{
Services: &httpFilterAuthzApi.ExtAuthz_GrpcService{
GrpcService: &coreAPI.GrpcService{
TargetSpecifier: &coreAPI.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &coreAPI.GrpcService_EnvoyGrpc{
ClusterName: "integration_sidecar",
},
},
Timeout: durationpb.New(500 * time.Millisecond),
},
},
IncludePeerCertificate: true,
})
if err != nil {
return nil, err
}
return &httpConnectionManagerAPI.HttpFilter{
Name: IntegrationSidecarFilterName,
ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{
TypedConfig: e,
},
IsOptional: false,
}, nil
}
func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) { func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) {
httpFilterConfigType, err := anypb.New(&routerAPI.Router{}) httpFilterConfigType, err := anypb.New(&routerAPI.Router{})
if err != nil { if err != nil {
@ -170,6 +245,16 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) {
return nil, errors.Wrapf(err, "Unable to render routes") return nil, errors.Wrapf(err, "Unable to render routes")
} }
var httpFilters []*httpConnectionManagerAPI.HttpFilter
if i := c.IntegrationSidecar; i != nil {
q, err := c.RenderIntegrationSidecarFilter()
if err != nil {
return nil, err
}
httpFilters = append(httpFilters, q)
}
filterConfigType, err := anypb.New(&httpConnectionManagerAPI.HttpConnectionManager{ filterConfigType, err := anypb.New(&httpConnectionManagerAPI.HttpConnectionManager{
StatPrefix: "ingress_http", StatPrefix: "ingress_http",
CodecType: httpConnectionManagerAPI.HttpConnectionManager_AUTO, CodecType: httpConnectionManagerAPI.HttpConnectionManager_AUTO,
@ -185,14 +270,13 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) {
}, },
}, },
}, },
HttpFilters: []*httpConnectionManagerAPI.HttpFilter{ HttpFilters: append(httpFilters, &httpConnectionManagerAPI.HttpFilter{
{
Name: "envoy.filters.http.routerAPI", Name: "envoy.filters.http.routerAPI",
ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{ ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{
TypedConfig: httpFilterConfigType, TypedConfig: httpFilterConfigType,
}, },
}, },
}, ),
}) })
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Unable to render http connection manager") return nil, errors.Wrapf(err, "Unable to render http connection manager")
@ -211,7 +295,7 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) {
func (c Config) RenderDefaultFilterChain() (*listenerAPI.FilterChain, error) { func (c Config) RenderDefaultFilterChain() (*listenerAPI.FilterChain, error) {
filters, err := c.RenderFilters() filters, err := c.RenderFilters()
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Unable to render filters") return nil, err
} }
ret := &listenerAPI.FilterChain{ ret := &listenerAPI.FilterChain{
@ -228,7 +312,16 @@ func (c Config) RenderDefaultFilterChain() (*listenerAPI.FilterChain, error) {
} }
func (c Config) RenderSecondaryFilterChains() ([]*listenerAPI.FilterChain, error) { func (c Config) RenderSecondaryFilterChains() ([]*listenerAPI.FilterChain, error) {
if len(c.SNI) == 0 {
return nil, nil return nil, nil
}
filters, err := c.RenderFilters()
if err != nil {
return nil, err
}
return c.SNI.RenderFilterChain(filters)
} }
func (c Config) RenderListener() (*listenerAPI.Listener, error) { func (c Config) RenderListener() (*listenerAPI.Listener, error) {
@ -242,6 +335,22 @@ func (c Config) RenderListener() (*listenerAPI.Listener, error) {
return nil, errors.Wrapf(err, "Unable to render default filter") return nil, errors.Wrapf(err, "Unable to render default filter")
} }
var listenerFilters []*listenerAPI.ListenerFilter
if c.DefaultTLS != nil {
w, err := anypb.New(&tlsInspectorApi.TlsInspector{})
if err != nil {
return nil, errors.Wrapf(err, "Unable to render TLS Inspector")
}
listenerFilters = append(listenerFilters, &listenerAPI.ListenerFilter{
Name: "envoy.filters.listener.tls_inspector",
ConfigType: &listenerAPI.ListenerFilter_TypedConfig{
TypedConfig: w,
},
})
}
return &listenerAPI.Listener{ return &listenerAPI.Listener{
Name: "default", Name: "default",
Address: &coreAPI.Address{ Address: &coreAPI.Address{
@ -253,7 +362,7 @@ func (c Config) RenderListener() (*listenerAPI.Listener, error) {
}, },
}, },
FilterChains: filterChains, FilterChains: filterChains,
ListenerFilters: listenerFilters,
DefaultFilterChain: defaultFilterChain, DefaultFilterChain: defaultFilterChain,
}, nil }, nil
} }

View file

@ -26,6 +26,7 @@ import (
clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" routeAPI "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
anypb "github.com/golang/protobuf/ptypes/any"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
@ -63,7 +64,10 @@ type ConfigDestination struct {
Path *string `json:"path,omitempty"` Path *string `json:"path,omitempty"`
} }
func (c ConfigDestination) Validate() error { func (c *ConfigDestination) Validate() error {
if c == nil {
c = &ConfigDestination{}
}
return shared.WithErrors( return shared.WithErrors(
shared.PrefixResourceError("targets", c.Targets.Validate()), shared.PrefixResourceError("targets", c.Targets.Validate()),
shared.PrefixResourceError("type", c.Type.Validate()), shared.PrefixResourceError("type", c.Type.Validate()),
@ -71,15 +75,15 @@ func (c ConfigDestination) Validate() error {
) )
} }
func (c ConfigDestination) GetPath() string { func (c *ConfigDestination) GetPath() string {
if c.Path == nil { if c == nil || c.Path == nil {
return "/" return "/"
} }
return *c.Path return *c.Path
} }
func (c ConfigDestination) RenderRoute(name, prefix string) (*routeAPI.Route, error) { func (c *ConfigDestination) RenderRoute(name, prefix string) (*routeAPI.Route, error) {
return &routeAPI.Route{ return &routeAPI.Route{
Match: &routeAPI.RouteMatch{ Match: &routeAPI.RouteMatch{
PathSpecifier: &routeAPI.RouteMatch_Prefix{ PathSpecifier: &routeAPI.RouteMatch_Prefix{
@ -94,10 +98,11 @@ func (c ConfigDestination) RenderRoute(name, prefix string) (*routeAPI.Route, er
PrefixRewrite: c.GetPath(), PrefixRewrite: c.GetPath(),
}, },
}, },
TypedPerFilterConfig: map[string]*anypb.Any{},
}, nil }, nil
} }
func (c ConfigDestination) RenderCluster(name string) (*clusterAPI.Cluster, error) { func (c *ConfigDestination) RenderCluster(name string) (*clusterAPI.Cluster, error) {
cluster := &clusterAPI.Cluster{ cluster := &clusterAPI.Cluster{
Name: name, Name: name,
ConnectTimeout: durationpb.New(time.Second), ConnectTimeout: durationpb.New(time.Second),

View file

@ -54,7 +54,10 @@ type ConfigDestinationTarget struct {
Port int32 `json:"port,omitempty"` Port int32 `json:"port,omitempty"`
} }
func (c ConfigDestinationTarget) Validate() error { func (c *ConfigDestinationTarget) Validate() error {
if c == nil {
return nil
}
return shared.WithErrors( return shared.WithErrors(
shared.ValidateRequiredPath("ip", &c.Host, func(t string) error { shared.ValidateRequiredPath("ip", &c.Host, func(t string) error {
if t == "" { if t == "" {
@ -71,7 +74,10 @@ func (c ConfigDestinationTarget) Validate() error {
) )
} }
func (c ConfigDestinationTarget) RenderEndpoint() *endpointAPI.LbEndpoint { func (c *ConfigDestinationTarget) RenderEndpoint() *endpointAPI.LbEndpoint {
if c == nil {
return nil
}
return &endpointAPI.LbEndpoint{ return &endpointAPI.LbEndpoint{
HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{ HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{
Endpoint: &endpointAPI.Endpoint{ Endpoint: &endpointAPI.Endpoint{

View file

@ -0,0 +1,82 @@
//
// DISCLAIMER
//
// Copyright 2024 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 gateway
import (
listenerAPI "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1"
"github.com/arangodb/kube-arangodb/pkg/util"
"github.com/arangodb/kube-arangodb/pkg/util/errors"
)
type ConfigSNIList []ConfigSNI
func (c ConfigSNIList) RenderFilterChain(filters []*listenerAPI.Filter) ([]*listenerAPI.FilterChain, error) {
var r = make([]*listenerAPI.FilterChain, len(filters))
for id := range filters {
if f, err := c[id].RenderFilterChain(filters); err != nil {
return nil, err
} else {
r[id] = f
}
}
return r, nil
}
func (c ConfigSNIList) Validate() error {
return shared.ValidateList(c, func(sni ConfigSNI) error {
return sni.Validate()
})
}
type ConfigSNI struct {
ConfigTLS `json:",inline"`
ServerNames []string `json:"serverNames,omitempty"`
}
func (c ConfigSNI) RenderFilterChain(filters []*listenerAPI.Filter) (*listenerAPI.FilterChain, error) {
transport, err := c.RenderListenerTransportSocket()
if err != nil {
return nil, err
}
return &listenerAPI.FilterChain{
TransportSocket: transport,
FilterChainMatch: &listenerAPI.FilterChainMatch{
ServerNames: util.CopyList(c.ServerNames),
},
Filters: filters,
}, nil
}
func (c ConfigSNI) Validate() error {
return shared.WithErrors(
shared.ValidateList(c.ServerNames, sharedApi.IsValidDomain, func(in []string) error {
if len(in) == 0 {
return errors.Errorf("AtLeast one element required on list")
}
return nil
}),
)
}

View file

@ -147,4 +147,45 @@ func Test_GatewayConfig(t *testing.T) {
}, },
}) })
}) })
t.Run("Default", func(t *testing.T) {
renderAndPrintGatewayConfig(t, Config{
DefaultDestination: ConfigDestination{
Targets: []ConfigDestinationTarget{
{
Host: "127.0.0.1",
Port: 12345,
},
},
Path: util.NewType("/test/path/"),
Type: util.NewType(ConfigDestinationTypeHTTPS),
},
DefaultTLS: &ConfigTLS{
CertificatePath: "/test",
PrivateKeyPath: "/test12",
},
SNI: []ConfigSNI{
{
ConfigTLS: ConfigTLS{
CertificatePath: "/cp",
PrivateKeyPath: "/pp",
},
ServerNames: []string{
"example.com",
},
},
},
Destinations: ConfigDestinations{
"/_test/": {
Targets: []ConfigDestinationTarget{
{
Host: "127.0.0.1",
Port: 12346,
},
},
Path: util.NewType("/test/path/"),
Type: util.NewType(ConfigDestinationTypeHTTP),
},
},
})
})
} }

View file

@ -74,7 +74,7 @@ func (r *Resources) EnsurePDBs(ctx context.Context) error {
} }
minGateways, currGateways := 0, 0 minGateways, currGateways := 0, 0
if features.Gateway().Enabled() && spec.IsGatewayEnabled() { if features.IsGatewayEnabled(spec) {
minGateways = spec.GetServerGroupSpec(api.ServerGroupGateways).New().GetCount() - 1 minGateways = spec.GetServerGroupSpec(api.ServerGroupGateways).New().GetCount() - 1
currGateways = status.Members.Gateways.MembersReady() currGateways = status.Members.Gateways.MembersReady()
} }

View file

@ -34,6 +34,8 @@ const (
GatewayVolumeMountDir = "/etc/gateway/" GatewayVolumeMountDir = "/etc/gateway/"
GatewayVolumeName = "gateway" GatewayVolumeName = "gateway"
GatewayConfigFileName = "gateway.yaml" GatewayConfigFileName = "gateway.yaml"
GatewayConfigChecksumFileName = "gateway.checksum"
GatewayConfigChecksumENV = "GATEWAY_CONFIG_CHECKSUM"
GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName
) )
@ -50,6 +52,9 @@ func createGatewayVolumes(input pod.Input) pod.Volumes {
// TLS // TLS
volumes.Append(pod.TLS(), input) volumes.Append(pod.TLS(), input)
// SNI
volumes.Append(pod.SNIGateway(), input)
return volumes return volumes
} }

View file

@ -131,6 +131,19 @@ func (a *ArangoGatewayContainer) GetEnvs() ([]core.EnvVar, []core.EnvFromSource)
envs.Add(true, k8sutil.GetLifecycleEnv()...) envs.Add(true, k8sutil.GetLifecycleEnv()...)
var cmChecksum = ""
if cm, ok := a.cachedStatus.ConfigMap().V1().GetSimple(GetGatewayConfigMapName(a.input.ApiObject.GetName())); ok {
if v, ok := cm.Data[GatewayConfigChecksumFileName]; ok {
cmChecksum = v
}
}
envs.Add(true, core.EnvVar{
Name: GatewayConfigChecksumENV,
Value: cmChecksum,
})
if len(a.groupSpec.Envs) > 0 { if len(a.groupSpec.Envs) > 0 {
for _, env := range a.groupSpec.Envs { for _, env := range a.groupSpec.Envs {
// Do not override preset envs // Do not override preset envs

View file

@ -192,7 +192,13 @@ func (m *MemberGatewayPod) Init(ctx context.Context, cachedStatus interfaces.Ins
return nil return nil
} }
func (m *MemberGatewayPod) Validate(_ interfaces.Inspector) error { func (m *MemberGatewayPod) Validate(cachedStatus interfaces.Inspector) error {
i := m.AsInput()
if err := pod.SNI().Verify(i, cachedStatus); err != nil {
return err
}
if err := validateSidecars(m.groupSpec.SidecarCoreNames, m.groupSpec.GetSidecars()); err != nil { if err := validateSidecars(m.groupSpec.SidecarCoreNames, m.groupSpec.GetSidecars()); err != nil {
return err return err
} }
@ -232,7 +238,9 @@ func (m *MemberGatewayPod) Labels() map[string]string {
func (m *MemberGatewayPod) Profiles() (schedulerApi.ProfileTemplates, error) { func (m *MemberGatewayPod) Profiles() (schedulerApi.ProfileTemplates, error) {
integration, err := sidecar.NewIntegration(&schedulerContainerResourcesApi.Image{ integration, err := sidecar.NewIntegration(&schedulerContainerResourcesApi.Image{
Image: util.NewType(m.resources.context.GetOperatorImage()), Image: util.NewType(m.resources.context.GetOperatorImage()),
}, m.spec.Gateway.GetSidecar(), []string{shared.ServerContainerName}) }, m.spec.Gateway.GetSidecar(), []string{shared.ServerContainerName}, sidecar.IntegrationEnvoyV3{
Spec: m.spec,
})
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -531,7 +531,7 @@ func (r *Resources) InspectPods(ctx context.Context, cachedStatus inspectorInter
} }
spec := r.context.GetSpec() spec := r.context.GetSpec()
allMembersReady := status.Members.AllMembersReady(spec.GetMode(), r.context.IsSyncEnabled(), features.Gateway().Enabled() && spec.IsGatewayEnabled()) allMembersReady := status.Members.AllMembersReady(spec.GetMode(), r.context.IsSyncEnabled(), features.IsGatewayEnabled(spec))
status.Conditions.Update(api.ConditionTypeReady, allMembersReady, "", "") status.Conditions.Update(api.ConditionTypeReady, allMembersReady, "", "")
// Update conditions // Update conditions

View file

@ -82,7 +82,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn
defer metrics.SetDuration(inspectServicesDurationGauges.WithLabelValues(deploymentName), start) defer metrics.SetDuration(inspectServicesDurationGauges.WithLabelValues(deploymentName), start)
counterMetric := inspectedServicesCounters.WithLabelValues(deploymentName) counterMetric := inspectedServicesCounters.WithLabelValues(deploymentName)
if features.Gateway().Enabled() && spec.IsGatewayEnabled() { if features.IsGatewayEnabled(spec) {
role = api.ServerGroupGateways.AsRole() role = api.ServerGroupGateways.AsRole()
} }

View file

@ -24,6 +24,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/arangodb/kube-arangodb/pkg/util"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
) )
@ -50,10 +51,12 @@ func (c *Core) GetExternal() bool {
func (c *Core) Args(int Integration) k8sutil.OptionPairs { func (c *Core) Args(int Integration) k8sutil.OptionPairs {
var options k8sutil.OptionPairs var options k8sutil.OptionPairs
name, ver := int.Name() cmd := strings.Join(util.FormatList(int.Name(), func(a string) string {
return strings.ToLower(a)
}), ".")
options.Add(fmt.Sprintf("--integration.%s.%s.internal", strings.ToLower(name), strings.ToLower(ver)), c.GetInternal()) options.Add(fmt.Sprintf("--integration.%s.internal", cmd), c.GetInternal())
options.Add(fmt.Sprintf("--integration.%s.%s.external", strings.ToLower(name), strings.ToLower(ver)), c.GetExternal()) options.Add(fmt.Sprintf("--integration.%s.external", cmd), c.GetExternal())
return options return options
} }

View file

@ -37,8 +37,8 @@ type IntegrationAuthenticationV1 struct {
Deployment *api.ArangoDeployment Deployment *api.ArangoDeployment
} }
func (i IntegrationAuthenticationV1) Name() (string, string) { func (i IntegrationAuthenticationV1) Name() []string {
return "AUTHENTICATION", "V1" return []string{"AUTHENTICATION", "V1"}
} }
func (i IntegrationAuthenticationV1) Validate() error { func (i IntegrationAuthenticationV1) Validate() error {

View file

@ -28,8 +28,8 @@ type IntegrationAuthorizationV0 struct {
Core *Core Core *Core
} }
func (i IntegrationAuthorizationV0) Name() (string, string) { func (i IntegrationAuthorizationV0) Name() []string {
return "AUTHORIZATION", "V0" return []string{"AUTHORIZATION", "V0"}
} }
func (i IntegrationAuthorizationV0) Validate() error { func (i IntegrationAuthorizationV0) Validate() error {

View file

@ -22,34 +22,26 @@ package sidecar
import ( import (
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
"github.com/arangodb/kube-arangodb/pkg/util/errors"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
) )
type IntegrationEnvoyV3 struct { type IntegrationEnvoyV3 struct {
Core *Core Core *Core
Deployment *api.ArangoDeployment Spec api.DeploymentSpec
} }
func (i IntegrationEnvoyV3) Name() (string, string) { func (i IntegrationEnvoyV3) Name() []string {
return "ENVOY", "V3" return []string{"ENVOY", "AUTH", "V3"}
} }
func (i IntegrationEnvoyV3) Validate() error { func (i IntegrationEnvoyV3) Validate() error {
if i.Deployment == nil {
return errors.Errorf("Deployment is nil")
}
return nil return nil
} }
func (i IntegrationEnvoyV3) Args() (k8sutil.OptionPairs, error) { func (i IntegrationEnvoyV3) Args() (k8sutil.OptionPairs, error) {
options := k8sutil.CreateOptionPairs() options := k8sutil.CreateOptionPairs()
options.Add("--integration.authentication.v1", true) options.Add("--integration.envoy.auth.v3", true)
options.Add("--integration.authentication.v1.enabled", i.Deployment.GetAcceptedSpec().IsAuthenticated())
options.Add("--integration.authentication.v1.path", shared.ClusterJWTSecretVolumeMountDir)
options.Merge(i.Core.Args(i)) options.Merge(i.Core.Args(i))

View file

@ -55,7 +55,7 @@ type IntegrationVolumes interface {
} }
type Integration interface { type Integration interface {
Name() (string, string) Name() []string
Args() (k8sutil.OptionPairs, error) Args() (k8sutil.OptionPairs, error)
Validate() error Validate() error
} }
@ -63,9 +63,9 @@ type Integration interface {
func NewIntegration(image *schedulerContainerResourcesApi.Image, integration *schedulerApi.IntegrationSidecar, coreContainers []string, integrations ...Integration) (*schedulerApi.ProfileTemplate, error) { func NewIntegration(image *schedulerContainerResourcesApi.Image, integration *schedulerApi.IntegrationSidecar, coreContainers []string, integrations ...Integration) (*schedulerApi.ProfileTemplate, error) {
for _, integration := range integrations { for _, integration := range integrations {
if err := integration.Validate(); err != nil { if err := integration.Validate(); err != nil {
name, version := integration.Name() name := strings.Join(integration.Name(), "/")
return nil, errors.Wrapf(err, "Failure in %s/%s", name, version) return nil, errors.Wrapf(err, "Failure in %s", name)
} }
} }
@ -100,33 +100,35 @@ func NewIntegration(image *schedulerContainerResourcesApi.Image, integration *sc
} }
for _, i := range integrations { for _, i := range integrations {
name, version := i.Name() name := strings.Join(i.Name(), "/")
if err := i.Validate(); err != nil { if err := i.Validate(); err != nil {
return nil, errors.Wrapf(err, "Failure in %s/%s", name, version) return nil, errors.Wrapf(err, "Failure in %s", name)
} }
if args, err := i.Args(); err != nil { if args, err := i.Args(); err != nil {
return nil, errors.Wrapf(err, "Failure in arguments %s/%s", name, version) return nil, errors.Wrapf(err, "Failure in arguments %s", name)
} else if len(args) > 0 { } else if len(args) > 0 {
options.Merge(args) options.Merge(args)
} }
if lvolumes, lvolumeMounts, err := WithIntegrationVolumes(i); err != nil { if lvolumes, lvolumeMounts, err := WithIntegrationVolumes(i); err != nil {
return nil, errors.Wrapf(err, "Failure in volumes %s/%s", name, version) return nil, errors.Wrapf(err, "Failure in volumes %s", name)
} else if len(lvolumes) > 0 || len(lvolumeMounts) > 0 { } else if len(lvolumes) > 0 || len(lvolumeMounts) > 0 {
volumes = append(volumes, lvolumes...) volumes = append(volumes, lvolumes...)
volumeMounts = append(volumeMounts, lvolumeMounts...) volumeMounts = append(volumeMounts, lvolumeMounts...)
} }
if lenvs, err := WithIntegrationEnvs(i); err != nil { if lenvs, err := WithIntegrationEnvs(i); err != nil {
return nil, errors.Wrapf(err, "Failure in envs %s/%s", name, version) return nil, errors.Wrapf(err, "Failure in envs %s", name)
} else if len(lenvs) > 0 { } else if len(lenvs) > 0 {
envs = append(envs, lenvs...) envs = append(envs, lenvs...)
} }
envs = append(envs, core.EnvVar{ envs = append(envs, core.EnvVar{
Name: fmt.Sprintf("INTEGRATION_SERVICE_%s_%s", strings.ToUpper(name), strings.ToUpper(version)), Name: fmt.Sprintf("INTEGRATION_SERVICE_%s", strings.Join(util.FormatList(i.Name(), func(a string) string {
return strings.ToUpper(a)
}), "_")),
Value: fmt.Sprintf("127.0.0.1:%d", integration.GetListenPort()), Value: fmt.Sprintf("127.0.0.1:%d", integration.GetListenPort()),
}) })
} }

View file

@ -28,8 +28,8 @@ type IntegrationShutdownV1 struct {
Core *Core Core *Core
} }
func (i IntegrationShutdownV1) Name() (string, string) { func (i IntegrationShutdownV1) Name() []string {
return "SHUTDOWN", "V1" return []string{"SHUTDOWN", "V1"}
} }
func (i IntegrationShutdownV1) Validate() error { func (i IntegrationShutdownV1) Validate() error {

View file

@ -26,6 +26,7 @@ import (
core "k8s.io/api/core/v1" core "k8s.io/api/core/v1"
schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1" schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/configmap"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/service" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/service"
) )
@ -33,6 +34,7 @@ import (
type Inspector interface { type Inspector interface {
secret.Inspector secret.Inspector
service.Inspector service.Inspector
configmap.Inspector
} }
type PodModifier interface { type PodModifier interface {

View file

@ -101,3 +101,9 @@ func FormatListErr[A, B any](in []A, format func(A) (B, error)) ([]B, error) {
return r, nil return r, nil
} }
func CopyList[A any](in []A) []A {
ret := make([]A, len(in))
copy(ret, in)
return ret
}

33
pkg/util/tests/cast.go Normal file
View file

@ -0,0 +1,33 @@
//
// DISCLAIMER
//
// Copyright 2024 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 tests
import (
"testing"
"github.com/stretchr/testify/require"
)
func CastAs[A any](t *testing.T, in any) A {
v, ok := in.(A)
require.True(t, ok)
return v
}