@abtnode/core 1.16.52-beta-20251003-083412-fdfc4e36 → 1.16.52-beta-20251005-235515-42ad5caf

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.
@@ -0,0 +1,292 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ const logger = require('@abtnode/logger')('@abtnode/core:blocklet-manager:blue-green');
3
+ const { BlockletStatus, BlockletGroup, BlockletEvents } = require('@blocklet/constant');
4
+ const {
5
+ hasRunnableComponent,
6
+ getDisplayName,
7
+ isExternalBlocklet,
8
+ forEachBlockletSync,
9
+ getComponentMissingConfigs,
10
+ } = require('@blocklet/meta/lib/util');
11
+ const { getBlockletEngine, hasStartEngine } = require('@blocklet/meta/lib/engine');
12
+ const {
13
+ forEachBlocklet,
14
+ validateBlocklet,
15
+ validateBlockletChainInfo,
16
+ ensureAppPortsNotOccupied,
17
+ getHealthyCheckTimeout,
18
+ getHookArgs,
19
+ shouldSkipComponent,
20
+ } = require('../../../util/blocklet');
21
+ const { startBlockletProcess } = require('../../../util/blocklet');
22
+ const hooks = require('../../hooks');
23
+ const checkNeedRunDocker = require('../../../util/docker/check-need-run-docker');
24
+ const { blueGreenGetComponentIds } = require('./blue-green-get-componentids');
25
+ const { dockerExec } = require('../../../util/docker/docker-exec');
26
+ const { isDockerOnlySingleInstance } = require('../../../util/docker/is-docker-only-single-instances');
27
+
28
+ /**
29
+ * 绿(Green)环境:指一个并行的、与蓝环境几乎一模一样的环境,用来部署新的版本。新代码、新配置都会先部署到绿环境中。
30
+ *
31
+ * @param {Object} params - 启动参数
32
+ * @param {string} params.did - blocklet DID
33
+ * @param {Array<string>} params.componentDids - 组件 DID 列表
34
+ * @param {string} params.operator - 操作者
35
+ * @param {Object} context - 上下文信息
36
+ * @param {Object} manager - blocklet 管理器实例
37
+ * @param {Object} states - 状态管理器
38
+ * @returns {Promise<Object>} 返回启动后的 blocklet 对象
39
+ */
40
+ const blueGreenStartBlocklet = async (
41
+ { did, componentDids, operator: _operator, ignoreErrorNotification },
42
+ context,
43
+ manager,
44
+ states
45
+ ) => {
46
+ const operator = _operator || context?.user?.did;
47
+ const throwOnError = true;
48
+ const checkHealthImmediately = true;
49
+ const e2eMode = false;
50
+
51
+ logger.info('start green blocklet (blue-green deployment)', {
52
+ did,
53
+ componentDids,
54
+ throwOnError,
55
+ checkHealthImmediately,
56
+ e2eMode,
57
+ operator,
58
+ });
59
+
60
+ // 获取并验证 blocklet
61
+ const blocklet1 = await manager.ensureBlocklet(did, { e2eMode });
62
+ did = blocklet1.meta.did; // eslint-disable-line no-param-reassign
63
+
64
+ // 验证组件需求和引擎
65
+ await validateBlocklet(blocklet1);
66
+ await validateBlockletChainInfo(blocklet1);
67
+
68
+ if (!hasRunnableComponent(blocklet1)) {
69
+ throw new Error('No runnable component found');
70
+ }
71
+
72
+ const customerDockerUseVolumeComponentIds = [];
73
+ const otherComponentIds = [];
74
+ await forEachBlockletSync(blocklet1, (b) => {
75
+ if (!componentDids.includes(b.meta.did)) {
76
+ return;
77
+ }
78
+ if (isDockerOnlySingleInstance(b.meta)) {
79
+ customerDockerUseVolumeComponentIds.push(b.meta.did);
80
+ } else {
81
+ otherComponentIds.push(b.meta.did);
82
+ }
83
+ });
84
+
85
+ if (customerDockerUseVolumeComponentIds.length) {
86
+ await manager.stop(
87
+ { did, componentDids: customerDockerUseVolumeComponentIds, updateStatus: false, operator },
88
+ context
89
+ );
90
+ await manager.start({ did, componentDids: customerDockerUseVolumeComponentIds, operator }, context);
91
+ }
92
+
93
+ // 分类组件 ID
94
+ const entryComponentIds = [];
95
+ const nonEntryComponentIds = [];
96
+ const componentDidsSet = new Set(otherComponentIds);
97
+
98
+ await forEachBlocklet(
99
+ blocklet1,
100
+ (b) => {
101
+ if (!componentDidsSet.has(b.meta.did)) {
102
+ return;
103
+ }
104
+
105
+ if (b.meta.group === BlockletGroup.gateway) {
106
+ nonEntryComponentIds.push(b.meta.did);
107
+ return;
108
+ }
109
+ const engine = getBlockletEngine(b.meta);
110
+ if (engine.interpreter === 'blocklet') {
111
+ nonEntryComponentIds.push(b.meta.did);
112
+ return;
113
+ }
114
+ if (!hasStartEngine(b.meta)) {
115
+ nonEntryComponentIds.push(b.meta.did);
116
+ return;
117
+ }
118
+ entryComponentIds.push(b.meta.did);
119
+ },
120
+ { parallel: true, concurrencyLimit: 4 }
121
+ );
122
+
123
+ if (nonEntryComponentIds.length) {
124
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.running, {
125
+ componentDids: nonEntryComponentIds,
126
+ operator,
127
+ isGreen: true,
128
+ });
129
+ }
130
+
131
+ if (!entryComponentIds.length) {
132
+ await manager.emit(BlockletEvents.started, {
133
+ ...blocklet1,
134
+ componentDids: nonEntryComponentIds,
135
+ });
136
+ return;
137
+ }
138
+
139
+ // check required config
140
+ try {
141
+ for (const component of blocklet1.children) {
142
+ if (!entryComponentIds.includes(component.meta.did)) {
143
+ continue;
144
+ }
145
+ if (!shouldSkipComponent(component.meta.did, entryComponentIds)) {
146
+ const missingProps = getComponentMissingConfigs(component, blocklet1);
147
+ if (missingProps.length) {
148
+ throw new Error(
149
+ `Missing required configuration to start ${component.meta.title}: ${missingProps.map((x) => x.key).join(',')}`
150
+ );
151
+ }
152
+ }
153
+ }
154
+ } catch (error) {
155
+ const description = `Green environment start failed for ${getDisplayName(blocklet1)}: ${error.message}`;
156
+ if (!ignoreErrorNotification) {
157
+ manager._createNotification(did, {
158
+ title: 'Blue-Green Deployment: Green Start Failed',
159
+ description,
160
+ entityType: 'blocklet',
161
+ entityId: did,
162
+ severity: 'error',
163
+ });
164
+ }
165
+ return;
166
+ }
167
+
168
+ const blueGreenComponentIds = await blueGreenGetComponentIds(blocklet1, entryComponentIds);
169
+
170
+ // eslint-disable-next-line no-unreachable-loop
171
+ await Promise.all(
172
+ blueGreenComponentIds.map(async (item) => {
173
+ if (!item.componentDids.length) {
174
+ return;
175
+ }
176
+ const nextBlocklet = await ensureAppPortsNotOccupied({
177
+ blocklet: blocklet1,
178
+ componentDids: item.componentDids,
179
+ states,
180
+ manager,
181
+ isGreen: item.changeToGreen,
182
+ });
183
+ try {
184
+ const doc1 = await states.blocklet.setBlockletStatus(did, BlockletStatus.starting, {
185
+ componentDids: item.componentDids,
186
+ operator,
187
+ isGreen: item.changeToGreen,
188
+ });
189
+ nextBlocklet.greenStatus = BlockletStatus.starting;
190
+ manager.emit(BlockletEvents.statusChange, doc1);
191
+
192
+ const nodeInfo = await states.node.read();
193
+ const nodeEnvironments = await states.node.getEnvironments();
194
+
195
+ // 钩子函数设置
196
+ const getHookFn =
197
+ (hookName) =>
198
+ async (b, { env }) => {
199
+ const hookArgs = getHookArgs(b);
200
+ const needRunDocker = await checkNeedRunDocker(b.meta, env, nodeInfo, isExternalBlocklet(nextBlocklet));
201
+ if (!b.meta.scripts?.[hookName]) {
202
+ return null;
203
+ }
204
+ if (needRunDocker) {
205
+ return dockerExec({
206
+ blocklet: nextBlocklet,
207
+ meta: b.meta,
208
+ script: b.meta.scripts?.[hookName],
209
+ hookName,
210
+ nodeInfo,
211
+ env,
212
+ ...hookArgs,
213
+ });
214
+ }
215
+ return hooks[hookName](b, {
216
+ appDir: b.env.appDir,
217
+ hooks: Object.assign(b.meta.hooks || {}, b.meta.scripts || {}),
218
+ env,
219
+ did, // root blocklet did,
220
+ teamManager: manager.teamManager,
221
+ ...hookArgs,
222
+ });
223
+ };
224
+
225
+ await startBlockletProcess(nextBlocklet, {
226
+ ...context,
227
+ preFlight: getHookFn('preFlight'),
228
+ preStart: getHookFn('preStart'),
229
+ postStart: getHookFn('postStart'),
230
+ nodeEnvironments,
231
+ nodeInfo,
232
+ e2eMode,
233
+ componentDids: item.componentDids,
234
+ configSynchronizer: manager.configSynchronizer,
235
+ isGreen: item.changeToGreen,
236
+ });
237
+
238
+ // 健康检查绿色环境
239
+ const { startTimeout, minConsecutiveTime } = getHealthyCheckTimeout(nextBlocklet, {
240
+ checkHealthImmediately,
241
+ componentDids: item.componentDids,
242
+ });
243
+
244
+ await manager._onCheckIfStarted(
245
+ {
246
+ did,
247
+ context,
248
+ minConsecutiveTime,
249
+ timeout: startTimeout,
250
+ componentDids: item.componentDids,
251
+ },
252
+ { throwOnError: true, isGreen: item.changeToGreen, needUpdateBlueStatus: true }
253
+ );
254
+
255
+ logger.info('Green environment started successfully', {
256
+ did,
257
+ componentDids: item.componentDids,
258
+ });
259
+ } catch (err) {
260
+ const error = Array.isArray(err) ? err[0] : err;
261
+ logger.error('Failed to start green environment', { error, did, title: blocklet1.meta.title });
262
+
263
+ try {
264
+ await manager.deleteProcess({ did, componentDids: item.componentDids, isGreen: item.changeToGreen });
265
+ manager.emit(BlockletEvents.statusChange, blocklet1);
266
+ } catch (cleanupError) {
267
+ logger.error('Failed to cleanup green environment', { cleanupError });
268
+ }
269
+
270
+ const description = `Green environment start failed for ${getDisplayName(blocklet1)}: ${error.message}`;
271
+ if (!ignoreErrorNotification) {
272
+ manager._createNotification(did, {
273
+ title: 'Blue-Green Deployment: Green Start Failed',
274
+ description,
275
+ entityType: 'blocklet',
276
+ entityId: did,
277
+ severity: 'error',
278
+ });
279
+ }
280
+
281
+ if (throwOnError) {
282
+ throw new Error(description);
283
+ }
284
+ throw error;
285
+ }
286
+ })
287
+ );
288
+ };
289
+
290
+ module.exports = {
291
+ blueGreenStartBlocklet,
292
+ };
@@ -0,0 +1,18 @@
1
+ const blueGreenUpdateBlockletStatus = async ({ states, did, status, blueGreenComponentIds }) => {
2
+ const outputBlocklet = {};
3
+ await Promise.all(
4
+ blueGreenComponentIds.map(async (item) => {
5
+ if (!item.componentDids.length) {
6
+ return;
7
+ }
8
+ const res = await states.blocklet.setBlockletStatus(did, status, {
9
+ componentDids: item.componentDids,
10
+ isGreen: item.changeToGreen,
11
+ });
12
+ Object.assign(outputBlocklet, res);
13
+ })
14
+ );
15
+ return outputBlocklet;
16
+ };
17
+
18
+ module.exports = { blueGreenUpdateBlockletStatus };
@@ -0,0 +1,191 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ const logger = require('@abtnode/logger')('@abtnode/core:blocklet-manager:upgrade-blue-green');
3
+ const { BlockletStatus, BlockletEvents, BlockletInternalEvents } = require('@blocklet/constant');
4
+ const { INSTALL_ACTIONS, WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
5
+ const { getDisplayName, hasStartEngine } = require('@blocklet/meta/lib/util');
6
+ const { getComponentsInternalInfo } = require('@blocklet/meta/lib/blocklet');
7
+ const { getComponentNamesWithVersion, updateBlockletFallbackLogo } = require('../../../util/blocklet');
8
+ const { blueGreenStartBlocklet } = require('./blue-green-start-blocklet');
9
+
10
+ const blueGreenUpgradeBlocklet = async (
11
+ { newBlocklet, oldBlocklet, componentDids, action, shouldCleanUploadFile, url },
12
+ context,
13
+ manager,
14
+ states
15
+ ) => {
16
+ const { meta, source, deployedFrom, children } = newBlocklet;
17
+ const { did, version, name } = meta;
18
+ const title = getDisplayName(newBlocklet);
19
+
20
+ for (const child of newBlocklet.children) {
21
+ for (const child2 of oldBlocklet.children) {
22
+ if (child && child2 && child.meta.did === child2.meta.did) {
23
+ child.status = child2.status;
24
+ child.greenStatus = child2.greenStatus;
25
+ child.ports = child2.ports;
26
+ child.greenPorts = child2.greenPorts;
27
+ }
28
+ }
29
+ }
30
+
31
+ try {
32
+ await states.blocklet.upgradeBlocklet({ meta, source, deployedFrom, children });
33
+ logger.info('updated blocklet for upgrading', { did, componentDids, source, name });
34
+
35
+ await manager._setConfigsFromMeta(did);
36
+
37
+ let blocklet = await manager.ensureBlocklet(did);
38
+
39
+ await manager._updateBlockletEnvironment(did);
40
+ blocklet = await manager.getBlocklet(did);
41
+
42
+ await manager._runUserHook('preInstall', blocklet, context);
43
+ await manager._runUserHook('postInstall', blocklet, context);
44
+ await manager._runUserHook('preFlight', blocklet, context);
45
+ await manager._runMigration({
46
+ parallel: false,
47
+ did,
48
+ blocklet,
49
+ oldBlocklet,
50
+ componentDids,
51
+ });
52
+
53
+ const runningDids = [];
54
+ const stoppedDids = [];
55
+
56
+ if (action === INSTALL_ACTIONS.INSTALL_COMPONENT) {
57
+ for (const componentDid of componentDids) {
58
+ const component = blocklet.children.find((x) => x.meta.did === componentDid);
59
+ if (!component) {
60
+ continue;
61
+ }
62
+ if (hasStartEngine(component.meta)) {
63
+ stoppedDids.push(componentDid);
64
+ } else {
65
+ runningDids.push(componentDid);
66
+ }
67
+ }
68
+ } else {
69
+ for (const componentDid of componentDids) {
70
+ const oldComponent = oldBlocklet.children.find((x) => x.meta.did === componentDid);
71
+ if (oldComponent?.status === BlockletStatus.running || oldComponent?.greenStatus === BlockletStatus.running) {
72
+ runningDids.push(componentDid);
73
+ } else {
74
+ stoppedDids.push(componentDid);
75
+ }
76
+ }
77
+ }
78
+
79
+ const initialized = !!blocklet.settings?.initialized;
80
+
81
+ if (runningDids.length) {
82
+ if (initialized) {
83
+ await blueGreenStartBlocklet(
84
+ { did, componentDids: runningDids, operator: context?.user?.did, ignoreErrorNotification: true },
85
+ context,
86
+ manager,
87
+ states
88
+ );
89
+ await states.blocklet.setInstalledAt(did);
90
+ } else {
91
+ await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped, {
92
+ componentDids: runningDids,
93
+ isGreenAndBlue: true,
94
+ });
95
+ }
96
+ }
97
+
98
+ if (stoppedDids.length) {
99
+ const status = action === INSTALL_ACTIONS.INSTALL_COMPONENT ? BlockletStatus.installed : BlockletStatus.stopped;
100
+ await states.blocklet.setBlockletStatus(did, status, {
101
+ componentDids: stoppedDids,
102
+ isGreenAndBlue: true,
103
+ });
104
+ }
105
+
106
+ blocklet = await manager.getBlocklet(did, context);
107
+
108
+ await updateBlockletFallbackLogo(blocklet);
109
+
110
+ await manager._updateDependents(did);
111
+
112
+ manager.refreshListCache();
113
+
114
+ manager.configSynchronizer.throttledSyncAppConfig(blocklet);
115
+
116
+ try {
117
+ manager.emit(BlockletEvents.upgraded, { blocklet, context });
118
+
119
+ const isInstallAction = action === INSTALL_ACTIONS.INSTALL_COMPONENT;
120
+ const notificationEvent = isInstallAction ? BlockletEvents.componentInstalled : BlockletEvents.componentUpgraded;
121
+ const actionName = isInstallAction ? 'installed' : 'upgraded';
122
+
123
+ manager.emit(notificationEvent, { ...blocklet, componentDids, oldBlocklet, context });
124
+
125
+ manager._createNotification(did, {
126
+ title: `Component ${actionName} succeed`,
127
+ description: `${getComponentNamesWithVersion(
128
+ newBlocklet,
129
+ componentDids
130
+ )} is ${actionName} successfully for ${title}`,
131
+ action: `/blocklets/${did}/overview`,
132
+ blockletDashboardAction: `${WELLKNOWN_SERVICE_PATH_PREFIX}/admin/blocklets`,
133
+ entityType: 'blocklet',
134
+ entityId: did,
135
+ severity: 'success',
136
+ });
137
+
138
+ if (shouldCleanUploadFile && url) {
139
+ manager._cleanUploadFile(url);
140
+ }
141
+ } catch (error) {
142
+ logger.error('emit upgrade notification failed', { name, version, error });
143
+ }
144
+
145
+ await manager._ensureDeletedChildrenInSettings(blocklet);
146
+
147
+ if (oldBlocklet.status === BlockletStatus.running || oldBlocklet.greenStatus === BlockletStatus.running) {
148
+ manager.emit(
149
+ action === INSTALL_ACTIONS.INSTALL_COMPONENT
150
+ ? BlockletInternalEvents.componentInstalled
151
+ : BlockletInternalEvents.componentUpgraded,
152
+ {
153
+ appDid: blocklet.appDid,
154
+ components: getComponentsInternalInfo(blocklet).filter((c) => componentDids.includes(c.did)),
155
+ }
156
+ );
157
+ }
158
+
159
+ return blocklet;
160
+ } catch (err) {
161
+ logger.error('failed to upgrade blocklet', { did, version, name, error: err });
162
+ manager.configSynchronizer.throttledSyncAppConfig(oldBlocklet);
163
+ await manager._updateDependents(did);
164
+
165
+ const actionName = action === INSTALL_ACTIONS.INSTALL_COMPONENT ? 'install' : 'upgrade';
166
+ const notificationEvent =
167
+ action === INSTALL_ACTIONS.INSTALL_COMPONENT
168
+ ? BlockletEvents.componentInstallFailed
169
+ : BlockletEvents.componentUpgradeFailed;
170
+
171
+ manager.emit(notificationEvent, {
172
+ blocklet: { ...newBlocklet, componentDids, error: { message: err.message } },
173
+ context,
174
+ });
175
+
176
+ manager._createNotification(did, {
177
+ title: `Component ${actionName} failed`,
178
+ description: `${getComponentNamesWithVersion(newBlocklet, componentDids)} ${actionName} failed for ${title}: ${
179
+ err.message
180
+ }.`,
181
+ entityType: 'blocklet',
182
+ entityId: did,
183
+ severity: 'error',
184
+ });
185
+ throw err;
186
+ }
187
+ };
188
+
189
+ module.exports = {
190
+ blueGreenUpgradeBlocklet,
191
+ };
@@ -3,7 +3,7 @@ const cloneDeep = require('@abtnode/util/lib/deep-clone');
3
3
 
4
4
  const logger = require('@abtnode/logger')('@abtnode/core:upgrade-component');
5
5
 
6
- const { BlockletStatus, BlockletEvents } = require('@blocklet/constant');
6
+ const { BlockletEvents } = require('@blocklet/constant');
7
7
  const { INSTALL_ACTIONS } = require('@abtnode/constant');
8
8
  const { parseOptionalComponents } = require('@blocklet/resolver');
9
9
  const {
@@ -127,17 +127,11 @@ const upgrade = async ({ updateId, componentDids, context, states, manager }) =>
127
127
  children: children.map((x) => ({ name: x.meta.name, version: x.meta.version })),
128
128
  });
129
129
 
130
- // new blocklet
131
- const newBlocklet = await states.blocklet.setBlockletStatus(did, BlockletStatus.waiting, { componentDids });
132
-
133
- newBlocklet.children = children;
130
+ const newBlocklet = { ...oldBlocklet, children };
134
131
  await validateBlocklet(newBlocklet);
135
132
 
136
- manager.emit(BlockletEvents.statusChange, newBlocklet);
137
-
138
133
  const action = INSTALL_ACTIONS.UPGRADE_COMPONENT;
139
134
 
140
- // backup rollback data
141
135
  await manager._rollbackCache.backup({ did, action, oldBlocklet });
142
136
 
143
137
  // add to queue
@@ -158,7 +152,6 @@ const upgrade = async ({ updateId, componentDids, context, states, manager }) =>
158
152
  ticket.on('failed', async (err) => {
159
153
  logger.error('queue failed', { entity: 'blocklet', action, did, error: err });
160
154
  await manager._rollback(action, did, oldBlocklet);
161
-
162
155
  manager.emit(BlockletEvents.componentUpgradeFailed, {
163
156
  blocklet: { ...oldBlocklet, componentDids, error: { message: err.message } },
164
157
  context: { ...context, createAuditLog: false },