@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.
- package/lib/api/team.js +579 -47
- package/lib/blocklet/downloader/blocklet-downloader.js +2 -2
- package/lib/blocklet/downloader/bundle-downloader.js +13 -38
- package/lib/blocklet/manager/disk.js +200 -84
- package/lib/blocklet/manager/ensure-blocklet-running.js +3 -2
- package/lib/blocklet/manager/helper/blue-green-get-componentids.js +59 -0
- package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +292 -0
- package/lib/blocklet/manager/helper/blue-green-update-blocklet-status.js +18 -0
- package/lib/blocklet/manager/helper/blue-green-upgrade-blocklet.js +191 -0
- package/lib/blocklet/manager/helper/upgrade-components.js +2 -9
- package/lib/blocklet/migration-dist/migration.cjs +458 -456
- package/lib/blocklet/passport/index.js +8 -2
- package/lib/index.js +18 -0
- package/lib/monitor/blocklet-runtime-monitor.js +12 -7
- package/lib/states/audit-log.js +54 -2
- package/lib/states/blocklet.js +23 -8
- package/lib/states/index.js +3 -0
- package/lib/states/org.js +661 -0
- package/lib/team/manager.js +10 -3
- package/lib/util/blocklet.js +190 -115
- package/lib/util/docker/is-docker-only-single-instances.js +17 -0
- package/lib/util/org.js +99 -0
- package/lib/validators/org.js +19 -0
- package/package.json +26 -26
package/lib/util/blocklet.js
CHANGED
|
@@ -116,7 +116,6 @@ const fetchPm2 = require('@abtnode/util/lib/pm2/fetch-pm2');
|
|
|
116
116
|
|
|
117
117
|
const promiseSpawn = require('@abtnode/util/lib/promise-spawn');
|
|
118
118
|
const { getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
|
|
119
|
-
const xbytes = require('xbytes');
|
|
120
119
|
const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
|
|
121
120
|
|
|
122
121
|
const isRequirementsSatisfied = require('./requirement');
|
|
@@ -184,6 +183,7 @@ const statusMap = {
|
|
|
184
183
|
errored: BlockletStatus.error,
|
|
185
184
|
stopping: BlockletStatus.stopping,
|
|
186
185
|
stopped: BlockletStatus.stopped,
|
|
186
|
+
'waiting restart': BlockletStatus.restarting,
|
|
187
187
|
};
|
|
188
188
|
|
|
189
189
|
const PRIVATE_NODE_ENVS = [
|
|
@@ -449,6 +449,7 @@ const getComponentSystemEnvironments = (blocklet) => {
|
|
|
449
449
|
if (port) {
|
|
450
450
|
portEnvironments[BLOCKLET_DEFAULT_PORT_NAME] = port;
|
|
451
451
|
}
|
|
452
|
+
|
|
452
453
|
if (ports) {
|
|
453
454
|
Object.assign(portEnvironments, ports);
|
|
454
455
|
}
|
|
@@ -466,11 +467,24 @@ const getComponentSystemEnvironments = (blocklet) => {
|
|
|
466
467
|
};
|
|
467
468
|
};
|
|
468
469
|
|
|
469
|
-
const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
|
|
470
|
+
const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors, isGreen = false) => {
|
|
470
471
|
const root = (ancestors || [])[0] || blocklet;
|
|
471
472
|
|
|
472
473
|
const initialized = root?.settings?.initialized;
|
|
473
474
|
|
|
475
|
+
const environmentObj = { ...(blocklet.environmentObj || {}) };
|
|
476
|
+
if (isGreen && blocklet.greenPorts) {
|
|
477
|
+
Object.entries(blocklet.greenPorts).forEach(([key, value]) => {
|
|
478
|
+
if (!value) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
environmentObj[key] = value;
|
|
482
|
+
if (key === BLOCKLET_DEFAULT_PORT_NAME || key === 'BLOCKLET_PORT') {
|
|
483
|
+
environmentObj[BLOCKLET_DEFAULT_PORT_NAME] = value;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
474
488
|
// pm2 will force inject env variables of daemon process to blocklet process
|
|
475
489
|
// we can only rewrite these private env variables to empty
|
|
476
490
|
const safeNodeEnvironments = PRIVATE_NODE_ENVS.reduce((o, x) => {
|
|
@@ -495,8 +509,9 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
|
|
|
495
509
|
const ports = {};
|
|
496
510
|
forEachBlockletSync(root, (x) => {
|
|
497
511
|
const webInterface = findWebInterface(x);
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
const envObj = x.meta?.did === blocklet.meta?.did ? environmentObj : x.environmentObj;
|
|
513
|
+
if (webInterface && envObj?.[webInterface.port]) {
|
|
514
|
+
ports[envObj.BLOCKLET_REAL_NAME] = envObj[webInterface.port];
|
|
500
515
|
}
|
|
501
516
|
});
|
|
502
517
|
|
|
@@ -518,7 +533,7 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
|
|
|
518
533
|
const env = {
|
|
519
534
|
...blocklet.configObj,
|
|
520
535
|
...getSharedConfigObj((ancestors || [])[0], blocklet, true),
|
|
521
|
-
...
|
|
536
|
+
...environmentObj,
|
|
522
537
|
...devEnvironments,
|
|
523
538
|
BLOCKLET_MOUNT_POINTS: JSON.stringify(componentsInternalInfo),
|
|
524
539
|
BLOCKLET_MODE: blocklet.mode || BLOCKLET_MODES.PRODUCTION,
|
|
@@ -559,6 +574,10 @@ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
|
|
|
559
574
|
env.initialized = initialized;
|
|
560
575
|
}
|
|
561
576
|
|
|
577
|
+
if (isGreen && blocklet.greenPorts?.[BLOCKLET_DEFAULT_PORT_NAME]) {
|
|
578
|
+
env[BLOCKLET_DEFAULT_PORT_NAME] = blocklet.greenPorts[BLOCKLET_DEFAULT_PORT_NAME];
|
|
579
|
+
}
|
|
580
|
+
|
|
562
581
|
// ensure all envs are literals and do not contain line breaks
|
|
563
582
|
Object.keys(env).forEach((key) => {
|
|
564
583
|
env[key] = formatEnv(env[key]);
|
|
@@ -573,7 +592,7 @@ const isUsefulError = (err) =>
|
|
|
573
592
|
!/id unknown/.test(err.message) &&
|
|
574
593
|
!/^Process \d+ not found$/.test(err.message);
|
|
575
594
|
|
|
576
|
-
const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
|
|
595
|
+
const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately, componentDids } = {}) => {
|
|
577
596
|
let minConsecutiveTime = 3000;
|
|
578
597
|
if (process.env.NODE_ENV === 'test' && process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME !== undefined) {
|
|
579
598
|
minConsecutiveTime = +process.env.ABT_NODE_TEST_MIN_CONSECUTIVE_TIME;
|
|
@@ -594,11 +613,15 @@ const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
|
|
|
594
613
|
};
|
|
595
614
|
}
|
|
596
615
|
|
|
616
|
+
const children = componentDids?.length
|
|
617
|
+
? blocklet.children.filter((child) => componentDids.includes(child.meta.did))
|
|
618
|
+
: blocklet.children;
|
|
619
|
+
|
|
597
620
|
// Let's wait for at least 1 minute for the blocklet to go live
|
|
598
621
|
let startTimeout =
|
|
599
622
|
Math.max(
|
|
600
623
|
get(blocklet, 'meta.timeout.start', 60),
|
|
601
|
-
...(
|
|
624
|
+
...(children || []).map((child) => child.meta?.timeout?.start || 0)
|
|
602
625
|
) * 1000;
|
|
603
626
|
|
|
604
627
|
if (process.env.NODE_ENV === 'test') {
|
|
@@ -628,6 +651,7 @@ const startBlockletProcess = async (
|
|
|
628
651
|
componentDids,
|
|
629
652
|
configSynchronizer,
|
|
630
653
|
onlyStart = false,
|
|
654
|
+
isGreen = false,
|
|
631
655
|
} = {}
|
|
632
656
|
) => {
|
|
633
657
|
if (!blocklet) {
|
|
@@ -637,6 +661,25 @@ const startBlockletProcess = async (
|
|
|
637
661
|
const dockerNetworkName = parseDockerName(blocklet?.meta?.did, 'docker-network');
|
|
638
662
|
await createDockerNetwork(dockerNetworkName);
|
|
639
663
|
|
|
664
|
+
blocklet.children.forEach((component) => {
|
|
665
|
+
if (!componentDids.includes(component.meta.did)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
for (const envItem of component.environments) {
|
|
669
|
+
const envKey = envItem.key || envItem.name;
|
|
670
|
+
if (envKey !== 'BLOCKLET_PORT') {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (isGreen) {
|
|
674
|
+
if (component.greenPorts?.BLOCKLET_PORT) {
|
|
675
|
+
envItem.value = component.greenPorts.BLOCKLET_PORT;
|
|
676
|
+
}
|
|
677
|
+
} else if (component.ports?.BLOCKLET_PORT) {
|
|
678
|
+
envItem.value = component.ports.BLOCKLET_PORT;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
640
683
|
const startBlockletTask = async (b, { ancestors }) => {
|
|
641
684
|
// 需要在在这里传入字符串类型,否则进程中如法转化成 Date 对象
|
|
642
685
|
const now = `${new Date()}`;
|
|
@@ -674,7 +717,7 @@ const startBlockletProcess = async (
|
|
|
674
717
|
}
|
|
675
718
|
|
|
676
719
|
// get env
|
|
677
|
-
const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors);
|
|
720
|
+
const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors, isGreen);
|
|
678
721
|
const startedAt = Date.now();
|
|
679
722
|
|
|
680
723
|
await installExternalDependencies({ appDir: env?.BLOCKLET_APP_DIR, nodeInfo });
|
|
@@ -683,8 +726,12 @@ const startBlockletProcess = async (
|
|
|
683
726
|
|
|
684
727
|
// kill process if port is occupied
|
|
685
728
|
try {
|
|
686
|
-
const { ports } = b;
|
|
687
|
-
await killProcessOccupiedPorts({
|
|
729
|
+
const { ports, greenPorts } = b;
|
|
730
|
+
await killProcessOccupiedPorts({
|
|
731
|
+
ports: isGreen ? greenPorts : ports,
|
|
732
|
+
pm2ProcessId: processId,
|
|
733
|
+
printError: logger.error.bind(logger),
|
|
734
|
+
});
|
|
688
735
|
} catch (error) {
|
|
689
736
|
logger.error('Failed to killProcessOccupiedPorts', { error });
|
|
690
737
|
}
|
|
@@ -692,12 +739,13 @@ const startBlockletProcess = async (
|
|
|
692
739
|
// start process
|
|
693
740
|
const maxMemoryRestart = get(nodeInfo, 'runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
|
|
694
741
|
|
|
742
|
+
const processIdName = isGreen ? `${processId}-green` : processId;
|
|
695
743
|
/**
|
|
696
744
|
* @type {pm2.StartOptions}
|
|
697
745
|
*/
|
|
698
746
|
const options = {
|
|
699
747
|
namespace: 'blocklets',
|
|
700
|
-
name:
|
|
748
|
+
name: processIdName,
|
|
701
749
|
cwd,
|
|
702
750
|
// FIXME @linchen [] does not work, so use () here
|
|
703
751
|
log_date_format: '(YYYY-MM-DD HH:mm:ss)',
|
|
@@ -772,34 +820,36 @@ const startBlockletProcess = async (
|
|
|
772
820
|
delete options.mergeLogs;
|
|
773
821
|
}
|
|
774
822
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
823
|
+
let nextOptions;
|
|
824
|
+
if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
|
|
825
|
+
nextOptions = options;
|
|
826
|
+
} else {
|
|
827
|
+
nextOptions = await parseDockerOptionsFromPm2({
|
|
828
|
+
options,
|
|
829
|
+
nodeInfo,
|
|
830
|
+
isExternal: isExternalBlocklet(b),
|
|
831
|
+
meta: b.meta,
|
|
832
|
+
ports: isGreen ? b.greenPorts : b.ports,
|
|
833
|
+
onlyStart,
|
|
834
|
+
dockerNetworkName,
|
|
835
|
+
rootBlocklet: blocklet,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
788
838
|
|
|
789
839
|
await fetchPm2({ ...nextOptions, pmx: false }, nodeEnvironments.ABT_NODE_SK);
|
|
790
840
|
|
|
791
841
|
// eslint-disable-next-line no-use-before-define
|
|
792
|
-
const status = await getProcessState(
|
|
842
|
+
const status = await getProcessState(processIdName);
|
|
793
843
|
if (status === BlockletStatus.error) {
|
|
794
|
-
throw new Error(`process ${
|
|
844
|
+
throw new Error(`process ${processIdName} is not running within 3 seconds`);
|
|
795
845
|
}
|
|
796
|
-
logger.info('done start blocklet', { processId, status, time: Date.now() - startedAt });
|
|
846
|
+
logger.info('done start blocklet', { processId: processIdName, status, time: Date.now() - startedAt });
|
|
797
847
|
|
|
798
848
|
if (nextOptions.env.connectInternalDockerNetwork) {
|
|
799
849
|
try {
|
|
800
850
|
await promiseSpawn(nextOptions.env.connectInternalDockerNetwork, { mute: true });
|
|
801
851
|
} catch (err) {
|
|
802
|
-
logger.error('blocklet connect internal docker network failed', { processId, error: err });
|
|
852
|
+
logger.error('blocklet connect internal docker network failed', { processId: processIdName, error: err });
|
|
803
853
|
}
|
|
804
854
|
}
|
|
805
855
|
|
|
@@ -807,7 +857,7 @@ const startBlockletProcess = async (
|
|
|
807
857
|
try {
|
|
808
858
|
await postStart(b, { env });
|
|
809
859
|
} catch (err) {
|
|
810
|
-
logger.error('blocklet post start failed', { processId, error: err });
|
|
860
|
+
logger.error('blocklet post start failed', { processId: processIdName, error: err });
|
|
811
861
|
}
|
|
812
862
|
};
|
|
813
863
|
|
|
@@ -842,16 +892,28 @@ const startBlockletProcess = async (
|
|
|
842
892
|
* Stop all precesses of a blocklet
|
|
843
893
|
* @param {*} blocklet should contain env props
|
|
844
894
|
*/
|
|
845
|
-
const stopBlockletProcess = (
|
|
895
|
+
const stopBlockletProcess = (
|
|
896
|
+
blocklet,
|
|
897
|
+
{ preStop = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
|
|
898
|
+
) => {
|
|
846
899
|
// eslint-disable-next-line no-use-before-define
|
|
847
|
-
return deleteBlockletProcess(blocklet, {
|
|
900
|
+
return deleteBlockletProcess(blocklet, {
|
|
901
|
+
preDelete: preStop,
|
|
902
|
+
skippedProcessIds,
|
|
903
|
+
componentDids,
|
|
904
|
+
isGreen,
|
|
905
|
+
isStopGreenAndBlue,
|
|
906
|
+
});
|
|
848
907
|
};
|
|
849
908
|
|
|
850
909
|
/**
|
|
851
910
|
* Delete all precesses of a blocklet
|
|
852
911
|
* @param {*} blocklet should contain env props
|
|
853
912
|
*/
|
|
854
|
-
const deleteBlockletProcess = async (
|
|
913
|
+
const deleteBlockletProcess = async (
|
|
914
|
+
blocklet,
|
|
915
|
+
{ preDelete = noop, skippedProcessIds = [], componentDids, isGreen = false, isStopGreenAndBlue = false } = {}
|
|
916
|
+
) => {
|
|
855
917
|
await forEachBlocklet(
|
|
856
918
|
blocklet,
|
|
857
919
|
async (b, { ancestors }) => {
|
|
@@ -875,8 +937,16 @@ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProces
|
|
|
875
937
|
return;
|
|
876
938
|
}
|
|
877
939
|
await preDelete(b, { ancestors });
|
|
940
|
+
if (isStopGreenAndBlue) {
|
|
941
|
+
// eslint-disable-next-line no-use-before-define
|
|
942
|
+
await deleteProcess(`${b.env.processId}-green`);
|
|
943
|
+
// eslint-disable-next-line no-use-before-define
|
|
944
|
+
await deleteProcess(b.env.processId);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const processId = isGreen ? `${b.env.processId}-green` : b.env.processId;
|
|
878
948
|
// eslint-disable-next-line no-use-before-define
|
|
879
|
-
await deleteProcess(
|
|
949
|
+
await deleteProcess(processId);
|
|
880
950
|
},
|
|
881
951
|
{ parallel: true }
|
|
882
952
|
);
|
|
@@ -975,57 +1045,10 @@ const validateBlockletChainInfo = (blocklet) => {
|
|
|
975
1045
|
return chainInfo;
|
|
976
1046
|
};
|
|
977
1047
|
|
|
978
|
-
const
|
|
979
|
-
blocklet
|
|
980
|
-
{ minConsecutiveTime, timeout, componentDids, setBlockletRunning } = {}
|
|
981
|
-
) => {
|
|
982
|
-
await forEachBlocklet(
|
|
983
|
-
blocklet,
|
|
984
|
-
async (b) => {
|
|
985
|
-
if (b.meta.group === BlockletGroup.gateway) {
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// components that relies on another engine component should not be checked
|
|
990
|
-
const engine = getBlockletEngine(b.meta);
|
|
991
|
-
if (engine.interpreter === 'blocklet') {
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if (!hasStartEngine(b.meta)) {
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// eslint-disable-next-line no-use-before-define
|
|
1000
|
-
if (shouldSkipComponent(b.meta.did, componentDids)) {
|
|
1001
|
-
logger.info('skip check component healthy', { processId: b.env.processId });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
|
|
1006
|
-
|
|
1007
|
-
const startedAt = Date.now();
|
|
1008
|
-
|
|
1009
|
-
// eslint-disable-next-line no-use-before-define
|
|
1010
|
-
await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal });
|
|
1011
|
-
logger.info('done check component healthy', { processId: b.env.processId, time: Date.now() - startedAt });
|
|
1012
|
-
|
|
1013
|
-
if (setBlockletRunning) {
|
|
1014
|
-
try {
|
|
1015
|
-
await setBlockletRunning(b.meta.did);
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
logger.error(`Failed to set blocklet as running for DID: ${b.meta.name || b.meta.did}`, { error });
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
},
|
|
1021
|
-
{ parallel: true }
|
|
1022
|
-
);
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, logToTerminal }) => {
|
|
1026
|
-
const { meta, ports, env } = blocklet;
|
|
1048
|
+
const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, logToTerminal, isGreen = false }) => {
|
|
1049
|
+
const { meta, ports, greenPorts, env } = blocklet;
|
|
1027
1050
|
const { name } = meta;
|
|
1028
|
-
const {
|
|
1051
|
+
const processId = isGreen ? `${env.processId}-green` : env.processId;
|
|
1029
1052
|
|
|
1030
1053
|
const webInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
|
|
1031
1054
|
const dockerInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
|
|
@@ -1048,6 +1071,7 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
|
|
|
1048
1071
|
};
|
|
1049
1072
|
|
|
1050
1073
|
let status = await getStatus();
|
|
1074
|
+
|
|
1051
1075
|
for (let i = 0; i < 20 && status !== 'online'; i++) {
|
|
1052
1076
|
const t = process.env.NODE_ENV !== 'test' ? 500 : 30;
|
|
1053
1077
|
await sleep(t);
|
|
@@ -1058,7 +1082,10 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
|
|
|
1058
1082
|
throw new Error('process not start within 10s');
|
|
1059
1083
|
}
|
|
1060
1084
|
|
|
1061
|
-
const port = findInterfacePortByName(
|
|
1085
|
+
const port = findInterfacePortByName(
|
|
1086
|
+
{ meta, ports: isGreen ? greenPorts : ports },
|
|
1087
|
+
(webInterface || dockerInterface).name
|
|
1088
|
+
);
|
|
1062
1089
|
if (logToTerminal) {
|
|
1063
1090
|
// eslint-disable-next-line no-console
|
|
1064
1091
|
console.log(
|
|
@@ -1087,6 +1114,52 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, log
|
|
|
1087
1114
|
}
|
|
1088
1115
|
};
|
|
1089
1116
|
|
|
1117
|
+
const checkBlockletProcessHealthy = async (
|
|
1118
|
+
blocklet,
|
|
1119
|
+
{ minConsecutiveTime, timeout, componentDids, setBlockletRunning, isGreen = false } = {}
|
|
1120
|
+
) => {
|
|
1121
|
+
await forEachBlocklet(
|
|
1122
|
+
blocklet,
|
|
1123
|
+
async (b) => {
|
|
1124
|
+
if (b.meta.group === BlockletGroup.gateway) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// components that relies on another engine component should not be checked
|
|
1129
|
+
const engine = getBlockletEngine(b.meta);
|
|
1130
|
+
if (engine.interpreter === 'blocklet') {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (!hasStartEngine(b.meta)) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// eslint-disable-next-line no-use-before-define
|
|
1139
|
+
if (shouldSkipComponent(b.meta.did, componentDids)) {
|
|
1140
|
+
logger.info('skip check component healthy', { processId: b.env.processId });
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
|
|
1145
|
+
|
|
1146
|
+
const startedAt = Date.now();
|
|
1147
|
+
|
|
1148
|
+
await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal, isGreen });
|
|
1149
|
+
logger.info('done check component healthy', { processId: b.env.processId, time: Date.now() - startedAt });
|
|
1150
|
+
|
|
1151
|
+
if (setBlockletRunning) {
|
|
1152
|
+
try {
|
|
1153
|
+
await setBlockletRunning(b.meta.did);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
logger.error(`Failed to set blocklet as running for DID: ${b.meta.name || b.meta.did}`, { error });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
{ parallel: true }
|
|
1160
|
+
);
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1090
1163
|
const shouldCheckHealthy = (blocklet) => {
|
|
1091
1164
|
if (blocklet.meta.group === BlockletGroup.gateway) {
|
|
1092
1165
|
return false;
|
|
@@ -1108,8 +1181,9 @@ const isBlockletPortHealthy = async (blocklet, { minConsecutiveTime = 3000, time
|
|
|
1108
1181
|
const { environments } = blocklet;
|
|
1109
1182
|
const webInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
|
|
1110
1183
|
const dockerInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_DOCKER);
|
|
1184
|
+
const key = webInterface?.port || dockerInterface?.port || 'BLOCKLET_PORT';
|
|
1111
1185
|
|
|
1112
|
-
let port =
|
|
1186
|
+
let port = blocklet.greenStatus === BlockletStatus.running ? blocklet.greenPorts?.[key] : blocklet.ports?.[key];
|
|
1113
1187
|
|
|
1114
1188
|
if (!port) {
|
|
1115
1189
|
const keyPort = webInterface?.port || dockerInterface?.port;
|
|
@@ -1354,26 +1428,6 @@ const getRuntimeInfo = async (processId) => {
|
|
|
1354
1428
|
};
|
|
1355
1429
|
};
|
|
1356
1430
|
|
|
1357
|
-
/**
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
* @param {string} dockerName
|
|
1361
|
-
* @return {Promise<{ memoryUsage?: number }>}
|
|
1362
|
-
*/
|
|
1363
|
-
const getDockerRuntimeInfoByDockerName = async (dockerName) => {
|
|
1364
|
-
try {
|
|
1365
|
-
const proc = await promiseSpawn(`docker stats --no-stream --format "{{json .}}" ${dockerName}`, { mute: true });
|
|
1366
|
-
const stats = JSON.parse(proc);
|
|
1367
|
-
return {
|
|
1368
|
-
cpuUsage: stats.CPUPerc ? parseFloat(stats.CPUPerc.replace('%', '').trim()) : 0,
|
|
1369
|
-
memoryUsage: stats.MemUsage ? xbytes.parseSize(stats.MemUsage.split('/')[0].trim()) : 0,
|
|
1370
|
-
};
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
logger.error('Get docker runtime info by docker name failed', { dockerName, error });
|
|
1373
|
-
return {};
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
|
-
|
|
1377
1431
|
/**
|
|
1378
1432
|
* merge services
|
|
1379
1433
|
* from meta.children[].mountPoints[].services, meta.children[].services
|
|
@@ -1449,7 +1503,7 @@ const getUpdateMetaList = (oldBlocklet = {}, newBlocklet = {}) => {
|
|
|
1449
1503
|
const res = [];
|
|
1450
1504
|
|
|
1451
1505
|
forEachChildSync(newBlocklet, (b, { id }) => {
|
|
1452
|
-
if (b.bundleSource && semver.gt(b.meta.version, oldMap[id])) {
|
|
1506
|
+
if ((b.bundleSource && semver.gt(b.meta.version, oldMap[id])) || process.env.TEST_UPDATE_ALL_BLOCKLET === 'true') {
|
|
1453
1507
|
res.push({ id, meta: b.meta });
|
|
1454
1508
|
}
|
|
1455
1509
|
});
|
|
@@ -2254,8 +2308,8 @@ const getBlockletStatus = (blocklet) => {
|
|
|
2254
2308
|
return;
|
|
2255
2309
|
}
|
|
2256
2310
|
|
|
2257
|
-
if (isRunning(component.status)) {
|
|
2258
|
-
runningStatus =
|
|
2311
|
+
if (isRunning(component.status) || isRunning(component.greenStatus)) {
|
|
2312
|
+
runningStatus = BlockletStatus.running;
|
|
2259
2313
|
return;
|
|
2260
2314
|
}
|
|
2261
2315
|
|
|
@@ -2289,25 +2343,35 @@ const ensureAppPortsNotOccupied = async ({
|
|
|
2289
2343
|
states,
|
|
2290
2344
|
manager,
|
|
2291
2345
|
checkPortsFn = isPortsOccupiedByOtherProcess,
|
|
2346
|
+
isGreen = false,
|
|
2292
2347
|
}) => {
|
|
2293
2348
|
const { did } = blocklet.meta;
|
|
2294
|
-
const
|
|
2349
|
+
const dids = new Set();
|
|
2295
2350
|
await forEachComponentV2(blocklet, async (b) => {
|
|
2296
2351
|
try {
|
|
2297
2352
|
if (shouldSkipComponent(b.meta.did, inputDids)) {
|
|
2298
2353
|
return;
|
|
2299
2354
|
}
|
|
2300
|
-
|
|
2301
2355
|
const { processId } = b.env;
|
|
2302
|
-
const { ports } = b;
|
|
2356
|
+
const { ports, greenPorts } = b;
|
|
2357
|
+
|
|
2358
|
+
if (
|
|
2359
|
+
await checkPortsFn({
|
|
2360
|
+
ports: greenPorts || ports,
|
|
2361
|
+
pm2ProcessId: isGreen ? `${processId}-green` : processId,
|
|
2362
|
+
printError: logger.error.bind(logger),
|
|
2363
|
+
})
|
|
2364
|
+
) {
|
|
2365
|
+
dids.add(b.meta.did);
|
|
2366
|
+
}
|
|
2303
2367
|
if (
|
|
2304
2368
|
await checkPortsFn({
|
|
2305
2369
|
ports,
|
|
2306
|
-
pm2ProcessId: processId,
|
|
2370
|
+
pm2ProcessId: !isGreen ? `${processId}-green` : processId,
|
|
2307
2371
|
printError: logger.error.bind(logger),
|
|
2308
2372
|
})
|
|
2309
2373
|
) {
|
|
2310
|
-
|
|
2374
|
+
dids.add(b.meta.did);
|
|
2311
2375
|
}
|
|
2312
2376
|
} catch (error) {
|
|
2313
2377
|
logger.error('Failed to check ports occupied', { error });
|
|
@@ -2315,8 +2379,9 @@ const ensureAppPortsNotOccupied = async ({
|
|
|
2315
2379
|
});
|
|
2316
2380
|
|
|
2317
2381
|
let newBlocklet = blocklet;
|
|
2382
|
+
const componentDids = Array.from(dids);
|
|
2318
2383
|
if (componentDids.length) {
|
|
2319
|
-
await states.blocklet.refreshBlockletPorts(did, componentDids);
|
|
2384
|
+
await states.blocklet.refreshBlockletPorts(did, componentDids, isGreen);
|
|
2320
2385
|
logger.info('refresh component ports', { did, componentDids });
|
|
2321
2386
|
await manager._updateBlockletEnvironment(did);
|
|
2322
2387
|
newBlocklet = await manager.ensureBlocklet(did);
|
|
@@ -2521,6 +2586,16 @@ const copyPackImages = async ({ appDataDir, packDir, packConfig = {} }) => {
|
|
|
2521
2586
|
*/
|
|
2522
2587
|
const isDevelopmentMode = (blocklet) => blocklet?.mode === BLOCKLET_MODES.DEVELOPMENT;
|
|
2523
2588
|
|
|
2589
|
+
const getHookArgs = (blocklet) => ({
|
|
2590
|
+
output: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'output.log'),
|
|
2591
|
+
error: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'error.log'),
|
|
2592
|
+
timeout:
|
|
2593
|
+
Math.max(
|
|
2594
|
+
get(blocklet, 'meta.timeout.script', 120),
|
|
2595
|
+
...(blocklet?.children || []).map((child) => child.meta?.timeout?.script || 0)
|
|
2596
|
+
) * 1000,
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2524
2599
|
module.exports = {
|
|
2525
2600
|
updateBlockletFallbackLogo,
|
|
2526
2601
|
forEachBlocklet,
|
|
@@ -2532,6 +2607,7 @@ module.exports = {
|
|
|
2532
2607
|
getAppOverwrittenEnvironments,
|
|
2533
2608
|
getComponentSystemEnvironments,
|
|
2534
2609
|
getRuntimeEnvironments,
|
|
2610
|
+
getHookArgs,
|
|
2535
2611
|
validateBlocklet,
|
|
2536
2612
|
validateBlockletChainInfo,
|
|
2537
2613
|
fillBlockletConfigs,
|
|
@@ -2551,7 +2627,6 @@ module.exports = {
|
|
|
2551
2627
|
pruneBlockletBundle,
|
|
2552
2628
|
getDiskInfo,
|
|
2553
2629
|
getRuntimeInfo,
|
|
2554
|
-
getDockerRuntimeInfoByDockerName,
|
|
2555
2630
|
mergeMeta,
|
|
2556
2631
|
getUpdateMetaList,
|
|
2557
2632
|
getTypeFromInstallParams,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 是否是自定义 docker 镜像, 并且是否挂载了数据卷
|
|
2
|
+
function isDockerOnlySingleInstance(meta = {}) {
|
|
3
|
+
if (!meta.docker) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
// 保留字段, 用于描述 docker 镜像支持多实例运行
|
|
7
|
+
if (meta.docker.allowMultipleInstances) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (meta.docker.RunBaseScript && meta.docker.installNodeModules) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return !!meta.docker.volume;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { isDockerOnlySingleInstance };
|
package/lib/util/org.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const { ROLES, SERVER_ROLES } = require('@abtnode/constant');
|
|
2
|
+
const { getAppUrl } = require('@blocklet/meta/lib/util');
|
|
3
|
+
const { CustomError } = require('@blocklet/error');
|
|
4
|
+
const { joinURL, withQuery } = require('ufo');
|
|
5
|
+
const { WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
|
|
6
|
+
const get = require('lodash/get');
|
|
7
|
+
|
|
8
|
+
const { isAdminPath, getEndpoint } = require('./verify-access-key-user');
|
|
9
|
+
|
|
10
|
+
const MAX_PAGE_SIZE = 100;
|
|
11
|
+
|
|
12
|
+
const formatPaging = (paging) => {
|
|
13
|
+
const { pageSize = 20, page = 1 } = paging || {};
|
|
14
|
+
const newPageSize = Math.max(1, Math.min(MAX_PAGE_SIZE, parseInt(pageSize, 10)));
|
|
15
|
+
const newPage = Math.max(1, parseInt(page, 10));
|
|
16
|
+
return { pageSize: newPageSize, page: newPage };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isAdminUser = (user) => {
|
|
20
|
+
const { role = '' } = user || {};
|
|
21
|
+
if (!role) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return [ROLES.ADMIN, ROLES.OWNER, SERVER_ROLES.BLOCKLET_ADMIN, SERVER_ROLES.BLOCKLET_OWNER].includes(role);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isOrgOwner = (user, org) => {
|
|
28
|
+
const { did } = user || {};
|
|
29
|
+
if (!org) {
|
|
30
|
+
throw new CustomError(403, 'Org not found');
|
|
31
|
+
}
|
|
32
|
+
return org.ownerDid === did;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isAdmingPath = (context = {}) => {
|
|
36
|
+
try {
|
|
37
|
+
const pathname = getEndpoint(context);
|
|
38
|
+
return isAdminPath(pathname);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getOrgInviteLink = (inviteInfo, blocklet) => {
|
|
45
|
+
const { inviteId } = inviteInfo || {};
|
|
46
|
+
const appUrl = getAppUrl(blocklet);
|
|
47
|
+
if (!inviteId || !appUrl) {
|
|
48
|
+
throw new CustomError(500, 'Invitation link creation failed, please try again later');
|
|
49
|
+
}
|
|
50
|
+
return withQuery(joinURL(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX, 'invite'), { inviteId });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const createOrgValidators = (blocklet = {}) => {
|
|
54
|
+
function getOrgSettings() {
|
|
55
|
+
const org = get(blocklet, 'settings.org', { enabled: false, maxMemberPerOrg: 100, maxOrgPerUser: 10 });
|
|
56
|
+
return org || { enabled: false, maxMemberPerOrg: 100, maxOrgPerUser: 10 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const org = getOrgSettings();
|
|
60
|
+
|
|
61
|
+
function verifyOrgEnabled() {
|
|
62
|
+
if (!org?.enabled) {
|
|
63
|
+
throw new CustomError(403, 'Org is not enabled');
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function veriftMaxMemberPerOrg(number) {
|
|
69
|
+
verifyOrgEnabled();
|
|
70
|
+
if (number >= org.maxMemberPerOrg) {
|
|
71
|
+
throw new CustomError(403, `The current org can invite up to ${org.maxMemberPerOrg} members`);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function veriftMaxOrgPerUser(number) {
|
|
77
|
+
verifyOrgEnabled();
|
|
78
|
+
if (number >= org.maxOrgPerUser) {
|
|
79
|
+
throw new CustomError(403, `You can create up to ${org.maxOrgPerUser} orgs`);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
verifyOrgEnabled,
|
|
86
|
+
veriftMaxMemberPerOrg,
|
|
87
|
+
veriftMaxOrgPerUser,
|
|
88
|
+
getOrgSettings,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
formatPaging,
|
|
94
|
+
isAdminUser,
|
|
95
|
+
isOrgOwner,
|
|
96
|
+
isAdmingPath,
|
|
97
|
+
getOrgInviteLink,
|
|
98
|
+
createOrgValidators,
|
|
99
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { Joi } = require('@arcblock/validator');
|
|
2
|
+
|
|
3
|
+
const createOrgInputSchema = Joi.object({
|
|
4
|
+
name: Joi.string().required().trim().min(1).max(20),
|
|
5
|
+
description: Joi.string().optional().allow('').trim().min(1).max(255),
|
|
6
|
+
ownerDid: Joi.DID().optional().allow('').allow(null),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 只允许更新 name 和 description
|
|
11
|
+
*/
|
|
12
|
+
const updateOrgInputSchema = Joi.object({
|
|
13
|
+
id: Joi.string().required(),
|
|
14
|
+
name: Joi.string().required().trim().min(1).max(20),
|
|
15
|
+
description: Joi.string().optional().allow('').trim().min(1).max(255),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
exports.createOrgInputSchema = createOrgInputSchema;
|
|
19
|
+
exports.updateOrgInputSchema = updateOrgInputSchema;
|