Skip to content

Commit f7d221e

Browse files
committed
feat: add atomic config update endpoint
1 parent cd89575 commit f7d221e

File tree

16 files changed

+782
-75
lines changed

16 files changed

+782
-75
lines changed

docs/registry.md

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ Currently, Registry supports authentication only. All authenticated entities wil
1010

1111
The following authentication providers are supported:
1212

13-
- **OpenID Connect**. Turned **off** by default.
14-
- **Locally configured login/password**. Default credentials: `root` / `pwd`.
15-
- **Locally configured Bearer token** for API machine-to-machine access. Default credentials: `Bearer cm9vdF9hcGlfdG9rZW4=:dG9rZW5fc2VjcmV0` or `Bearer root_api_token:token_secret` after base64 decoding.
13+
- **OpenID Connect**. Turned **off** by default.
14+
- **Locally configured login/password**. Default credentials: `root` / `pwd`.
15+
- **Locally configured Bearer token** for API machine-to-machine access. Default credentials: `Bearer cm9vdF9hcGlfdG9rZW4=:dG9rZW5fc2VjcmV0` or `Bearer root_api_token:token_secret` after base64 decoding.
1616

1717
You can change default credentials via Registry UI, in the `Auth entities` page, or via API.
1818

@@ -26,16 +26,16 @@ To configure OpenID:
2626

2727
Sample configuration (values are JSON-encoded):
2828

29-
| key | value |
30-
|--------------------------------|-----------------------------------------|
31-
| **`baseUrl`** | `"https://ilc-registry.example.com/"` |
32-
| **`auth.openid.enabled`** | `true` |
33-
| **`auth.openid.discoveryUrl`** | `"https://adfs.example.com/adfs/"` |
34-
| **`auth.openid.clientId`** | `"ba34c345-e543-6554-b0be-3e1097ddd32d"`|
35-
| **`auth.openid.clientSecret`** | `"XXXXXX"` |
29+
| key | value |
30+
| ------------------------------ | ---------------------------------------- |
31+
| **`baseUrl`** | `"https://ilc-registry.example.com/"` |
32+
| **`auth.openid.enabled`** | `true` |
33+
| **`auth.openid.discoveryUrl`** | `"https://adfs.example.com/adfs/"` |
34+
| **`auth.openid.clientId`** | `"ba34c345-e543-6554-b0be-3e1097ddd32d"` |
35+
| **`auth.openid.clientSecret`** | `"XXXXXX"` |
3636

3737
!!! warning ""
38-
OpenID Connect returnURL should be specified at provider in the following format: `{baseUrl}/auth/openid/return`
38+
OpenID Connect returnURL should be specified at provider in the following format: `{baseUrl}/auth/openid/return`
3939

4040
## User Interface
4141

@@ -51,25 +51,85 @@ to see how UI communicates with API. You can also explore code starting from the
5151
It is a common practice to store JS/CSS files of the micro frontend apps at CDN using unique URLs. For example,
5252
`https://site.com/layoutfragments-ui/app.80de7d4e36eae32662d2.js`.
5353

54-
By following this this approach, you need to update
54+
By following this this approach, you need to update
5555
links to the JS/CSS bundles in the Registry after each deployment.
5656

5757
To do this, there are the following options (at least):
5858

59-
- Manually via UI (_not recommended_)
60-
- Using Registry API (see [API](#api) section above)
61-
- **Using App Assets discovery mechanism**
59+
- Manually via UI (_not recommended_)
60+
- Using Registry API (see [API](#api) section above)
61+
- **Using App Assets discovery mechanism**
6262

6363
When registering micro frontend in the ILC Registry, it is possible to set a file for the "Assets discovery url" that will be periodically fetched
6464
by the Registry. The idea is that this file will contain actual references to JS/CSS bundles and be updated on CDN **right after** every deployment.
6565

6666
!!! example "`https://site.com/layoutfragments-ui/assets-discovery.json`"
67-
```json
67+
`json
6868
{
6969
"spaBundle": "https://site.com/layoutfragments-ui/app.80de7d4e36eae32662d2.js",
7070
"cssBundle": "./app.81340a47f3122508fd76.css", // It is possible to use relative links that will be resolved against the manifest URL
7171
"dependencies": {
7272
"react": "https://unpkg.com/react@16.13.1/umd/react.production.min.js"
7373
}
7474
}
75-
```
75+
`
76+
77+
## Batch Atomic Config Update
78+
79+
Atomic config updates can be performed using the `PUT /api/v1/config` endpoint. Resource data will be replaced completely (except few keys, such as `adminNotes` or `l10nManifest`). If the `namespace` key is specified in app or route, all resources that are not listed in the config payload with same namespace value will be automatically removed. Below is an example of a JSON request body:
80+
81+
```json
82+
{
83+
"apps": [
84+
{
85+
"name": "Application name",
86+
"assetsDiscoveryUrl": "Url to fetch assets location",
87+
"ssr": {
88+
"src": "SSR url",
89+
"timeout": 3000
90+
},
91+
"props": {
92+
"key": "value"
93+
},
94+
"ssrProps": {
95+
"key": "value"
96+
},
97+
"configSelector": ["sharedPropertiesKey"],
98+
"kind": "primary",
99+
"discoveryMetadata": {},
100+
"namespace": "Application namespace, used to as a part of unique identifier (application name and namespace). All apps, that are not listed in the config payload under this namespace will be automatically removed."
101+
}
102+
],
103+
"routes": [
104+
{
105+
"route": "/route/*",
106+
"slots": {
107+
"body": {
108+
"appName": "Application name",
109+
"props": {
110+
"key": "value"
111+
}
112+
}
113+
},
114+
"namespace": "Route namespace, used to as a part of unique identifier (route value, domainId and namespace). All routes, that are not listed in the config payload under this namespace will be automatically removed."
115+
}
116+
],
117+
"sharedLibs": [
118+
{
119+
"name": "Library name",
120+
"assetsDiscoveryUrl": "Url to fetch assets location"
121+
}
122+
]
123+
}
124+
```
125+
126+
There might be cases where you want to test your configuration before deploying services or simply perform a dry run of the config update action. To do this, call the `POST /api/v1/config/validate` endpoint with the same request body as the config update. This API returns a response in the following format:
127+
128+
```json
129+
{
130+
"valid": false,
131+
"details": "Error details"
132+
}
133+
```
134+
135+
Note, that this API is available only if using PostgreSQL database.

registry/server/appRoutes/routes/RoutesService.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,51 @@ export class RoutesService {
4545
});
4646
}
4747

48-
public async upsert(params: unknown, user: User, trxProvider: Knex.TransactionProvider) {
48+
/**
49+
* @returns routeId
50+
*/
51+
public async upsert(params: unknown, user: User, trxProvider: Knex.TransactionProvider): Promise<AppRoute> {
4952
const { slots, ...appRoute } = await appRouteSchema.validateAsync(params, {
50-
noDefaults: true,
53+
noDefaults: false,
5154
externals: false,
5255
});
5356

57+
let savedAppRouteId;
5458
await this.db.versioning(user, { type: EntityTypes.routes, trxProvider }, async (trx) => {
5559
const result = await this.db(Tables.Routes)
5660
.insert(prepareAppRouteToSave(appRoute), 'id')
5761
.onConflict(this.db.raw('("orderPos", "domainIdIdxble", namespace) WHERE namespace IS NOT NULL'))
5862
.merge()
5963
.transacting(trx);
60-
const savedAppRouteId = extractInsertedId(result as { id: number }[]);
64+
savedAppRouteId = extractInsertedId(result as { id: number }[]);
6165
await this.db(Tables.RouteSlots).where('routeId', savedAppRouteId).delete().transacting(trx);
6266
await this.db
6367
.batchInsert(Tables.RouteSlots, prepareAppRouteSlotsToSave(slots, savedAppRouteId))
6468
.transacting(trx);
65-
return extractInsertedId(result as { id: number }[]);
69+
return savedAppRouteId;
6670
});
71+
return { ...appRoute, id: savedAppRouteId };
72+
}
73+
74+
public async deleteByNamespace(
75+
namespace: string,
76+
exclude: number[],
77+
{ user, trxProvider }: { user: User; trxProvider: Knex.TransactionProvider },
78+
) {
79+
const trx = await trxProvider?.();
80+
const routeIdsToDelete = await this.db(Tables.Routes)
81+
.select('id')
82+
.where({ namespace })
83+
.whereNotIn('id', exclude)
84+
.transacting(trx);
85+
86+
await Promise.all(
87+
routeIdsToDelete.map(async (route) => {
88+
await this.db.versioning(user, { type: EntityTypes.routes, id: route.id, trxProvider }, async (trx) => {
89+
await this.db(Tables.Routes).delete().where({ id: route.id }).transacting(trx);
90+
});
91+
}),
92+
);
6793
}
6894

6995
public isOrderPosError(error: any) {

registry/server/appRoutes/routes/deleteAppRoute.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { Request, Response } from 'express';
22
import Joi from 'joi';
33
import * as httpErrors from '../../errorHandler/httpErrors';
44

5-
import db from '../../db';
65
import validateRequestFactory from '../../common/services/validateRequest';
6+
import db from '../../db';
7+
import { Tables } from '../../db/structure';
78
import { appRouteIdSchema } from '../interfaces';
89
import { makeSpecialRoute } from '../services/transformSpecialRoutes';
910

@@ -20,28 +21,20 @@ const validateRequestBeforeDeleteAppRoute = validateRequestFactory([
2021
},
2122
]);
2223

23-
let idDefault404: number | undefined;
24-
2524
const deleteAppRoute = async (req: Request<DeleteAppRouteRequestParams>, res: Response) => {
26-
if (!idDefault404) {
27-
const [default404] = await db
28-
.select()
29-
.from('routes')
30-
.where({ route: makeSpecialRoute('404'), domainId: null });
31-
32-
idDefault404 = default404?.id;
33-
}
25+
const [default404Route] = await db(Tables.Routes)
26+
.select()
27+
.where({ route: makeSpecialRoute('404'), domainId: null });
3428

3529
const appRouteId = req.params.id;
3630

37-
if (idDefault404 === +appRouteId) {
31+
if (default404Route?.id === Number(appRouteId)) {
3832
throw new httpErrors.CustomError({
3933
message: "Default 404 error can't be deleted",
4034
});
4135
}
4236

4337
await db.versioning(req.user, { type: 'routes', id: appRouteId }, async (transaction) => {
44-
await db('route_slots').where('routeId', appRouteId).delete().transacting(transaction);
4538
const count = await db('routes').where('id', appRouteId).delete().transacting(transaction);
4639
if (!count) {
4740
throw new httpErrors.NotFoundError();

registry/server/apps/interfaces/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export interface AppDependencies {
4646
export const appNameSchema = Joi.string().trim().min(1);
4747

4848
const commonApp = {
49-
spaBundle: Joi.string().trim().uri().default(null),
50-
cssBundle: Joi.string().trim().uri().default(null),
49+
spaBundle: Joi.string().trim().uri(),
50+
cssBundle: Joi.string().trim().uri(),
5151
assetsDiscoveryUrl: Joi.string().trim().uri().default(null),
5252
dependencies: Joi.object().default({}),
5353
props: Joi.object().default({}),
@@ -60,7 +60,7 @@ const commonApp = {
6060
.and('src', 'timeout')
6161
.empty({})
6262
.default(null),
63-
kind: Joi.string().valid('primary', 'essential', 'regular', 'wrapper'),
63+
kind: Joi.string().valid('primary', 'essential', 'regular', 'wrapper').default('regular'),
6464
wrappedWith: Joi.when('kind', {
6565
is: 'wrapper',
6666
then: Joi.any().custom(() => null),
@@ -81,9 +81,9 @@ const commonApp = {
8181
}),
8282
}),
8383
discoveryMetadata: Joi.object().default({}),
84-
adminNotes: Joi.string().trim().default(null),
84+
adminNotes: Joi.string().trim(),
8585
enforceDomain: Joi.number().default(null),
86-
l10nManifest: Joi.string().max(255).default(null),
86+
l10nManifest: Joi.string().max(255),
8787
versionId: Joi.string().strip(),
8888
namespace: Joi.string(),
8989
};

registry/server/common/services/entries/ApplicationEntry.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Knex } from 'knex';
2+
import { User } from '../../../../typings/User';
13
import { App, appSchema, partialAppSchema } from '../../../apps/interfaces';
24
import { VersionedKnex } from '../../../db';
35
import { Tables } from '../../../db/structure';
@@ -80,8 +82,8 @@ export class ApplicationEntry implements Entry {
8082
return savedApp;
8183
}
8284

83-
public async upsert(params: unknown, { user, trxProvider, fetchManifest = true }: UpsertOptions): Promise<void> {
84-
const appDto = await appSchema.validateAsync(params, { noDefaults: true, externals: true });
85+
public async upsert(params: unknown, { user, trxProvider, fetchManifest = true }: UpsertOptions): Promise<App> {
86+
const appDto = await appSchema.validateAsync(params, { noDefaults: false, externals: true });
8587

8688
const appManifest = fetchManifest ? await this.getManifest(appDto.assetsDiscoveryUrl) : {};
8789

@@ -97,6 +99,27 @@ export class ApplicationEntry implements Entry {
9799
.merge()
98100
.transacting(trx);
99101
});
102+
return appEntity;
103+
}
104+
105+
public async deleteByNamespace(
106+
namespace: string,
107+
exclude: string[],
108+
{ user, trxProvider }: { user: User; trxProvider: Knex.TransactionProvider },
109+
) {
110+
const trx = await trxProvider?.();
111+
const appNamesToDelete = await this.db(Tables.Apps)
112+
.select('name')
113+
.where({ namespace })
114+
.whereNotIn('name', exclude)
115+
.transacting(trx);
116+
await Promise.all(
117+
appNamesToDelete.map(async (app) => {
118+
await this.db.versioning(user, { type: EntityTypes.apps, id: app.name, trxProvider }, async (trx) => {
119+
await this.db(Tables.Apps).delete().where({ name: app.name }).transacting(trx);
120+
});
121+
}),
122+
);
100123
}
101124

102125
private cleanComplexDefaultKeys(appDTO: Omit<App, 'name'>, params: unknown) {

registry/server/common/services/entries/SharedLibEntry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class SharedLibEntry implements Entry {
6969
return savedSharedLib;
7070
}
7171
public async upsert(entity: unknown, { user, trxProvider, fetchManifest = true }: UpsertOptions): Promise<void> {
72-
const sharedLibDto = await sharedLibSchema.validateAsync(entity, { noDefaults: true });
72+
const sharedLibDto = await sharedLibSchema.validateAsync(entity, { noDefaults: false });
7373

7474
const sharedLibManifest = fetchManifest
7575
? await this.getManifest(sharedLibDto.assetsDiscoveryUrl)

0 commit comments

Comments
 (0)