|
| 1 | +# Configuration state and flows |
| 2 | + |
| 3 | +## Short hints |
| 4 | + |
| 5 | +Every environment variable that is used or refactored, should be added to ./config/default.schema.json.. |
| 6 | + |
| 7 | +We want to avoid any process.env.XXX call inside of the code. |
| 8 | +> It is a syncron call that stops the node process for a short moment and other async tasks can not be executed. |
| 9 | +> It is not documentated in a single place, or has a description to understand for what it is used. |
| 10 | +> Any validation and used default value is set on the called placed and is hard to detected by deploying and reuse on other places. |
| 11 | +
|
| 12 | +## History and legacy tech stack |
| 13 | + |
| 14 | +### FeatherJS and Express |
| 15 | + |
| 16 | +Our legacy stack uses featherJS <https://docs.feathersjs.com/api/configuration.html>. |
| 17 | +It is embedded in the express application. |
| 18 | + |
| 19 | +Express and featherJS use the environment variable as the default behavior. |
| 20 | +NODE_ENV=production|test|default |
| 21 | +It is extended over featherJS and directly matched to |
| 22 | +./config/ |
| 23 | + default.json |
| 24 | + test.json |
| 25 | + prodcution.json (deprecated) |
| 26 | + development.json (matching added by us) |
| 27 | + |
| 28 | +Default is (like it is expected) as default and is overwritten by added environment variables from test.json, or production.json. |
| 29 | + |
| 30 | +## Current and NestJS solutions |
| 31 | + |
| 32 | +### @hpi-schul-cloud/commons |
| 33 | + |
| 34 | +``` javascript |
| 35 | + const { Configuration } = require('@hpi-schul-cloud/commons'); |
| 36 | + |
| 37 | + const url = Configuration.get('FILES_STORAGE__SERVICE_BASE_URL'); |
| 38 | +``` |
| 39 | + |
| 40 | +It is used for parsing any environment value that is added in ./config/default.schema.json. |
| 41 | +It is overridden with values in default.json. |
| 42 | +The default.json is also overridden by development.json (NODE_ENV==='default'), or test.json. (NODE_ENV==='test'). |
| 43 | + |
| 44 | +For legacy featherJS stack, or legacy client it is the only solution that should be used. |
| 45 | +For newer nestjs stack we use it for parsing values in the right order, but map it to the nestjs based solution. |
| 46 | +> Look to the topic nestjs in this file for more information. |
| 47 | +
|
| 48 | +The vue client we pass this values over an api endpoint. |
| 49 | +> Look to the topic "Passing configuration to vue client" in this file |
| 50 | +
|
| 51 | +``` javascript |
| 52 | + let configBefore; |
| 53 | + |
| 54 | + before(() => { |
| 55 | + configBefore = Configuration.toObject({ plainSecrets: true }); |
| 56 | + |
| 57 | + Configuration.set('ENVIRONMENT_NAME', 'fake.value'); |
| 58 | + }); |
| 59 | + |
| 60 | + after(async () => { |
| 61 | + Configuration.reset(configBefore); |
| 62 | + }); |
| 63 | + |
| 64 | +``` |
| 65 | + |
| 66 | +### ./config/global.js (deprecated) |
| 67 | + |
| 68 | +To collect and cleanup all existing process.env.XXX calls in code, it exists a step, where all environments variables are moved to global.js file. |
| 69 | +This should not be used anymore and cleaned up. |
| 70 | + |
| 71 | +Feel free to move variables to default.schema.json. |
| 72 | + |
| 73 | +### ./config/production.js (deprecated) |
| 74 | + |
| 75 | +This config values should not be used anymore. |
| 76 | + |
| 77 | +The default.schema.json and default.json represent the default values that should be set in all production systems. |
| 78 | +All other production values are added over autodeployment configurations. |
| 79 | + |
| 80 | +### ./config/default.schema.json |
| 81 | + |
| 82 | +We want to move any environment variable to this file for now. |
| 83 | +> Please add a discription and if possible default values to it. |
| 84 | +> Any default values that are set on this files should be for production systems. They can be overridden with autodeployment configurations. |
| 85 | +
|
| 86 | +It make sense to cluster variables with same context. |
| 87 | +Depending on the context, the motivation for clustering variables can vary, please see current usage for further examples. |
| 88 | +For this cases you can add embedded objects. By passing a value to embedded objects, you can write MY_SCOPE_NAME__MY_VARIABLEN_NAME. |
| 89 | +The scope name and value is splitted by double underscore ( _ ). |
| 90 | +Defaults values for embedded objects do not work well. For this we let the default.json stay alive. |
| 91 | + |
| 92 | +### ./config/default.json |
| 93 | + |
| 94 | +It stays alive and is only used for *default values of embedded objects* in default.schema.json |
| 95 | +The values should be the same as in default.schema.json and are added as default values. (means production values) |
| 96 | + |
| 97 | +### ./config/development.json |
| 98 | + |
| 99 | +This file overrides default.json and default.schema.json values. |
| 100 | +It is used for local development. |
| 101 | +For example it increases the timeouts to enable us to debug stuff. |
| 102 | + |
| 103 | +> Please look to "local setups" topic on this page, if you only want to add your personal settings. |
| 104 | +
|
| 105 | +### ./config/test.json |
| 106 | + |
| 107 | +This file overrides default.json and default.schema.json values. |
| 108 | +It is used for test executions. (NODE_ENV==='test') |
| 109 | +For example to reduce log outputs. |
| 110 | +> Please change carefully. It effects all tests in this repository. In best case avoid using it. |
| 111 | +
|
| 112 | +For test we can also use injections of the nestjs configuration module, or service to set values for a special test. |
| 113 | + |
| 114 | +> Please look to featherJS test examples or nestjs test examples in this file for configurations that should only effect single tests. |
| 115 | +
|
| 116 | +### Auto deployment (overriding and setting additional values) |
| 117 | + |
| 118 | +We have 2 sources that can fullfill and add environments. |
| 119 | +One is our auto deployment repository that can set environment values directly over config.jsons. |
| 120 | +The other source fetches secrets for .dev systems from gitHub, or 1password for productions. |
| 121 | + |
| 122 | +We only add values to it if we need them. If we want the default values from default.schema.json, |
| 123 | +on all production like systems (dev, ref, production), they shouldn't be added to configurations in autodeployment. |
| 124 | +> Over this way we can reduce the total amount of environment values in production pods. |
| 125 | +
|
| 126 | +<https://github.com/hpi-schul-cloud/dof_app_deploy/blob/main/ansible/group_vars/all/config.yml> |
| 127 | + |
| 128 | +> For documentation on how it is works, plase look at our confluence. No github documentation exists atm. |
| 129 | +
|
| 130 | +### Nestjs configuration module |
| 131 | + |
| 132 | +#### Setup configuration interfaces |
| 133 | + |
| 134 | +<https://docs.nestjs.com/techniques/configuration> |
| 135 | + |
| 136 | +We implemented a solution that is based on nestjs and combined it with the parsing from the @hpi-schul-cloud/commons of the existing config files. |
| 137 | +In future we want to replace it with a nestjs only solution. |
| 138 | + |
| 139 | +Any module that needs configuration, can define his need by creating a interface file with the schema I*MY_NAME*Config. |
| 140 | +In first step we add this interfaces directly to an app config file which extends ...,I*MY_NAME*Config,...,.. . |
| 141 | +The combined Iconfig interface can be used to initilized the nestjs configuration module. |
| 142 | +The nestjs configuration module is defined globally in the hole app and can be used over injections. |
| 143 | + |
| 144 | +> We force it this way, so that modules can be defined by their needs. |
| 145 | +> We only have a single point, where all envirements are added to our application. |
| 146 | +> We can easily replace this solution with a nestjs parser instead, of Configuration from @hpi-schul-cloud/commons in future. |
| 147 | +
|
| 148 | +This code shows a minimal flow. |
| 149 | + |
| 150 | +``` javascript |
| 151 | + // needed configuration for a module |
| 152 | + export interface UserConfig { |
| 153 | + AVAILABLE_LANGUAGES: string[]; |
| 154 | + } |
| 155 | + |
| 156 | + // server.config.ts |
| 157 | + export interface ServerConfig extends ICoreModuleConfig, UserConfig, IFilesStorageClientConfig { |
| 158 | + NODE_ENV: string; |
| 159 | + } |
| 160 | + |
| 161 | + // server.module.ts |
| 162 | + import { Module } from '@nestjs/common'; |
| 163 | + import { ConfigModule } from '@nestjs/config'; |
| 164 | + import serverConfig from './server.config'; |
| 165 | + import { createConfigModuleOptions } from '@shared/common/config-module-options'; |
| 166 | + |
| 167 | + |
| 168 | + const serverModules = [ |
| 169 | + ConfigModule.forRoot(createConfigModuleOptions(serverConfig)) |
| 170 | + ] |
| 171 | + |
| 172 | + @Module({ |
| 173 | + imports: [...serverModules], |
| 174 | + }) |
| 175 | + export class ServerModule {} |
| 176 | + |
| 177 | + //use via injections |
| 178 | + import { ConfigService } from '@nestjs/config'; |
| 179 | + import { UserConfig } from '../interfaces'; |
| 180 | + |
| 181 | + constructor(private readonly configService: ConfigService<UserConfig, true>){} |
| 182 | + |
| 183 | + this.configService.get<string[]>('AVAILABLE_LANGUAGES'); |
| 184 | + |
| 185 | + //use in modules construction |
| 186 | + import { ConfigService } from '@nestjs/config'; |
| 187 | + import { Configuration, FileApi } from './filesStorageApi/v3'; |
| 188 | + |
| 189 | + @Module({ |
| 190 | + providers: [ |
| 191 | + { |
| 192 | + provide: 'Module', |
| 193 | + useFactory: (configService: ConfigService<IFilesStorageClientConfig, true>) => { |
| 194 | + const timeout = configService.get<number>('INCOMING_REQUEST_TIMEOUT'); |
| 195 | + |
| 196 | + const options = new Configuration({ |
| 197 | + baseOptions: { timeout }, |
| 198 | + }); |
| 199 | + |
| 200 | + return new FileApi(options, baseUrl + apiUri); |
| 201 | + }, |
| 202 | + inject: [ConfigService], |
| 203 | + }) |
| 204 | + export class Module {} |
| 205 | + |
| 206 | +``` |
| 207 | +
|
| 208 | +Mocking in unit and integration tests. |
| 209 | +
|
| 210 | +``` javascript |
| 211 | + import { Test, TestingModule } from '@nestjs/testing'; |
| 212 | + import { createMock, DeepMocked } from '@golevelup/ts-jest'; |
| 213 | + |
| 214 | + describe('XXX', () => { |
| 215 | + let config: DeepMocked<ConfigService>; |
| 216 | + let app: INestApplication; |
| 217 | + |
| 218 | + beforeAll(async () => { |
| 219 | + const module: TestingModule = await Test.createTestingModule({ |
| 220 | + providers: [ |
| 221 | + { |
| 222 | + provide: ConfigService, |
| 223 | + useValue: createMock<ConfigService>(), |
| 224 | + }, |
| 225 | + ], |
| 226 | + }).compile(); |
| 227 | + |
| 228 | + config = module.get(ConfigService); |
| 229 | + app = module.createNestApplication(); |
| 230 | + }); |
| 231 | + |
| 232 | + const setup = () => { |
| 233 | + config.get.mockReturnValueOnce(['value']); |
| 234 | + } |
| 235 | + |
| 236 | + it('XXX', () => { |
| 237 | + setup(); |
| 238 | + }) |
| 239 | + |
| 240 | + afterAll(async () => { |
| 241 | + config.get.mockRestore(); |
| 242 | + await app.close(); |
| 243 | + }) |
| 244 | + |
| 245 | + }); |
| 246 | + |
| 247 | +``` |
| 248 | +
|
| 249 | +Mocking in api tests. |
| 250 | +
|
| 251 | +``` javascript |
| 252 | + import { ServerTestModule } from '@modules/server/server.app.module'; |
| 253 | + import { serverConfig } from '@modules/server/server.config'; |
| 254 | + import { TestConfigHelper } from '@testing/test-config.helper'; |
| 255 | + |
| 256 | + describe('...Controller (API)', () => { |
| 257 | + let app: INestApplication; |
| 258 | + let em: EntityManager; |
| 259 | + let testApiClient: TestApiClient; |
| 260 | + let testConfigHelper: TestConfigHelper<ServerConfig>; |
| 261 | + |
| 262 | + beforeAll(async () => { |
| 263 | + const module: TestingModule = await Test.createTestingModule({ |
| 264 | + imports: [ServerTestModule], |
| 265 | + }).compile(); |
| 266 | + |
| 267 | + app = module.createNestApplication(); |
| 268 | + await app.init(); |
| 269 | + em = app.get(EntityManager); |
| 270 | + testApiClient = new TestApiClient(app, '...path...'); |
| 271 | + |
| 272 | + const config = serverConfig(); |
| 273 | + testConfigHelper = new TestConfigHelper(config); |
| 274 | + }); |
| 275 | + |
| 276 | + afterEach(() => { |
| 277 | + testConfigHelper.reset(); |
| 278 | + }); |
| 279 | + |
| 280 | + describe('PATCH /:id', () => { |
| 281 | + describe('when feature X is activated', () => { |
| 282 | + const setup = async () => { |
| 283 | + testConfigHelper.set('FEATURE_X', true); |
| 284 | + }; |
| 285 | + |
| 286 | + it('should ...', () => { |
| 287 | + await setup(); |
| 288 | + }); |
| 289 | + |
| 290 | + it('should ...', () => { |
| 291 | + await setup(); |
| 292 | + }); |
| 293 | + |
| 294 | + it('should ...', () => { |
| 295 | + await setup(); |
| 296 | + }); |
| 297 | + }); |
| 298 | + }); |
| 299 | + }); |
| 300 | +``` |
| 301 | +
|
| 302 | +### Special cases in nestjs |
| 303 | +
|
| 304 | +If we want to use values in decorators, we can not use the nestjs configuration module. |
| 305 | +The parsing of decorators in files starts first and after it the injections are solved. |
| 306 | +
|
| 307 | +It is possible to import the config file of the application directly and use the values. |
| 308 | +
|
| 309 | +``` javascript |
| 310 | + import serverConfig from '@modules/server/server.config'; |
| 311 | + |
| 312 | + @RequestTimeout(serverConfig().INCOMING_REQUEST_TIMEOUT_COPY_API) |
| 313 | +``` |
| 314 | +
|
| 315 | +## Passing configuration to vue client |
| 316 | +
|
| 317 | +It exists an endpoint that exposes environment values. |
| 318 | +This values are used by the vue client. |
| 319 | +The solution is the only existing way how environments should be passed to the new vue client. |
| 320 | +
|
| 321 | +Please be careful! Secrets should be never exposed! |
| 322 | +They are readable in browser and request response. |
| 323 | +
|
| 324 | +<https://github.com/hpi-schul-cloud/schulcloud-server/blob/main/apps/server/src/modules/server/api/server-config.controller.ts> |
| 325 | +
|
| 326 | +<http://{{HOST}}:{{PORT}}/api/v3/config/public> |
| 327 | +<http://{{HOST}}:{{PORT}}/api/v3/files/config/public> |
| 328 | +
|
| 329 | +
|
| 330 | +## Desired changes in future |
| 331 | +
|
| 332 | +We want to remove the different config files and the Configuration from @hpi-schul-cloud/commons package. |
| 333 | +We want to use the nestjs solutions over parsing configuration values for different states. |
| 334 | +This results in a new format for the default.schema.json file. |
| 335 | +
|
| 336 | +We also want to put more environment values to database, to enable us to switching it without redeploys over our dashboard. |
| 337 | +
|
| 338 | +## Local setups |
| 339 | +
|
| 340 | +You can use the .env convention to set settings that only work for you locally. |
| 341 | +For temporary checks you can add environments to your terminal based on the solution of your IOS. |
| 342 | +You can also add environments for debugging, or if you run your applications over .vscode/lunch.json with: |
| 343 | +
|
| 344 | +``` json |
| 345 | + "env": { |
| 346 | + "NODE_ENV": "test" |
| 347 | + } |
| 348 | +``` |
0 commit comments