Skip to content

Commit 8e7f9c8

Browse files
authored
BC-9076 Move documentation (#53)
1 parent 1e24e3d commit 8e7f9c8

File tree

2 files changed

+456
-0
lines changed

2 files changed

+456
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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

Comments
 (0)