diff --git a/feature_flags.md b/feature_flags.md new file mode 100644 index 0000000000..d7f5d737c3 --- /dev/null +++ b/feature_flags.md @@ -0,0 +1,88 @@ +# Feature flags in the web stack + +This document describes the recommended way to use of feature flags in our web stack. + +## Flag management + +At the moment for managing feature flags, we're relying on good old environment variables. +Currently we do not have a need to use an external flag managing service, but that might change in the future. + +## Usage + +The Speckle shared package exposes a `FeatureFlags` object, that contains all the available flags. + +> ⚠️ Warning +> The feature flag mechanism doesn't work on the old frontend stack the same way. +> If you still need some of this functionality, the backend `serverInfo` graphql query is probably the best place to expose the value at. + +### Backend, script usage + +For any usecase that is not Nuxt based, the code below is the preferred way of using feature flags. + +```typescript +import { Environment } from '@speckle/shared' + +if (Environment.getFeatureFlags().ENABLE_AUTOMATE_MODULE) + console.log("Hurray I'm enabled") +``` + +### Frontend usage + +For our Nuxt based frontend we hook into Nuxt's config module to expose the feature flags in both SSR and frontend context. +So using the feature flag is the same as using any nuxt public runtime config value. + +```typescript +const config = useRuntimeConfig() + +if (config.public.ENABLE_AUTOMATE_MODULE) console.log("Hurray I'm enabled") +``` + +## Definition + +The `@speckle/shared` package is the place where the common implementation of the feature flags is declared. +To add a new feature flag, you need to modify the `featureFlagSchema` zod definition in `./src/environment/index.ts`. + +> 📣 Important +> +> Always add a default value, most probably `false` to the flag. +> This will ensure that the feature you areKeep in mind, in order to support adding doesn't automatically roll out to all our deployments + +Once the flag is added to the zod schema, the flag is ready to be used in our apps. +To enable the specific feature, please add an environment variable to the `.env` file of the component. + +> Note +> +> Since `znv` uses 1-1 name matching from the environment variables, we prefer using `MACRO_CASE` names. + +## Deployment + +With the use of `znv` we are directly parsing all environment variables into feature flags. So in general, all feature flags are just environment variables. We need to supply them to the application runtimes where they are needed. + +### Docker compose + +This is less relevant nowadays, but practically it is a copy pasta exercise, each container definition declares env vars. + +### Helm chart + +Helm charts allow configurations via the chart `values.yaml` for this purpose (intentionally omitting secrets). The chart values file defines a `featureFlags` object, that should be extended with the newly added flag. + +```yaml +## @section Feature flags +## @descriptionStart +## This object is a central location to define feature flags for the whole chart. +## @descriptionEnd +featureFlags: + ## @param enableAutomateModule High level flag fully toggles the integrated automate module + enableAutomateModule: false +``` + +To expose the flag to specific deployments, we need to add the value into each deployment's env array like so + +```yaml +# ... +env: + - name: ENABLE_AUTOMATE_MODULE + // prettier-ignore + value: {{ .Values.featureFlags.enableAutomateModule | quote }} +# ... +``` diff --git a/packages/frontend-2/composables/globals.ts b/packages/frontend-2/composables/globals.ts index 41d0b20e07..b930e0860d 100644 --- a/packages/frontend-2/composables/globals.ts +++ b/packages/frontend-2/composables/globals.ts @@ -3,10 +3,10 @@ import { useGlobalToast } from '~/lib/common/composables/toast' export const useIsAutomateModuleEnabled = () => { const { - public: { enableAutomateModule } + public: { ENABLE_AUTOMATE_MODULE } } = useRuntimeConfig() - return ref(enableAutomateModule) + return ref(ENABLE_AUTOMATE_MODULE) } export { useGlobalToast, useActiveUser } diff --git a/packages/frontend-2/nuxt.config.ts b/packages/frontend-2/nuxt.config.ts index 259f30bda1..b0015ed87b 100644 --- a/packages/frontend-2/nuxt.config.ts +++ b/packages/frontend-2/nuxt.config.ts @@ -3,6 +3,7 @@ import { withoutLeadingSlash } from 'ufo' import { sanitizeFilePath } from 'mlly' import { filename } from 'pathe/utils' import legacy from '@vitejs/plugin-legacy' +import { Environment } from '@speckle/shared' // Copied out from nuxt vite-builder source to correctly build output chunk/entry/asset/etc file names const buildOutputFileName = (chunkName: string) => @@ -16,6 +17,8 @@ const { NUXT_PUBLIC_LOG_PRETTY = false } = process.env +const featureFlags = Environment.getFeatureFlags() + const isLogPretty = ['1', 'true', true, 1].includes(NUXT_PUBLIC_LOG_PRETTY) // https://v3.nuxtjs.org/api/configuration/nuxt.config @@ -68,7 +71,7 @@ export default defineNuxtConfig({ datadogSite: '', datadogService: '', datadogEnv: '', - enableAutomateModule: false + ...featureFlags } }, diff --git a/packages/server/modules/core/index.js b/packages/server/modules/core/index.js index 4a873ae5bb..f771027449 100644 --- a/packages/server/modules/core/index.js +++ b/packages/server/modules/core/index.js @@ -19,7 +19,7 @@ exports.init = async (app) => { require('./rest/diffUpload')(app) require('./rest/diffDownload')(app) - // Register core-based scoeps + // Register core-based scopes const scopes = require('./scopes.js') for (const scope of scopes) { await registerOrUpdateScope(scope) diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 24940c84d0..96fafba012 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -1,7 +1,5 @@ import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' import { trimEnd } from 'lodash' -import { parseEnv } from 'znv' -import { z } from 'zod' export function isTestEnv() { return process.env.NODE_ENV === 'test' @@ -274,6 +272,3 @@ export function delayGraphqlResponsesBy() { if (!isDevEnv()) return 0 return getIntFromEnv('DELAY_GQL_RESPONSES_BY', '0') } -export const { ENABLE_AUTOMATE_MODULE } = parseEnv(process.env, { - ENABLE_AUTOMATE_MODULE: z.boolean().default(false) -}) diff --git a/packages/server/package.json b/packages/server/package.json index 37dddf9a39..87afff9425 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -102,7 +102,6 @@ "undici": "^5.28.3", "verror": "^1.10.1", "xml-escape": "^1.1.0", - "znv": "^0.4.0", "zod": "^3.22.4", "zod-validation-error": "^1.5.0", "zxcvbn": "^4.4.2" diff --git a/packages/shared/package.json b/packages/shared/package.json index 22f68f5ea0..0d1a2f5919 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -34,7 +34,9 @@ "dependencies": { "lodash": "^4.17.0", "lodash-es": "^4.17.21", - "type-fest": "^3.11.1" + "type-fest": "^3.11.1", + "znv": "^0.4.0", + "zod": "^3.22.4" }, "peerDependencies": { "@tiptap/core": "^2.0.0-beta.176", diff --git a/packages/shared/src/environment/index.ts b/packages/shared/src/environment/index.ts new file mode 100644 index 0000000000..b66bd6c33d --- /dev/null +++ b/packages/shared/src/environment/index.ts @@ -0,0 +1,17 @@ +import { parseEnv } from 'znv' +import { z } from 'zod' + +const featureFlagSchema = z.object({ + ENABLE_AUTOMATE_MODULE: z.boolean().default(false) +}) + +function parseFeatureFlags() { + return parseEnv(process.env, featureFlagSchema.shape) +} + +let parsedFlags: ReturnType | undefined + +export function getFeatureFlags() { + if (!parsedFlags) parsedFlags = parseFeatureFlags() + return parsedFlags +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 66b26edb0f..22f9046611 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,5 @@ export * as RichTextEditor from './rich-text-editor' export * as Observability from './observability' export * as SpeckleViewer from './viewer' +export * as Environment from './environment' export * from './core' diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index ec8bba4ac5..7c4c2aa4ba 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -126,6 +126,9 @@ spec: value: {{ .Values.analytics.datadog_env | quote }} {{- end }} + - name: ENABLE_AUTOMATE_MODULE + value: {{ .Values.featureFlags.enableAutomateModule | quote }} + priorityClassName: high-priority {{- if .Values.frontend_2.affinity }} affinity: {{- include "speckle.renderTpl" (dict "value" .Values.frontend_2.affinity "context" $) | nindent 8 }} diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index 3d1e02a33d..d974e1c921 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -111,6 +111,9 @@ spec: - name: ENABLE_FE2_MESSAGING value: {{ .Values.server.enableFe2Messaging | quote }} + - name: ENABLE_AUTOMATE_MODULE + value: {{ .Values.featureFlags.enableAutomateModule | quote }} + - name: ONBOARDING_STREAM_URL value: {{ .Values.server.onboarding.stream_url }} - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index bd1672c952..7737150054 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -32,6 +32,16 @@ "description": "The name of the ClusterIssuer kubernetes resource that provides the SSL Certificate", "default": "letsencrypt-staging" }, + "featureFlags": { + "type": "object", + "properties": { + "enableAutomateModule": { + "type": "boolean", + "description": "High level flag fully toggles the integrated automate module", + "default": false + } + } + }, "analytics": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index cefb616211..363523cb0c 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -30,6 +30,14 @@ tlsRejectUnauthorized: '1' ## cert_manager_issuer: letsencrypt-staging +## @section Feature flags +## @descriptionStart +## This object is a central location to define feature flags for the whole chart. +## @descriptionEnd +featureFlags: + ## @param featureFlags.enableAutomateModule High level flag fully toggles the integrated automate module + enableAutomateModule: false + analytics: ## @param analytics.enabled Enable or disable analytics enabled: true diff --git a/yarn.lock b/yarn.lock index 1590228718..71b32df3f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14361,7 +14361,6 @@ __metadata: ws: ^7.5.7 xml-escape: ^1.1.0 yargs: ^17.3.1 - znv: ^0.4.0 zod: ^3.22.4 zod-validation-error: ^1.5.0 zxcvbn: ^4.4.2 @@ -14402,6 +14401,8 @@ __metadata: rollup-plugin-typescript2: ^0.34.1 type-fest: ^3.11.1 typescript: ^4.5.4 + znv: ^0.4.0 + zod: ^3.22.4 peerDependencies: "@tiptap/core": ^2.0.0-beta.176 pino: ^8.7.0