@nocobase/app-plugin-releases 0.1.0-beta.13 → 0.1.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +19 -13
- package/dist/client/components/environment-form.d.ts +1 -1
- package/dist/client/components/environment-form.d.ts.map +1 -1
- package/dist/client/components/environment-form.js +8 -1
- package/dist/client/components/environment-form.js.map +1 -1
- package/dist/client/components/variable-alerts.d.ts.map +1 -1
- package/dist/client/components/variable-alerts.js +2 -11
- package/dist/client/components/variable-alerts.js.map +1 -1
- package/dist/client/locales/de-DE.d.ts.map +1 -1
- package/dist/client/locales/de-DE.js +4 -1
- package/dist/client/locales/de-DE.js.map +1 -1
- package/dist/client/locales/en-US.d.ts +7 -1
- package/dist/client/locales/en-US.d.ts.map +1 -1
- package/dist/client/locales/en-US.js +4 -1
- package/dist/client/locales/en-US.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts.map +1 -1
- package/dist/client/locales/es-ES.js +4 -1
- package/dist/client/locales/es-ES.js.map +1 -1
- package/dist/client/locales/fr-FR.d.ts.map +1 -1
- package/dist/client/locales/fr-FR.js +4 -1
- package/dist/client/locales/fr-FR.js.map +1 -1
- package/dist/client/locales/id-ID.d.ts.map +1 -1
- package/dist/client/locales/id-ID.js +4 -1
- package/dist/client/locales/id-ID.js.map +1 -1
- package/dist/client/locales/ja-JP.d.ts.map +1 -1
- package/dist/client/locales/ja-JP.js +4 -1
- package/dist/client/locales/ja-JP.js.map +1 -1
- package/dist/client/locales/pt-BR.d.ts.map +1 -1
- package/dist/client/locales/pt-BR.js +4 -1
- package/dist/client/locales/pt-BR.js.map +1 -1
- package/dist/client/locales/vi-VN.d.ts.map +1 -1
- package/dist/client/locales/vi-VN.js +4 -1
- package/dist/client/locales/vi-VN.js.map +1 -1
- package/dist/client/locales/zh-CN.d.ts.map +1 -1
- package/dist/client/locales/zh-CN.js +4 -1
- package/dist/client/locales/zh-CN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts.map +1 -1
- package/dist/client/locales/zh-TW.js +4 -1
- package/dist/client/locales/zh-TW.js.map +1 -1
- package/dist/client/pages/environment-page.js +1 -1
- package/dist/client/pages/environment-page.js.map +1 -1
- package/dist/client/pages/environments-page.d.ts +8 -3
- package/dist/client/pages/environments-page.d.ts.map +1 -1
- package/dist/client/pages/environments-page.js +16 -4
- package/dist/client/pages/environments-page.js.map +1 -1
- package/dist/client/pages.d.ts +4 -1
- package/dist/client/pages.d.ts.map +1 -1
- package/dist/client/pages.js +1 -0
- package/dist/client/pages.js.map +1 -1
- package/dist/database/migrations/.manifest.json +4 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts +8 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts.map +1 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.js +30 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.js.map +1 -0
- package/dist/server/routes/api.d.ts.map +1 -1
- package/dist/server/routes/api.js +55 -6
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +3 -1
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +21 -3
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/services/environments.d.ts +2 -2
- package/dist/server/services/environments.d.ts.map +1 -1
- package/dist/server/services/environments.js +21 -1
- package/dist/server/services/environments.js.map +1 -1
- package/dist/server/services/releases.d.ts +30 -6
- package/dist/server/services/releases.d.ts.map +1 -1
- package/dist/server/services/releases.js +77 -51
- package/dist/server/services/releases.js.map +1 -1
- package/dist/shared/releases.d.ts +15 -1
- package/dist/shared/releases.d.ts.map +1 -1
- package/dist/shared/releases.js +5 -0
- package/dist/shared/releases.js.map +1 -1
- package/package.json +6 -6
|
@@ -26,6 +26,11 @@ import { decryptText, encryptText } from './secrets.js';
|
|
|
26
26
|
import { DEFAULT_INITIAL_ADMIN, INITIAL_ADMIN_PATHS, PUBLIC_ORIGIN_PATH, SAMPLE_DATA_PATH, appVariableView, assertVariableName, assertVariableValue, configValueAt, decodeVariablesManifest, generateVariable, hasRealValue, isSecretName, parseVariablesManifest, randomPassword, resolveVariables, } from './variables.js';
|
|
27
27
|
import { APP_ID_PATTERN, assertAppId, assertPagination, normalizeLabels, normalizeName, normalizeRuntimePolicy, releaseArtifactKey, } from './validation.js';
|
|
28
28
|
const DEFAULT_STARTUP_WAIT_MS = 5_000;
|
|
29
|
+
/** `<appId>-<version>.tar.gz`, with anything but letters, digits, `.`, `_` and `-` replaced. */
|
|
30
|
+
export function archiveFileName(appId, version) {
|
|
31
|
+
const safe = (text) => text.replace(/[^A-Za-z0-9._-]+/gu, '_').replace(/^\.+/u, '') || 'release';
|
|
32
|
+
return `${safe(appId)}-${safe(version)}.tar.gz`;
|
|
33
|
+
}
|
|
29
34
|
export class ReleasesService {
|
|
30
35
|
options;
|
|
31
36
|
diagnostic;
|
|
@@ -118,7 +123,7 @@ export class ReleasesService {
|
|
|
118
123
|
: {}),
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
|
-
async createApp(caller, input) {
|
|
126
|
+
async createApp(caller, input, options = {}) {
|
|
122
127
|
this.options.guard.requireCreate(caller);
|
|
123
128
|
if (!input || typeof input.id !== 'string')
|
|
124
129
|
throw new ReleasesError('An App ID is required.', 'INVALID_APP_ID', 'INVALID_ARGUMENT');
|
|
@@ -147,28 +152,32 @@ export class ReleasesService {
|
|
|
147
152
|
if (await this.findApp(id))
|
|
148
153
|
throw new ReleasesError('Application ID is unavailable. Choose a different ID.', 'APP_EXISTS', 'ALREADY_EXISTS');
|
|
149
154
|
await this.assertEnvironmentCapacity(environment);
|
|
150
|
-
await this.
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
await this.options.database.transaction(async (connection) => {
|
|
156
|
+
await connection.query
|
|
157
|
+
.insertInto('relApps')
|
|
158
|
+
.values({
|
|
159
|
+
id,
|
|
160
|
+
environmentId: environment.id,
|
|
161
|
+
name,
|
|
162
|
+
description: typeof input.description === 'string' &&
|
|
163
|
+
input.description.trim()
|
|
164
|
+
? input.description.trim()
|
|
165
|
+
: null,
|
|
166
|
+
currentDeploymentId: null,
|
|
167
|
+
enabled: false,
|
|
168
|
+
activation: policy.activation,
|
|
169
|
+
idleStopMinutes: policy.idleStopMinutes,
|
|
170
|
+
dormantAfterHours: policy.dormantAfterHours,
|
|
171
|
+
labels: JSON.stringify(labels),
|
|
172
|
+
previewOf,
|
|
173
|
+
createdBy: caller.userId,
|
|
174
|
+
createdVia: caller.kind,
|
|
175
|
+
createdAt: now,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
})
|
|
178
|
+
.execute();
|
|
179
|
+
await options.initialize?.(connection, id);
|
|
180
|
+
});
|
|
172
181
|
});
|
|
173
182
|
}
|
|
174
183
|
catch (error) {
|
|
@@ -271,6 +280,48 @@ export class ReleasesService {
|
|
|
271
280
|
const images = await this.artifacts.imagesOf([release.id]);
|
|
272
281
|
return releaseView(release, images.get(release.id) ?? []);
|
|
273
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Several Apps' summaries in one batched read (one runtime status call per environment), for the assembling
|
|
285
|
+
* application: no caller is checked. Unknown ids are left out; the rest keep the order asked.
|
|
286
|
+
*/
|
|
287
|
+
async summarizeApps(appIds) {
|
|
288
|
+
const ids = [...new Set(appIds)];
|
|
289
|
+
if (ids.length === 0)
|
|
290
|
+
return [];
|
|
291
|
+
const rows = await this.query()
|
|
292
|
+
.selectFrom('relApps')
|
|
293
|
+
.selectAll()
|
|
294
|
+
.where('id', 'in', ids)
|
|
295
|
+
.execute();
|
|
296
|
+
const byId = new Map(rows.map(decodeApp).map((app) => [app.id, app]));
|
|
297
|
+
const apps = ids.flatMap((id) => {
|
|
298
|
+
const app = byId.get(id);
|
|
299
|
+
return app ? [app] : [];
|
|
300
|
+
});
|
|
301
|
+
return this.summarize(apps);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* A release's archive, to download: `view` on the App. An image release has none (`RELEASE_HAS_NO_ARCHIVE`), and an
|
|
305
|
+
* archive missing from the disk is `RELEASE_ARCHIVE_NOT_FOUND`. The stream is opened last, so a refusal never touches
|
|
306
|
+
* the disk.
|
|
307
|
+
*/
|
|
308
|
+
async openArchive(caller, appId, releaseId) {
|
|
309
|
+
const app = await this.requireApp(appId);
|
|
310
|
+
await this.options.guard.requireApp(caller, 'view', app);
|
|
311
|
+
const release = await this.requireRelease(appId, releaseId);
|
|
312
|
+
const key = release.artifactKey;
|
|
313
|
+
if (!key)
|
|
314
|
+
throw new ReleasesError('This release is a set of images and has no archive to download.', 'RELEASE_HAS_NO_ARCHIVE', 'FAILED_PRECONDITION');
|
|
315
|
+
// A local disk opens its file lazily, so a missing archive would fail only after the response headers went out.
|
|
316
|
+
if (!(await this.disk.exists(key)))
|
|
317
|
+
throw notFound('Archive', 'RELEASE_ARCHIVE_NOT_FOUND', releaseId);
|
|
318
|
+
return {
|
|
319
|
+
stream: await this.disk.getStream(key),
|
|
320
|
+
size: release.size,
|
|
321
|
+
checksum: release.checksum,
|
|
322
|
+
fileName: archiveFileName(app.id, release.version),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
274
325
|
/**
|
|
275
326
|
* Registers an image CI built and pushed, by digest, as a release of `version` (an image release, which Docker
|
|
276
327
|
* environments pull). It needs `upload` on the App, as uploading an archive does. The same digest again answers its
|
|
@@ -930,7 +981,6 @@ export class ReleasesService {
|
|
|
930
981
|
* not report every App as stopped or a finished deployment as still running.
|
|
931
982
|
*/
|
|
932
983
|
async restore() {
|
|
933
|
-
await this.clearExpiredInitialAdmins().catch((error) => this.diagnostic.warn('Failed to clear expired first administrators', error));
|
|
934
984
|
const interrupted = (await this.query()
|
|
935
985
|
.selectFrom('relDeployments')
|
|
936
986
|
.selectAll()
|
|
@@ -1201,15 +1251,14 @@ export class ReleasesService {
|
|
|
1201
1251
|
}
|
|
1202
1252
|
/**
|
|
1203
1253
|
* The first administrator the App's first deployment generated, for a person who may deploy it: until someone
|
|
1204
|
-
* says it is saved
|
|
1254
|
+
* says it is saved or the App is removed.
|
|
1205
1255
|
*/
|
|
1206
1256
|
async readInitialAdmin(caller, appId) {
|
|
1207
1257
|
const app = await this.requireApp(appId);
|
|
1208
1258
|
await this.requireInitialAdminReader(caller, app);
|
|
1209
|
-
await this.clearExpiredInitialAdmins();
|
|
1210
1259
|
const found = await this.initialAdminOf(app);
|
|
1211
1260
|
if (!found)
|
|
1212
|
-
throw new ReleasesError('This App has no first administrator to show: it was saved
|
|
1261
|
+
throw new ReleasesError('This App has no first administrator to show: it was saved or the application did not generate one.', 'INITIAL_ADMIN_UNAVAILABLE', 'NOT_FOUND');
|
|
1213
1262
|
return found;
|
|
1214
1263
|
}
|
|
1215
1264
|
/** Forgets the generated first administrator once someone has saved it. */
|
|
@@ -1227,16 +1276,6 @@ export class ReleasesService {
|
|
|
1227
1276
|
.where('initialAdmin', 'is not', null)
|
|
1228
1277
|
.execute();
|
|
1229
1278
|
}
|
|
1230
|
-
/** Clears the first administrators whose 24 hours are over. */
|
|
1231
|
-
async clearExpiredInitialAdmins(now = new Date()) {
|
|
1232
|
-
await this.query()
|
|
1233
|
-
.updateTable('relDeployments')
|
|
1234
|
-
.set({ initialAdmin: null })
|
|
1235
|
-
.where('initialAdmin', 'is not', null)
|
|
1236
|
-
.where('initialAdminExpiresAt', 'is not', null)
|
|
1237
|
-
.where('initialAdminExpiresAt', '<=', now)
|
|
1238
|
-
.execute();
|
|
1239
|
-
}
|
|
1240
1279
|
async requireInitialAdminReader(caller, app) {
|
|
1241
1280
|
// A password a person copies: never an agent's or a key's, whatever they may deploy.
|
|
1242
1281
|
if (caller.kind !== 'human' &&
|
|
@@ -1253,18 +1292,14 @@ export class ReleasesService {
|
|
|
1253
1292
|
.where('initialAdmin', 'is not', null)
|
|
1254
1293
|
.orderBy('createdAt', 'desc')
|
|
1255
1294
|
.execute();
|
|
1256
|
-
const now = new Date();
|
|
1257
1295
|
for (const deployment of rows.map(decodeDeployment)) {
|
|
1258
|
-
if (deployment.initialAdminExpiresAt &&
|
|
1259
|
-
deployment.initialAdminExpiresAt <= now)
|
|
1260
|
-
continue;
|
|
1261
1296
|
const admin = this.openInitialAdmin(deployment);
|
|
1262
1297
|
if (!admin)
|
|
1263
1298
|
continue;
|
|
1264
1299
|
return {
|
|
1265
1300
|
deploymentId: deployment.id,
|
|
1266
1301
|
...admin,
|
|
1267
|
-
expiresAt:
|
|
1302
|
+
expiresAt: null,
|
|
1268
1303
|
};
|
|
1269
1304
|
}
|
|
1270
1305
|
return null;
|
|
@@ -1434,7 +1469,6 @@ export class ReleasesService {
|
|
|
1434
1469
|
for (const [name, variable] of plan.oneShot)
|
|
1435
1470
|
generated[name] = generateVariable(variable.generate);
|
|
1436
1471
|
let initialAdmin = null;
|
|
1437
|
-
let initialAdminExpiresAt = null;
|
|
1438
1472
|
if (plan.initialAdmin) {
|
|
1439
1473
|
const password = (await this.earlierInitialAdmin(app))?.password ?? randomPassword();
|
|
1440
1474
|
generated[plan.initialAdmin.name] = password;
|
|
@@ -1444,10 +1478,6 @@ export class ReleasesService {
|
|
|
1444
1478
|
password,
|
|
1445
1479
|
};
|
|
1446
1480
|
initialAdmin = encryptText(JSON.stringify(admin), [deploymentId], this.options.secrets, 'initial-admin');
|
|
1447
|
-
// A preview App's goes with the preview; any other's is deleted after a day.
|
|
1448
|
-
initialAdminExpiresAt = app.previewOf
|
|
1449
|
-
? null
|
|
1450
|
-
: new Date(Date.now() + INITIAL_ADMIN_TTL_MS);
|
|
1451
1481
|
}
|
|
1452
1482
|
const resolved = resolveVariables({
|
|
1453
1483
|
manifest: release.variables,
|
|
@@ -1465,7 +1495,6 @@ export class ReleasesService {
|
|
|
1465
1495
|
: encryptText(JSON.stringify(resolved.env), [deploymentId], this.options.secrets, 'deployment-env'),
|
|
1466
1496
|
variablesFingerprint: release.variables || names.length > 0 ? resolved.fingerprint : null,
|
|
1467
1497
|
initialAdmin,
|
|
1468
|
-
initialAdminExpiresAt,
|
|
1469
1498
|
};
|
|
1470
1499
|
}
|
|
1471
1500
|
/**
|
|
@@ -2333,8 +2362,6 @@ export class ReleasesService {
|
|
|
2333
2362
|
return this.options.database.connection().query;
|
|
2334
2363
|
}
|
|
2335
2364
|
}
|
|
2336
|
-
// --- Variables --------------------------------------------------------------------------------------------------
|
|
2337
|
-
const INITIAL_ADMIN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2338
2365
|
/** A variable's value from the App or the environment, when one of them sets it. */
|
|
2339
2366
|
function suppliedValue(context, name) {
|
|
2340
2367
|
return (context.appVariables.find((variable) => variable.name === name)?.value ??
|
|
@@ -2689,7 +2716,6 @@ function decodeDeployment(row) {
|
|
|
2689
2716
|
env: nullableString(row.env),
|
|
2690
2717
|
variablesFingerprint: nullableString(row.variablesFingerprint),
|
|
2691
2718
|
initialAdmin: nullableString(row.initialAdmin),
|
|
2692
|
-
initialAdminExpiresAt: decodeOptionalDate(row.initialAdminExpiresAt),
|
|
2693
2719
|
initialAdminSavedBy: nullableString(row.initialAdminSavedBy),
|
|
2694
2720
|
initialAdminSavedAt: decodeOptionalDate(row.initialAdminSavedAt),
|
|
2695
2721
|
createdAt: decodeDate(row.createdAt),
|