diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7db150f..68b34d2d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - (Bugfix) Fix race condition in ArangoBackup - (Feature) Improve Gateway Config gen - (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) - (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries diff --git a/Dockerfile b/Dockerfile index 9cfc8a5d8..0c5d1c196 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,9 @@ FROM ${ENVOY_IMAGE} AS envoy 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 diff --git a/Makefile b/Makefile index 2cadcdadb..7760ebab0 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,8 @@ ifndef LOCALONLY PUSHIMAGES := 1 endif +BUILD_SKIP_UPDATE ?= false + ifdef IMAGETAG IMAGESUFFIX := :$(IMAGETAG) 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 \ 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_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)) @@ -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) ifdef PUSHIMAGES 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) . else 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) . 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]\+" \ "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ + "$(ROOT)/pkg/integrations/" \ "$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ @@ -818,6 +821,7 @@ set-api-version/%: @grep -rHn "github.com/arangodb/kube-arangodb/pkg/apis/$*/v[A-Za-z0-9]\+" \ "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ + "$(ROOT)/pkg/integrations/" \ "$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ @@ -831,6 +835,7 @@ set-api-version/%: @grep -rHn "DatabaseV[A-Za-z0-9]\+()" \ "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ + "$(ROOT)/pkg/integrations/" \ "$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ @@ -844,6 +849,7 @@ set-api-version/%: @grep -rHn "ReplicationV[A-Za-z0-9]\+()" \ "$(ROOT)/pkg/deployment/" \ "$(ROOT)/pkg/replication/" \ + "$(ROOT)/pkg/integrations/" \ "$(ROOT)/pkg/operator/" \ "$(ROOT)/pkg/operatorV2/" \ "$(ROOT)/pkg/server/" \ diff --git a/README.md b/README.md index 863f667b5..71e7392a3 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Flags: --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) --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 or =. 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 or =. 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) --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 diff --git a/chart/arangodb-ingress-proxy/Chart.yaml b/chart/arangodb-ingress-proxy/Chart.yaml deleted file mode 100644 index 74d1c8abe..000000000 --- a/chart/arangodb-ingress-proxy/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -description: ArangoDB Ingress Proxy -name: arangodb-ingress-proxy -tillerVersion: '>2.7' -version: 1.0.0 diff --git a/chart/arangodb-ingress-proxy/LICENSE b/chart/arangodb-ingress-proxy/LICENSE deleted file mode 100644 index 79013b689..000000000 --- a/chart/arangodb-ingress-proxy/LICENSE +++ /dev/null @@ -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 \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/README.md b/chart/arangodb-ingress-proxy/README.md deleted file mode 100644 index 9b5499e96..000000000 --- a/chart/arangodb-ingress-proxy/README.md +++ /dev/null @@ -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 create secret tls --cert --key -``` - -## Installation - -To install Ingress: -``` -helm install --name --namespace /chart/arangodb-ingress-proxy --set replicas=2 --set tls=TLS Secret name> --set deployment= -``` \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/templates/NOTES.txt b/chart/arangodb-ingress-proxy/templates/NOTES.txt deleted file mode 100644 index b99bf1620..000000000 --- a/chart/arangodb-ingress-proxy/templates/NOTES.txt +++ /dev/null @@ -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}"` \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/templates/_helpers.tpl b/chart/arangodb-ingress-proxy/templates/_helpers.tpl deleted file mode 100644 index d35a6ae38..000000000 --- a/chart/arangodb-ingress-proxy/templates/_helpers.tpl +++ /dev/null @@ -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 -}} diff --git a/chart/arangodb-ingress-proxy/templates/configmap.yaml b/chart/arangodb-ingress-proxy/templates/configmap.yaml deleted file mode 100644 index cfa7b9592..000000000 --- a/chart/arangodb-ingress-proxy/templates/configmap.yaml +++ /dev/null @@ -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; - } - } \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/templates/deployment.yaml b/chart/arangodb-ingress-proxy/templates/deployment.yaml deleted file mode 100644 index ff40aaa2a..000000000 --- a/chart/arangodb-ingress-proxy/templates/deployment.yaml +++ /dev/null @@ -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 }} \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/templates/service.yaml b/chart/arangodb-ingress-proxy/templates/service.yaml deleted file mode 100644 index f11e2c815..000000000 --- a/chart/arangodb-ingress-proxy/templates/service.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/chart/arangodb-ingress-proxy/values.yaml b/chart/arangodb-ingress-proxy/values.yaml deleted file mode 100644 index 186e10dd7..000000000 --- a/chart/arangodb-ingress-proxy/values.yaml +++ /dev/null @@ -1,3 +0,0 @@ -replicas: 2 -imagePullPolicy: Always -image: nginx:1.16.1-alpine diff --git a/docs/cli/arangodb_operator.md b/docs/cli/arangodb_operator.md index e9629d728..938126a07 100644 --- a/docs/cli/arangodb_operator.md +++ b/docs/cli/arangodb_operator.md @@ -80,7 +80,7 @@ Flags: --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) --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 or =. 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 or =. 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) --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 diff --git a/integrations/envoy/auth/v3/impl.go b/integrations/envoy/auth/v3/impl.go index b85bd2c40..c3930b08b 100644 --- a/integrations/envoy/auth/v3/impl.go +++ b/integrations/envoy/auth/v3/impl.go @@ -53,5 +53,10 @@ func (i *impl) Register(registrar *grpc.Server) { } 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 } diff --git a/integrations/envoy/auth/v3/logger.go b/integrations/envoy/auth/v3/logger.go new file mode 100644 index 000000000..26d7ae87d --- /dev/null +++ b/integrations/envoy/auth/v3/logger.go @@ -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) diff --git a/integrations/envoy/auth/v3/service_test.go b/integrations/envoy/auth/v3/service_test.go index 440182d9d..a1eeb34c3 100644 --- a/integrations/envoy/auth/v3/service_test.go +++ b/integrations/envoy/auth/v3/service_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/require" "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" ) @@ -53,6 +54,7 @@ func Test_AllowAll(t *testing.T) { require.NoError(t, err) require.NoError(t, resp.Validate()) 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) } diff --git a/pkg/apis/shared/validate.go b/pkg/apis/shared/validate.go index 2c746f63e..431b5f241 100644 --- a/pkg/apis/shared/validate.go +++ b/pkg/apis/shared/validate.go @@ -180,19 +180,23 @@ func ValidateRequiredInterfacePath[T ValidateInterface](path string, in T) error } // ValidateList validates all elements on the list -func ValidateList[T any](in []T, validator func(T) error) error { - errors := make([]error, len(in)) +func ValidateList[T any](in []T, validator func(T) error, checks ...func(in []T) error) error { + errors := make([]error, len(in)+len(checks)) for id := range in { errors[id] = PrefixResourceError(fmt.Sprintf("[%d]", id), validator(in[id])) } + for id, c := range checks { + errors[len(in)+id] = c(in) + } + return WithErrors(errors...) } // ValidateMap validates all elements on the list -func ValidateMap[T any](in map[string]T, validator func(string, T) error) error { - errors := make([]error, 0, len(in)) +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)+len(checks)) for id := range in { 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...) } diff --git a/pkg/deployment/features/gateway.go b/pkg/deployment/features/gateway.go index 40173ef50..ce2c7b454 100644 --- a/pkg/deployment/features/gateway.go +++ b/pkg/deployment/features/gateway.go @@ -20,6 +20,8 @@ package features +import api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + func init() { registerFeature(gateway) } @@ -35,3 +37,7 @@ var gateway = &feature{ func Gateway() Feature { return gateway } + +func IsGatewayEnabled(spec api.DeploymentSpec) bool { + return Gateway().Enabled() && spec.IsGatewayEnabled() +} diff --git a/pkg/deployment/pod/sni-gateway.go b/pkg/deployment/pod/sni-gateway.go new file mode 100644 index 000000000..2bdf15d0b --- /dev/null +++ b/pkg/deployment/pod/sni-gateway.go @@ -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 +} diff --git a/pkg/deployment/pod/sni.go b/pkg/deployment/pod/sni.go index 4b6da103b..74f66e7a2 100644 --- a/pkg/deployment/pod/sni.go +++ b/pkg/deployment/pod/sni.go @@ -21,7 +21,6 @@ package pod import ( - "crypto/sha256" "fmt" core "k8s.io/api/core/v1" @@ -36,12 +35,18 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" ) -func GroupSNISupported(mode api.DeploymentMode, group api.ServerGroup) bool { - switch mode { +func GroupSNISupported(spec api.DeploymentSpec, group api.ServerGroup) bool { + switch spec.Mode.Get() { case api.DeploymentModeCluster: + if features.IsGatewayEnabled(spec) { + return group == api.ServerGroupGateways + } return group == api.ServerGroupCoordinators case api.DeploymentModeSingle: + if features.IsGatewayEnabled(spec) { + return group == api.ServerGroupGateways + } fallthrough case api.DeploymentModeActiveFailover: return group == api.ServerGroupSingle @@ -70,7 +75,7 @@ func (s sni) isSupported(i Input) bool { 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 { @@ -102,7 +107,7 @@ func (s sni) Volumes(i Input) ([]core.Volume, []core.VolumeMount) { volumeMounts := make([]core.VolumeMount, 0, len(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]) diff --git a/pkg/deployment/reconcile/plan_builder_rebalancer_v2.go b/pkg/deployment/reconcile/plan_builder_rebalancer_v2.go index cf76139f2..13178b430 100644 --- a/pkg/deployment/reconcile/plan_builder_rebalancer_v2.go +++ b/pkg/deployment/reconcile/plan_builder_rebalancer_v2.go @@ -45,7 +45,7 @@ func (r *Reconciler) createRebalancerV2GeneratePlan(spec api.DeploymentSpec, sta 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 } diff --git a/pkg/deployment/reconcile/plan_builder_scale.go b/pkg/deployment/reconcile/plan_builder_scale.go index 8ae98d66f..2744263b1 100644 --- a/pkg/deployment/reconcile/plan_builder_scale.go +++ b/pkg/deployment/reconcile/plan_builder_scale.go @@ -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)...) } } - 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)...) } else { plan = append(plan, r.createScalePlan(status, status.Members.Gateways, api.ServerGroupGateways, 0, context)...) diff --git a/pkg/deployment/reconcile/plan_builder_tls_sni.go b/pkg/deployment/reconcile/plan_builder_tls_sni.go index 0e314fc94..b9dd4ebfc 100644 --- a/pkg/deployment/reconcile/plan_builder_tls_sni.go +++ b/pkg/deployment/reconcile/plan_builder_tls_sni.go @@ -1,7 +1,7 @@ // // 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"); // 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 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 } for _, m := range status.Members.MembersOfGroup(group) { diff --git a/pkg/deployment/resources/config_map_gateway.go b/pkg/deployment/resources/config_map_gateway.go index 6f1668696..739474f8e 100644 --- a/pkg/deployment/resources/config_map_gateway.go +++ b/pkg/deployment/resources/config_map_gateway.go @@ -23,6 +23,7 @@ package resources import ( "context" "fmt" + "path" "path/filepath" core "k8s.io/api/core/v1" @@ -65,7 +66,8 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec Name: configMapName, }, 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() } else { // 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 if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, patcher.PatchConfigMapData(map[string]string{ - GatewayConfigFileName: string(gatewayCfgYaml), + GatewayConfigFileName: string(gatewayCfgYaml), + GatewayConfigChecksumFileName: gatewayCfgChecksum, })); err != nil { log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap") return errors.WithStack(err) @@ -116,6 +119,11 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto 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{ Targets: []gateway.ConfigDestinationTarget{ { @@ -133,6 +141,25 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto PrivateKeyPath: keyPath, } 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 diff --git a/pkg/deployment/resources/config_maps.go b/pkg/deployment/resources/config_maps.go index 29a7f7317..e6fec0239 100644 --- a/pkg/deployment/resources/config_maps.go +++ b/pkg/deployment/resources/config_maps.go @@ -49,7 +49,7 @@ func (r *Resources) EnsureConfigMaps(ctx context.Context, cachedStatus inspector reconcileRequired := k8sutil.NewReconcile(cachedStatus) - if features.Gateway().Enabled() && spec.IsGatewayEnabled() { + if features.IsGatewayEnabled(spec) { counterMetric.Inc() if err := reconcileRequired.WithError(r.ensureGatewayConfig(ctx, cachedStatus, configMaps)); err != nil { return errors.Section(err, "Gateway ConfigMap") diff --git a/pkg/deployment/resources/gateway/consts.go b/pkg/deployment/resources/gateway/consts.go new file mode 100644 index 000000000..a624495df --- /dev/null +++ b/pkg/deployment/resources/gateway/consts.go @@ -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" +) diff --git a/pkg/deployment/resources/gateway/gateway_config.go b/pkg/deployment/resources/gateway/gateway_config.go index 465ee41f1..5206aaafd 100644 --- a/pkg/deployment/resources/gateway/gateway_config.go +++ b/pkg/deployment/resources/gateway/gateway_config.go @@ -23,16 +23,23 @@ package gateway import ( "fmt" "sort" + "time" bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/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" 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" + 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" + 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/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" "sigs.k8s.io/yaml" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" @@ -46,12 +53,18 @@ type Config struct { Destinations ConfigDestinations `json:"destinations,omitempty"` DefaultTLS *ConfigTLS `json:"defaultTLS,omitempty"` + + IntegrationSidecar *ConfigDestinationTarget `json:"integrationSidecar,omitempty"` + + SNI ConfigSNIList `json:"sni,omitempty"` } func (c Config) Validate() error { return errors.Errors( shared.PrefixResourceErrors("defaultDestination", c.DefaultDestination.Validate()), + shared.PrefixResourceErrors("integrationSidecar", c.IntegrationSidecar.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, } + 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 { name := fmt.Sprintf("cluster_%s", util.SHA256FromString(k)) c, err := v.RenderCluster(name) @@ -159,6 +207,33 @@ func (c Config) RenderRoutes() ([]*routeAPI.Route, error) { 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) { httpFilterConfigType, err := anypb.New(&routerAPI.Router{}) if err != nil { @@ -170,6 +245,16 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) { 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{ StatPrefix: "ingress_http", CodecType: httpConnectionManagerAPI.HttpConnectionManager_AUTO, @@ -185,14 +270,13 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) { }, }, }, - HttpFilters: []*httpConnectionManagerAPI.HttpFilter{ - { - Name: "envoy.filters.http.routerAPI", - ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{ - TypedConfig: httpFilterConfigType, - }, + HttpFilters: append(httpFilters, &httpConnectionManagerAPI.HttpFilter{ + Name: "envoy.filters.http.routerAPI", + ConfigType: &httpConnectionManagerAPI.HttpFilter_TypedConfig{ + TypedConfig: httpFilterConfigType, }, }, + ), }) if err != nil { 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) { filters, err := c.RenderFilters() if err != nil { - return nil, errors.Wrapf(err, "Unable to render filters") + return nil, err } ret := &listenerAPI.FilterChain{ @@ -228,7 +312,16 @@ func (c Config) RenderDefaultFilterChain() (*listenerAPI.FilterChain, error) { } func (c Config) RenderSecondaryFilterChains() ([]*listenerAPI.FilterChain, error) { - return nil, nil + if len(c.SNI) == 0 { + 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) { @@ -242,6 +335,22 @@ func (c Config) RenderListener() (*listenerAPI.Listener, error) { 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{ Name: "default", Address: &coreAPI.Address{ @@ -252,8 +361,8 @@ func (c Config) RenderListener() (*listenerAPI.Listener, error) { }, }, }, - FilterChains: filterChains, - + FilterChains: filterChains, + ListenerFilters: listenerFilters, DefaultFilterChain: defaultFilterChain, }, nil } diff --git a/pkg/deployment/resources/gateway/gateway_config_destination.go b/pkg/deployment/resources/gateway/gateway_config_destination.go index 29dbcace1..f8ee81c63 100644 --- a/pkg/deployment/resources/gateway/gateway_config_destination.go +++ b/pkg/deployment/resources/gateway/gateway_config_destination.go @@ -26,6 +26,7 @@ import ( clusterAPI "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" endpointAPI "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/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" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" @@ -63,7 +64,10 @@ type ConfigDestination struct { Path *string `json:"path,omitempty"` } -func (c ConfigDestination) Validate() error { +func (c *ConfigDestination) Validate() error { + if c == nil { + c = &ConfigDestination{} + } return shared.WithErrors( shared.PrefixResourceError("targets", c.Targets.Validate()), shared.PrefixResourceError("type", c.Type.Validate()), @@ -71,15 +75,15 @@ func (c ConfigDestination) Validate() error { ) } -func (c ConfigDestination) GetPath() string { - if c.Path == nil { +func (c *ConfigDestination) GetPath() string { + if c == nil || c.Path == nil { return "/" } 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{ Match: &routeAPI.RouteMatch{ PathSpecifier: &routeAPI.RouteMatch_Prefix{ @@ -94,10 +98,11 @@ func (c ConfigDestination) RenderRoute(name, prefix string) (*routeAPI.Route, er PrefixRewrite: c.GetPath(), }, }, + TypedPerFilterConfig: map[string]*anypb.Any{}, }, nil } -func (c ConfigDestination) RenderCluster(name string) (*clusterAPI.Cluster, error) { +func (c *ConfigDestination) RenderCluster(name string) (*clusterAPI.Cluster, error) { cluster := &clusterAPI.Cluster{ Name: name, ConnectTimeout: durationpb.New(time.Second), diff --git a/pkg/deployment/resources/gateway/gateway_config_destination_target.go b/pkg/deployment/resources/gateway/gateway_config_destination_target.go index 4f2d974c9..74227a22d 100644 --- a/pkg/deployment/resources/gateway/gateway_config_destination_target.go +++ b/pkg/deployment/resources/gateway/gateway_config_destination_target.go @@ -54,7 +54,10 @@ type ConfigDestinationTarget struct { Port int32 `json:"port,omitempty"` } -func (c ConfigDestinationTarget) Validate() error { +func (c *ConfigDestinationTarget) Validate() error { + if c == nil { + return nil + } return shared.WithErrors( shared.ValidateRequiredPath("ip", &c.Host, func(t string) error { 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{ HostIdentifier: &endpointAPI.LbEndpoint_Endpoint{ Endpoint: &endpointAPI.Endpoint{ diff --git a/pkg/deployment/resources/gateway/gateway_config_sni.go b/pkg/deployment/resources/gateway/gateway_config_sni.go new file mode 100644 index 000000000..6eae003ab --- /dev/null +++ b/pkg/deployment/resources/gateway/gateway_config_sni.go @@ -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 + }), + ) +} diff --git a/pkg/deployment/resources/gateway/gateway_config_test.go b/pkg/deployment/resources/gateway/gateway_config_test.go index d8655f03f..0d0a86599 100644 --- a/pkg/deployment/resources/gateway/gateway_config_test.go +++ b/pkg/deployment/resources/gateway/gateway_config_test.go @@ -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), + }, + }, + }) + }) } diff --git a/pkg/deployment/resources/pdbs.go b/pkg/deployment/resources/pdbs.go index 36530396c..abd10ed79 100644 --- a/pkg/deployment/resources/pdbs.go +++ b/pkg/deployment/resources/pdbs.go @@ -74,7 +74,7 @@ func (r *Resources) EnsurePDBs(ctx context.Context) error { } minGateways, currGateways := 0, 0 - if features.Gateway().Enabled() && spec.IsGatewayEnabled() { + if features.IsGatewayEnabled(spec) { minGateways = spec.GetServerGroupSpec(api.ServerGroupGateways).New().GetCount() - 1 currGateways = status.Members.Gateways.MembersReady() } diff --git a/pkg/deployment/resources/pod_creator_gateway.go b/pkg/deployment/resources/pod_creator_gateway.go index 993e6f820..55df1a4b3 100644 --- a/pkg/deployment/resources/pod_creator_gateway.go +++ b/pkg/deployment/resources/pod_creator_gateway.go @@ -30,11 +30,13 @@ import ( ) const ( - ArangoGatewayExecutor string = "/usr/local/bin/envoy" - GatewayVolumeMountDir = "/etc/gateway/" - GatewayVolumeName = "gateway" - GatewayConfigFileName = "gateway.yaml" - GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName + ArangoGatewayExecutor string = "/usr/local/bin/envoy" + GatewayVolumeMountDir = "/etc/gateway/" + GatewayVolumeName = "gateway" + GatewayConfigFileName = "gateway.yaml" + GatewayConfigChecksumFileName = "gateway.checksum" + GatewayConfigChecksumENV = "GATEWAY_CONFIG_CHECKSUM" + GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName ) func GetGatewayConfigMapName(name string) string { @@ -50,6 +52,9 @@ func createGatewayVolumes(input pod.Input) pod.Volumes { // TLS volumes.Append(pod.TLS(), input) + // SNI + volumes.Append(pod.SNIGateway(), input) + return volumes } diff --git a/pkg/deployment/resources/pod_creator_gateway_container.go b/pkg/deployment/resources/pod_creator_gateway_container.go index 6510b4e72..cc4a3767c 100644 --- a/pkg/deployment/resources/pod_creator_gateway_container.go +++ b/pkg/deployment/resources/pod_creator_gateway_container.go @@ -131,6 +131,19 @@ func (a *ArangoGatewayContainer) GetEnvs() ([]core.EnvVar, []core.EnvFromSource) 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 { for _, env := range a.groupSpec.Envs { // Do not override preset envs diff --git a/pkg/deployment/resources/pod_creator_gateway_pod.go b/pkg/deployment/resources/pod_creator_gateway_pod.go index 75daaaf92..5f8fc16bf 100644 --- a/pkg/deployment/resources/pod_creator_gateway_pod.go +++ b/pkg/deployment/resources/pod_creator_gateway_pod.go @@ -192,7 +192,13 @@ func (m *MemberGatewayPod) Init(ctx context.Context, cachedStatus interfaces.Ins 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 { return err } @@ -232,7 +238,9 @@ func (m *MemberGatewayPod) Labels() map[string]string { func (m *MemberGatewayPod) Profiles() (schedulerApi.ProfileTemplates, error) { integration, err := sidecar.NewIntegration(&schedulerContainerResourcesApi.Image{ 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 { return nil, err diff --git a/pkg/deployment/resources/pod_inspector.go b/pkg/deployment/resources/pod_inspector.go index 1a51f28e3..1d6e2e3b8 100644 --- a/pkg/deployment/resources/pod_inspector.go +++ b/pkg/deployment/resources/pod_inspector.go @@ -531,7 +531,7 @@ func (r *Resources) InspectPods(ctx context.Context, cachedStatus inspectorInter } 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, "", "") // Update conditions diff --git a/pkg/deployment/resources/services.go b/pkg/deployment/resources/services.go index a040f6f9b..6e691e41a 100644 --- a/pkg/deployment/resources/services.go +++ b/pkg/deployment/resources/services.go @@ -82,7 +82,7 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn defer metrics.SetDuration(inspectServicesDurationGauges.WithLabelValues(deploymentName), start) counterMetric := inspectedServicesCounters.WithLabelValues(deploymentName) - if features.Gateway().Enabled() && spec.IsGatewayEnabled() { + if features.IsGatewayEnabled(spec) { role = api.ServerGroupGateways.AsRole() } diff --git a/pkg/integrations/sidecar/core.go b/pkg/integrations/sidecar/core.go index fc61f232b..a79132cf2 100644 --- a/pkg/integrations/sidecar/core.go +++ b/pkg/integrations/sidecar/core.go @@ -24,6 +24,7 @@ import ( "fmt" "strings" + "github.com/arangodb/kube-arangodb/pkg/util" "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 { 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.%s.external", strings.ToLower(name), strings.ToLower(ver)), c.GetExternal()) + options.Add(fmt.Sprintf("--integration.%s.internal", cmd), c.GetInternal()) + options.Add(fmt.Sprintf("--integration.%s.external", cmd), c.GetExternal()) return options } diff --git a/pkg/integrations/sidecar/integration.authentication.v1.go b/pkg/integrations/sidecar/integration.authentication.v1.go index 7fc742fa6..a51df0c78 100644 --- a/pkg/integrations/sidecar/integration.authentication.v1.go +++ b/pkg/integrations/sidecar/integration.authentication.v1.go @@ -37,8 +37,8 @@ type IntegrationAuthenticationV1 struct { Deployment *api.ArangoDeployment } -func (i IntegrationAuthenticationV1) Name() (string, string) { - return "AUTHENTICATION", "V1" +func (i IntegrationAuthenticationV1) Name() []string { + return []string{"AUTHENTICATION", "V1"} } func (i IntegrationAuthenticationV1) Validate() error { diff --git a/pkg/integrations/sidecar/integration.authorization.v1.go b/pkg/integrations/sidecar/integration.authorization.v1.go index 76fbe8b67..a94653e7f 100644 --- a/pkg/integrations/sidecar/integration.authorization.v1.go +++ b/pkg/integrations/sidecar/integration.authorization.v1.go @@ -28,8 +28,8 @@ type IntegrationAuthorizationV0 struct { Core *Core } -func (i IntegrationAuthorizationV0) Name() (string, string) { - return "AUTHORIZATION", "V0" +func (i IntegrationAuthorizationV0) Name() []string { + return []string{"AUTHORIZATION", "V0"} } func (i IntegrationAuthorizationV0) Validate() error { diff --git a/pkg/integrations/sidecar/integration.envoy.v3.go b/pkg/integrations/sidecar/integration.envoy.v3.go index 08f626871..e8509eeb7 100644 --- a/pkg/integrations/sidecar/integration.envoy.v3.go +++ b/pkg/integrations/sidecar/integration.envoy.v3.go @@ -22,34 +22,26 @@ package sidecar import ( 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" ) type IntegrationEnvoyV3 struct { - Core *Core - Deployment *api.ArangoDeployment + Core *Core + Spec api.DeploymentSpec } -func (i IntegrationEnvoyV3) Name() (string, string) { - return "ENVOY", "V3" +func (i IntegrationEnvoyV3) Name() []string { + return []string{"ENVOY", "AUTH", "V3"} } func (i IntegrationEnvoyV3) Validate() error { - if i.Deployment == nil { - return errors.Errorf("Deployment is nil") - } - return nil } func (i IntegrationEnvoyV3) Args() (k8sutil.OptionPairs, error) { options := k8sutil.CreateOptionPairs() - options.Add("--integration.authentication.v1", true) - options.Add("--integration.authentication.v1.enabled", i.Deployment.GetAcceptedSpec().IsAuthenticated()) - options.Add("--integration.authentication.v1.path", shared.ClusterJWTSecretVolumeMountDir) + options.Add("--integration.envoy.auth.v3", true) options.Merge(i.Core.Args(i)) diff --git a/pkg/integrations/sidecar/integration.go b/pkg/integrations/sidecar/integration.go index 53146ff0e..0512fc8cb 100644 --- a/pkg/integrations/sidecar/integration.go +++ b/pkg/integrations/sidecar/integration.go @@ -55,7 +55,7 @@ type IntegrationVolumes interface { } type Integration interface { - Name() (string, string) + Name() []string Args() (k8sutil.OptionPairs, 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) { for _, integration := range integrations { 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 { - name, version := i.Name() + name := strings.Join(i.Name(), "/") 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 { - 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 { options.Merge(args) } 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 { volumes = append(volumes, lvolumes...) volumeMounts = append(volumeMounts, lvolumeMounts...) } 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 { envs = append(envs, lenvs...) } 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()), }) } diff --git a/pkg/integrations/sidecar/integration.shutdown.v1.go b/pkg/integrations/sidecar/integration.shutdown.v1.go index f8752c6d2..c990cf7a7 100644 --- a/pkg/integrations/sidecar/integration.shutdown.v1.go +++ b/pkg/integrations/sidecar/integration.shutdown.v1.go @@ -28,8 +28,8 @@ type IntegrationShutdownV1 struct { Core *Core } -func (i IntegrationShutdownV1) Name() (string, string) { - return "SHUTDOWN", "V1" +func (i IntegrationShutdownV1) Name() []string { + return []string{"SHUTDOWN", "V1"} } func (i IntegrationShutdownV1) Validate() error { diff --git a/pkg/util/k8sutil/interfaces/pod_creator.go b/pkg/util/k8sutil/interfaces/pod_creator.go index 2bfe75934..577302ede 100644 --- a/pkg/util/k8sutil/interfaces/pod_creator.go +++ b/pkg/util/k8sutil/interfaces/pod_creator.go @@ -26,6 +26,7 @@ import ( core "k8s.io/api/core/v1" 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/service" ) @@ -33,6 +34,7 @@ import ( type Inspector interface { secret.Inspector service.Inspector + configmap.Inspector } type PodModifier interface { diff --git a/pkg/util/list.go b/pkg/util/list.go index d58b2c6dc..2046e4ee8 100644 --- a/pkg/util/list.go +++ b/pkg/util/list.go @@ -101,3 +101,9 @@ func FormatListErr[A, B any](in []A, format func(A) (B, error)) ([]B, error) { return r, nil } + +func CopyList[A any](in []A) []A { + ret := make([]A, len(in)) + copy(ret, in) + return ret +} diff --git a/pkg/util/tests/cast.go b/pkg/util/tests/cast.go new file mode 100644 index 000000000..e0bda955b --- /dev/null +++ b/pkg/util/tests/cast.go @@ -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 +}