diff --git a/packages/server/modules/multiregion/domain/operations.ts b/packages/server/modules/multiregion/domain/operations.ts index d54e100669..c2ce5cc26d 100644 --- a/packages/server/modules/multiregion/domain/operations.ts +++ b/packages/server/modules/multiregion/domain/operations.ts @@ -1,5 +1,3 @@ -import { RegionServerConfig } from '@/modules/multiregion/domain/types' +import { MultiRegionConfig } from '@/modules/multiregion/domain/types' -export type GetAvailableRegionConfigs = () => Promise<{ - [key: string]: RegionServerConfig -}> +export type GetAvailableRegionConfigs = () => Promise diff --git a/packages/server/modules/multiregion/domain/types.ts b/packages/server/modules/multiregion/domain/types.ts index 0360640595..e905028584 100644 --- a/packages/server/modules/multiregion/domain/types.ts +++ b/packages/server/modules/multiregion/domain/types.ts @@ -1,12 +1,4 @@ -export type RegionServerConfig = { - postgres: { - /** - * Full Postgres connection URI (e.g. "postgres://user:password@host:port/dbname") - */ - connectionUri: string - /** - * SSL cert, if any - */ - publicTlsCertificate?: string - } -} +import { z } from 'zod' +import { multiRegionConfigSchema } from '@/modules/multiregion/helpers/validation' + +export type MultiRegionConfig = z.infer diff --git a/packages/server/modules/multiregion/helpers/validation.ts b/packages/server/modules/multiregion/helpers/validation.ts new file mode 100644 index 0000000000..23b8c0fe6e --- /dev/null +++ b/packages/server/modules/multiregion/helpers/validation.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' + +export const regionServerConfigSchema = z.object({ + postgres: z.object({ + connectionUri: z + .string() + .url() + .describe( + 'Full Postgres connection URI (e.g. "postgres://user:password@host:port/dbname")' + ), + publicTlsCertificate: z + .string() + .describe('Public TLS ("CA") certificate for the Postgres server') + }) + //TODO - add the rest of the config when blob storage is implemented + // blobStorage: z + // .object({ + // endpoint: z.string().url(), + // accessKey: z.string(), + // secretKey: z.string(), + // bucket: z.string() + // }) +}) + +export const multiRegionConfigSchema = z.record(z.string(), regionServerConfigSchema) diff --git a/packages/server/modules/multiregion/services/config.ts b/packages/server/modules/multiregion/services/config.ts index 13c9eb5e97..7292b1a658 100644 --- a/packages/server/modules/multiregion/services/config.ts +++ b/packages/server/modules/multiregion/services/config.ts @@ -1,14 +1,26 @@ -import { GetAvailableRegionConfigs } from '@/modules/multiregion/domain/operations' +import { packageRoot } from '@/bootstrap' +import path from 'node:path' +import fs from 'node:fs/promises' +import { getMultiRegionConfigPath } from '@/modules/shared/helpers/envHelper' +import type { Optional } from '@speckle/shared' +import type { GetAvailableRegionConfigs } from '@/modules/multiregion/domain/operations' +import { type MultiRegionConfig } from '@/modules/multiregion/domain/types' +import { multiRegionConfigSchema } from '@/modules/multiregion/helpers/validation' + +let multiRegionConfig: Optional = undefined export const getAvailableRegionConfigsFactory = (): GetAvailableRegionConfigs => async () => { - // TODO: Hardcoded for now, should be fetched from a config file - return { - eu: { - postgres: { - connectionUri: 'postgresql://speckle:speckle@localhost/speckle_eu', - publicTlsCertificate: undefined - } - } - } + if (multiRegionConfig) return multiRegionConfig + + const relativePath = getMultiRegionConfigPath() // This will throw if the path is not set + const fullPath = path.resolve(packageRoot, relativePath) + const file = await fs.readFile(fullPath, 'utf-8') + + const parsedJson = JSON.parse(file) // This will throw if the file is not valid JSON + + const multiRegionConfigFileContents = multiRegionConfigSchema.parse(parsedJson) // This will throw if the config is invalid + + multiRegionConfig = multiRegionConfigFileContents + return multiRegionConfig } diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index fb851794de..aa2754c8a5 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -412,3 +412,7 @@ export function getOtelTraceKey() { export function getOtelHeaderValue() { return getStringFromEnv('OTEL_TRACE_VALUE') } + +export function getMultiRegionConfigPath() { + return getStringFromEnv('MULTI_REGION_CONFIG_PATH') +} diff --git a/utils/helm/speckle-server/templates/_helpers.tpl b/utils/helm/speckle-server/templates/_helpers.tpl index e4c2ec93d7..e859e2b308 100644 --- a/utils/helm/speckle-server/templates/_helpers.tpl +++ b/utils/helm/speckle-server/templates/_helpers.tpl @@ -1060,4 +1060,8 @@ Generate the environment variables for Speckle server and Speckle objects deploy - name: OTEL_TRACE_VALUE value: {{ .Values.openTelemetry.tracing.value | quote }} {{- end }} +{{- if .Values.featureFlags.workspacesMultiRegionEnabled }} +- name: MULTI_REGION_CONFIG_PATH + value: {{ (printf "/%s" .Values.multiRegion.config.secretKey) | quote}} +{{- end }} {{- end }} diff --git a/utils/helm/speckle-server/templates/objects/deployment.yml b/utils/helm/speckle-server/templates/objects/deployment.yml index 4ca543debb..5b893159f4 100644 --- a/utils/helm/speckle-server/templates/objects/deployment.yml +++ b/utils/helm/speckle-server/templates/objects/deployment.yml @@ -57,6 +57,15 @@ spec: - name: postgres-certificate mountPath: /postgres-certificate {{- end }} + {{- if .Values.featureFlags.automateModuleEnabled }} + - name: encryption-keys + readOnly: true + mountPath: /encryption-keys + {{- end }} + {{- if .Values.featureFlags.workspacesMultiRegionEnabled }} + - name: multi-region-config + mountPath: /multi-region-config + {{- end }} # Allow for k8s to remove the pod from the service endpoints to stop receive traffic lifecycle: @@ -128,3 +137,13 @@ spec: configMap: name: postgres-certificate {{- end }} + {{- if .Values.featureFlags.automateModuleEnabled }} + - name: encryption-keys + secret: + secretName: encryption-keys + {{- end }} + {{- if .Values.featureFlags.workspacesMultiRegionEnabled }} + - name: multi-region-config + secret: + secretName: {{ .Values.multiRegion.config.secretName }} + {{- end }} diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index eddf2e6ab4..ae3516b31b 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -62,6 +62,11 @@ spec: readOnly: true mountPath: /encryption-keys {{- end }} + {{- if .Values.featureFlags.workspacesMultiRegionEnabled }} + - name: multi-region-config + mountPath: / + readOnly: true + {{- end }} # Allow for k8s to remove the pod from the service endpoints to stop receive traffic lifecycle: @@ -137,3 +142,11 @@ spec: secret: secretName: encryption-keys {{- end }} + {{- if .Values.featureFlags.workspacesMultiRegionEnabled }} + - name: multi-region-config + secret: + secretName: {{ .Values.multiRegion.config.secretName }} + items: + - key: {{ .Values.multiRegion.config.secretKey }} + path: "/" + {{- end }} diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index bf64262062..60590b3afd 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -74,6 +74,11 @@ "type": "boolean", "description": "High level flag that enables the billing integration", "default": false + }, + "workspacesMultiRegionEnabled": { + "type": "boolean", + "description": "Toggles whether multi-region is available within workspaces. workspaceModuleEnabled must also be enabled.", + "default": false } } }, @@ -523,6 +528,26 @@ } } }, + "multiRegion": { + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "secretName": { + "type": "string", + "description": "If workspacesMultiRegionEnabled is enabled, the server will be deployed in a multi-region configuration based on the values in a secret. This allows the default secret name to be overridden.", + "default": "multi-region-config" + }, + "secretKey": { + "type": "string", + "description": "If workspacesMultiRegionEnabled is enabled, the server will be deployed in a multi-region configuration based on the values in a secret. This allows the default secret key and filename to be overridden.", + "default": "multi-region-config.json" + } + } + } + } + }, "server": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 2b8d3e4105..4e97e8019b 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -51,6 +51,8 @@ featureFlags: gatekeeperModuleEnabled: false ## @param featureFlags.billingIntegrationEnabled High level flag that enables the billing integration billingIntegrationEnabled: false + ## @param featureFlags.workspacesMultiRegionEnabled Toggles whether multi-region is available within workspaces. workspaceModuleEnabled must also be enabled. + workspacesMultiRegionEnabled: false analytics: ## @param analytics.enabled Enable or disable analytics @@ -403,6 +405,13 @@ openTelemetry: ## value: '' +multiRegion: + config: + ## @param multiRegion.config.secretName If workspacesMultiRegionEnabled is enabled, the server will be deployed in a multi-region configuration based on the values in a secret. This allows the default secret name to be overridden. + secretName: 'multi-region-config' + ## @param multiRegion.config.secretKey If workspacesMultiRegionEnabled is enabled, the server will be deployed in a multi-region configuration based on the values in a secret. This allows the default secret key and filename to be overridden. + secretKey: 'multi-region-config.json' + ## @section Server ## @descriptionStart ## Defines parameters related to the backend server component of Speckle.