diff --git a/superset-websocket/README.md b/superset-websocket/README.md index 5beff6b7770e2..9a61d70025a0a 100644 --- a/superset-websocket/README.md +++ b/superset-websocket/README.md @@ -64,6 +64,8 @@ npm install Copy `config.example.json` to `config.json` and adjust the values for your environment. +Configuration via environment variables is also supported which can be helpful in certain contexts, e.g., deployment. `src/config.ts` can be consulted to see the full list of supported values. + ## Superset Configuration Configure the Superset Flask app to enable global async queries (in `superset_config.py`): diff --git a/superset-websocket/spec/config.spec.ts b/superset-websocket/spec/config.spec.ts new file mode 100644 index 0000000000000..588a743e751c9 --- /dev/null +++ b/superset-websocket/spec/config.spec.ts @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +import { buildConfig } from '../src/config'; + +describe('buildConfig', () => { + test('builds configuration and applies env overrides', () => { + let config = buildConfig(); + + expect(config.jwtSecret).toEqual( + 'test123-test123-test123-test123-test123-test123-test123', + ); + expect(config.redis.host).toEqual('127.0.0.1'); + expect(config.redis.port).toEqual(6379); + expect(config.redis.password).toEqual(''); + expect(config.redis.db).toEqual(10); + expect(config.redis.ssl).toEqual(false); + expect(config.statsd.host).toEqual('127.0.0.1'); + expect(config.statsd.port).toEqual(8125); + expect(config.statsd.globalTags).toEqual([]); + + process.env.JWT_SECRET = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + process.env.REDIS_HOST = '10.10.10.10'; + process.env.REDIS_PORT = '6380'; + process.env.REDIS_PASSWORD = 'admin'; + process.env.REDIS_DB = '4'; + process.env.REDIS_SSL = 'true'; + process.env.STATSD_HOST = '15.15.15.15'; + process.env.STATSD_PORT = '8000'; + process.env.STATSD_GLOBAL_TAGS = 'tag-1,tag-2'; + + config = buildConfig(); + + expect(config.jwtSecret).toEqual('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + expect(config.redis.host).toEqual('10.10.10.10'); + expect(config.redis.port).toEqual(6380); + expect(config.redis.password).toEqual('admin'); + expect(config.redis.db).toEqual(4); + expect(config.redis.ssl).toEqual(true); + expect(config.statsd.host).toEqual('15.15.15.15'); + expect(config.statsd.port).toEqual(8000); + expect(config.statsd.globalTags).toEqual(['tag-1', 'tag-2']); + + delete process.env.JWT_SECRET; + delete process.env.REDIS_HOST; + delete process.env.REDIS_PORT; + delete process.env.REDIS_PASSWORD; + delete process.env.REDIS_DB; + delete process.env.REDIS_SSL; + delete process.env.STATSD_HOST; + delete process.env.STATSD_PORT; + delete process.env.STATSD_GLOBAL_TAGS; + }); +}); diff --git a/superset-websocket/src/config.ts b/superset-websocket/src/config.ts new file mode 100644 index 0000000000000..203b9b5e784c1 --- /dev/null +++ b/superset-websocket/src/config.ts @@ -0,0 +1,133 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +type ConfigType = { + port: number; + logLevel: string; + logToFile: boolean; + logFilename: string; + statsd: { + host: string; + port: number; + globalTags: Array; + }; + redis: { + port: number; + host: string; + password: string; + db: number; + ssl: boolean; + }; + redisStreamPrefix: string; + redisStreamReadCount: number; + redisStreamReadBlockMs: number; + jwtSecret: string; + jwtCookieName: string; + socketResponseTimeoutMs: number; + pingSocketsIntervalMs: number; + gcChannelsIntervalMs: number; +}; + +function defaultConfig(): ConfigType { + return { + port: 8080, + logLevel: 'info', + logToFile: false, + logFilename: 'app.log', + redisStreamPrefix: 'async-events-', + redisStreamReadCount: 100, + redisStreamReadBlockMs: 5000, + jwtSecret: '', + jwtCookieName: 'async-token', + socketResponseTimeoutMs: 60 * 1000, + pingSocketsIntervalMs: 20 * 1000, + gcChannelsIntervalMs: 120 * 1000, + statsd: { + host: '127.0.0.1', + port: 8125, + globalTags: [], + }, + redis: { + host: '127.0.0.1', + port: 6379, + password: '', + db: 0, + ssl: false, + }, + }; +} + +function configFromFile(): Partial { + const isTest = process.env.NODE_ENV === 'test'; + const configFile = isTest ? '../config.test.json' : '../config.json'; + try { + return require(configFile); + } catch (err) { + console.warn('config.json file not found'); + return {}; + } +} + +const isPresent = (s: string) => /\S+/.test(s); +const toNumber = Number; +const toBoolean = (s: string) => s.toLowerCase() === 'true'; +const toStringArray = (s: string) => s.split(','); + +function applyEnvOverrides(config: ConfigType): ConfigType { + const envVarConfigSetter: { [envVar: string]: (val: string) => void } = { + PORT: val => (config.port = toNumber(val)), + LOG_LEVEL: val => (config.logLevel = val), + LOG_TO_FILE: val => (config.logToFile = toBoolean(val)), + LOG_FILENAME: val => (config.logFilename = val), + REDIS_STREAM_PREFIX: val => (config.redisStreamPrefix = val), + REDIS_STREAM_READ_COUNT: val => + (config.redisStreamReadCount = toNumber(val)), + REDIS_STREAM_READ_BLOCK_MS: val => + (config.redisStreamReadBlockMs = toNumber(val)), + JWT_SECRET: val => (config.jwtSecret = val), + JWT_COOKIE_NAME: val => (config.jwtCookieName = val), + SOCKET_RESPONSE_TIMEOUT_MS: val => + (config.socketResponseTimeoutMs = toNumber(val)), + PING_SOCKETS_INTERVAL_MS: val => + (config.pingSocketsIntervalMs = toNumber(val)), + GC_CHANNELS_INTERVAL_MS: val => + (config.gcChannelsIntervalMs = toNumber(val)), + REDIS_HOST: val => (config.redis.host = val), + REDIS_PORT: val => (config.redis.port = toNumber(val)), + REDIS_PASSWORD: val => (config.redis.password = val), + REDIS_DB: val => (config.redis.db = toNumber(val)), + REDIS_SSL: val => (config.redis.ssl = toBoolean(val)), + STATSD_HOST: val => (config.statsd.host = val), + STATSD_PORT: val => (config.statsd.port = toNumber(val)), + STATSD_GLOBAL_TAGS: val => (config.statsd.globalTags = toStringArray(val)), + }; + + for (const [envVar, set] of Object.entries(envVarConfigSetter)) { + const envValue = process.env[envVar]; + if (envValue && isPresent(envValue)) { + set(envValue); + } + } + + return config; +} + +export function buildConfig(): ConfigType { + const config = Object.assign(defaultConfig(), configFromFile()); + return applyEnvOverrides(config); +} diff --git a/superset-websocket/src/index.ts b/superset-websocket/src/index.ts index dfb54ad634fb8..d5b737f2aef5b 100644 --- a/superset-websocket/src/index.ts +++ b/superset-websocket/src/index.ts @@ -26,6 +26,7 @@ import Redis from 'ioredis'; import StatsD from 'hot-shots'; import { createLogger } from './logger'; +import { buildConfig } from './config'; export type StreamResult = [ recordId: string, @@ -79,45 +80,9 @@ interface ChannelValue { const environment = process.env.NODE_ENV; -// default options -export const opts = { - port: 8080, - logLevel: 'info', - logToFile: false, - logFilename: 'app.log', - statsd: { - host: '127.0.0.1', - port: 8125, - globalTags: [], - }, - redis: { - port: 6379, - host: '127.0.0.1', - password: '', - db: 0, - ssl: false, - }, - redisStreamPrefix: 'async-events-', - redisStreamReadCount: 100, - redisStreamReadBlockMs: 5000, - jwtSecret: '', - jwtCookieName: 'async-token', - socketResponseTimeoutMs: 60 * 1000, - pingSocketsIntervalMs: 20 * 1000, - gcChannelsIntervalMs: 120 * 1000, -}; - const startServer = process.argv[2] === 'start'; -const configFile = - environment === 'test' ? '../config.test.json' : '../config.json'; -let config = {}; -try { - config = require(configFile); -} catch (err) { - console.error('config.json not found, using defaults'); -} -// apply config overrides -Object.assign(opts, config); + +export const opts = buildConfig(); // init logger const logger = createLogger({