querysub 0.259.0 → 0.261.0
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/.cursorrules +2 -0
- package/package.json +1 -1
- package/src/-e-certs/EdgeCertController.ts +1 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
- package/src/3-path-functions/PathFunctionHelpers.ts +5 -6
- package/src/3-path-functions/PathFunctionRunner.ts +10 -3
- package/src/3-path-functions/PathFunctionRunnerMain.ts +2 -4
- package/src/4-deploy/deployFunctions.ts +82 -0
- package/src/4-deploy/deployGetFunctionsInner.ts +94 -0
- package/src/4-deploy/deployMain.ts +9 -109
- package/src/4-deploy/edgeBootstrap.ts +3 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -0
- package/src/4-deploy/edgeNodes.ts +7 -1
- package/src/4-querysub/Querysub.ts +45 -6
- package/src/deployManager/MachinesPage.tsx +3 -0
- package/src/deployManager/components/DeployPage.tsx +385 -0
- package/src/deployManager/components/DeployProgressView.tsx +135 -0
- package/src/deployManager/components/MachinesListPage.tsx +10 -9
- package/src/deployManager/components/ServiceDetailPage.tsx +2 -1
- package/src/deployManager/components/ServicesListPage.tsx +2 -2
- package/src/deployManager/components/deployButtons.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +1 -0
- package/src/deployManager/machineController.ts +1 -0
- package/src/deployManager/machineSchema.ts +77 -3
- package/src/deployManager/spec.txt +22 -17
- package/src/deployManager/urlParams.ts +1 -1
- package/src/diagnostics/NodeViewer.tsx +17 -2
- package/src/library-components/ATag.tsx +14 -9
- package/src/misc/formatJSX.tsx +1 -1
- package/src/misc.ts +8 -0
- package/src/server.ts +10 -10
package/.cursorrules
CHANGED
|
@@ -49,6 +49,8 @@ Don't use switch statements. Use if statements instead.
|
|
|
49
49
|
|
|
50
50
|
Don't use ! when accessing a value from a map. Use the get / if undefined initialize and set, and then use style. It's faster, and more type safe.
|
|
51
51
|
|
|
52
|
+
NEVER use the non-null assertion operator. Null check correctly, using const if required to preserve the assertion.
|
|
53
|
+
|
|
52
54
|
Sort with this function, which takes a single function to map each object to a sortable value
|
|
53
55
|
import { sort } from "socket-function/src/misc";
|
|
54
56
|
export function sort<T>(arr: T[], sortKey: (obj: T) => unknown);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { getArchives } from "../-a-archives/archives";
|
|
3
|
-
import { getDomain, isDevDebugbreak, isNoNetwork } from "../config";
|
|
3
|
+
import { getDomain, isDevDebugbreak, isNoNetwork, isPublic } from "../config";
|
|
4
4
|
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
5
5
|
import { isNode, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
6
6
|
import { errorToUndefinedSilent, ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../errors";
|
|
@@ -178,8 +178,8 @@ function setNodeIds(nodeIds: string[]) {
|
|
|
178
178
|
nodeIds = nodeIds.filter(x => x !== SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE);
|
|
179
179
|
|
|
180
180
|
diskLog("setNodeIds", { nodeIds });
|
|
181
|
-
// Also try all localhost ports, if we are in
|
|
182
|
-
if (isNode() && isDevDebugbreak()) {
|
|
181
|
+
// Also try all localhost ports, if we are developing and not in public mode
|
|
182
|
+
if (isNode() && !isPublic() && isDevDebugbreak()) {
|
|
183
183
|
let ports = new Set(nodeIds.map(nodeId => decodeNodeId(nodeId)?.port).filter(isDefined));
|
|
184
184
|
for (let port of ports) {
|
|
185
185
|
let localNodeId = getNodeId("127-0-0-1." + getDomain(), port);
|
|
@@ -20,7 +20,6 @@ import { interceptCalls } from "../-0-hooks/hooks";
|
|
|
20
20
|
// NOTE: We could deploy single functions, but... we will almost always be updating all functions at
|
|
21
21
|
// once, because keeping everything on the same git hash reduces a lot of potential bugs.
|
|
22
22
|
export async function replaceFunctions(config: {
|
|
23
|
-
domainName: string;
|
|
24
23
|
functions: FunctionSpec[];
|
|
25
24
|
}) {
|
|
26
25
|
await proxyWatcher.commitFunction({
|
|
@@ -30,12 +29,12 @@ export async function replaceFunctions(config: {
|
|
|
30
29
|
return `${func.DomainName}:${func.FilePath}:${func.ModuleId}:${func.FunctionId}`;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
let {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
32
|
+
let { functions } = config;
|
|
33
|
+
let domainsNames = new Set(functions.map(x => x.DomainName));
|
|
34
|
+
if (domainsNames.size !== 1) {
|
|
35
|
+
throw new Error(`Tried to deploy functions with multiple domains, ${JSON.stringify(domainsNames)}`);
|
|
38
36
|
}
|
|
37
|
+
let domainName = Array.from(domainsNames)[0];
|
|
39
38
|
|
|
40
39
|
let base = functionSchema()[domainName].PathFunctionRunner;
|
|
41
40
|
let previousFunctions = Object.values(base).flatMap(x => Object.values(x.Sources)).filter(isDefined);
|
|
@@ -24,6 +24,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
24
24
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
25
25
|
import { getDomain, isLocal } from "../config";
|
|
26
26
|
import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
|
|
27
|
+
import { DeployProgress } from "../4-deploy/deployFunctions";
|
|
27
28
|
|
|
28
29
|
export const functionSchema = rawSchema<{
|
|
29
30
|
[domainName: string]: {
|
|
@@ -683,12 +684,18 @@ export class PathFunctionRunner {
|
|
|
683
684
|
}
|
|
684
685
|
}
|
|
685
686
|
|
|
686
|
-
export async function preloadFunctions(specs: FunctionSpec[]) {
|
|
687
|
+
export async function preloadFunctions(specs: FunctionSpec[], progress?: DeployProgress) {
|
|
688
|
+
progress?.({ section: "Finding FunctionPreloadControllers", progress: 0 });
|
|
687
689
|
let nodeIds = await getControllerNodeIdList(FunctionPreloadController);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
+
progress?.({ section: "Finding FunctionPreloadControllers", progress: 1 });
|
|
691
|
+
await Promise.allSettled(nodeIds.map(async nodeObj => {
|
|
692
|
+
let nodeId = nodeObj.nodeId;
|
|
693
|
+
let controller = FunctionPreloadController.nodes[nodeId];
|
|
694
|
+
let section = `${nodeObj.entryPoint}:${nodeId}|Preloading Functions`;
|
|
695
|
+
progress?.({ section, progress: 0 });
|
|
690
696
|
console.log(blue(`Preloading functions on ${String(nodeId)}`));
|
|
691
697
|
await errorToUndefined(controller.preloadFunctions(specs));
|
|
698
|
+
progress?.({ section, progress: 1 });
|
|
692
699
|
console.log(blue(`Finished preloading functions on ${String(nodeId)}`));
|
|
693
700
|
}));
|
|
694
701
|
}
|
|
@@ -19,10 +19,10 @@ import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
|
19
19
|
import { PermissionsCheck } from "../4-querysub/permissions";
|
|
20
20
|
import { timeInMinute } from "socket-function/src/misc";
|
|
21
21
|
import { getDomain, isLocal, isPublic } from "../config";
|
|
22
|
-
import { publishMachineARecords } from "../-e-certs/EdgeCertController";
|
|
23
22
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
24
23
|
import { parseFilterSelector } from "../misc/filterable";
|
|
25
24
|
import path from "path";
|
|
25
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
26
26
|
|
|
27
27
|
async function main() {
|
|
28
28
|
Error.stackTraceLimit = 20;
|
|
@@ -37,9 +37,7 @@ async function main() {
|
|
|
37
37
|
PathFunctionRunner.DEBUG_CALLS = true;
|
|
38
38
|
// debugCoreMode();
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
await SocketFunction.mount({ port: 0, ...keyPair, public: isPublic() });
|
|
42
|
-
await publishMachineARecords();
|
|
40
|
+
await Querysub.hostService("PathFunctionRunnerMain");
|
|
43
41
|
|
|
44
42
|
// Use a fairly high stick time (the default is 10s), because having wait to sync data is very slow,
|
|
45
43
|
// and the function runner SHOULD have more memory than the clients, and much faster network speeds
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { magenta } from "socket-function/src/formatting/logColors";
|
|
2
|
+
import { FunctionSpec, preloadFunctions } from "../3-path-functions/PathFunctionRunner";
|
|
3
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
4
|
+
import { errorify } from "../errors";
|
|
5
|
+
import { runPromise } from "../functional/runCommand";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { preloadUI } from "./edgeNodes";
|
|
8
|
+
import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
9
|
+
import { replaceFunctions } from "../3-path-functions/PathFunctionHelpers";
|
|
10
|
+
import { setLiveDeployedHash } from "./deploySchema";
|
|
11
|
+
|
|
12
|
+
export async function deployGetFunctions(): Promise<FunctionSpec[]> {
|
|
13
|
+
let innerPath = path.resolve(__dirname + "/deployGetFunctionsInner.ts").replaceAll("\\", "/");
|
|
14
|
+
let result = await runPromise(`yarn typenode ${innerPath}`);
|
|
15
|
+
let functionSpecs: FunctionSpec[] = [];
|
|
16
|
+
for (let line of result.split("\n")) {
|
|
17
|
+
if (line.startsWith("ERROR:")) {
|
|
18
|
+
throw errorify(line.slice("ERROR:".length));
|
|
19
|
+
}
|
|
20
|
+
if (line.startsWith("FUNCTION_SPEC:")) {
|
|
21
|
+
let spec = JSON.parse(line.slice("FUNCTION_SPEC:".length));
|
|
22
|
+
functionSpecs.push(spec);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return functionSpecs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//todonext;
|
|
29
|
+
// Ugh... some kind of throttling? Nah...
|
|
30
|
+
// So... progress sections, just... start and stop?
|
|
31
|
+
export type DeployProgress = {
|
|
32
|
+
(config: { section: string; progress: number; }): void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function deployFunctions(config: {
|
|
36
|
+
functionSpecs: FunctionSpec[];
|
|
37
|
+
notifyRefreshDelay: number;
|
|
38
|
+
deployOnlyCode?: boolean;
|
|
39
|
+
deployOnlyUI?: boolean;
|
|
40
|
+
progress?: DeployProgress;
|
|
41
|
+
}) {
|
|
42
|
+
const { functionSpecs, notifyRefreshDelay, deployOnlyCode, deployOnlyUI } = config;
|
|
43
|
+
let gitRefs = new Set(functionSpecs.map(x => x.gitRef));
|
|
44
|
+
if (gitRefs.size !== 1) {
|
|
45
|
+
throw new Error(`Tried to deploy functions with multiple git refs, ${JSON.stringify(gitRefs)}`);
|
|
46
|
+
}
|
|
47
|
+
let gitRef = Array.from(gitRefs)[0];
|
|
48
|
+
const currentFunctions = functionSpecs;
|
|
49
|
+
|
|
50
|
+
console.log();
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(magenta(`Preloading ${currentFunctions.length} functions & UI`));
|
|
53
|
+
await Promise.all([
|
|
54
|
+
preloadFunctions(currentFunctions, config.progress),
|
|
55
|
+
preloadUI(gitRef, config.progress),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
console.log();
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(magenta(`Deploying ${currentFunctions.length} functions`));
|
|
61
|
+
|
|
62
|
+
let refreshThresholdTime = Date.now() + notifyRefreshDelay;
|
|
63
|
+
|
|
64
|
+
config.progress?.({ section: "Deploying Functions", progress: 0 });
|
|
65
|
+
await proxyWatcher.commitFunction({
|
|
66
|
+
debugName: "setLiveDeployedHash",
|
|
67
|
+
inlineNestedWatchers: true,
|
|
68
|
+
watchFunction: () => {
|
|
69
|
+
if (deployOnlyUI) {
|
|
70
|
+
void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
|
|
71
|
+
} else if (deployOnlyCode) {
|
|
72
|
+
void replaceFunctions({ functions: currentFunctions, });
|
|
73
|
+
} else {
|
|
74
|
+
void replaceFunctions({ functions: currentFunctions, });
|
|
75
|
+
void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
config.progress?.({ section: "Deploying Functions", progress: 1 });
|
|
80
|
+
|
|
81
|
+
console.log(magenta(`FINISHED DEPLOY (${gitRef})`));
|
|
82
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { deployBlock } from "./deployBlock";
|
|
3
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
4
|
+
import { getThreadKeyCert } from "../-a-auth/certs";
|
|
5
|
+
import { getDomain, isPublic } from "../config";
|
|
6
|
+
import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
|
|
7
|
+
import { getSchemaObject, getModuleRelativePath, PERMISSIONS_FUNCTION_ID, getExportPath } from "../3-path-functions/syncSchema";
|
|
8
|
+
import { getPathStr2 } from "../path";
|
|
9
|
+
import { FunctionSpec } from "../3-path-functions/PathFunctionRunner";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import { getGitRefLive, getGitURLLive } from "./git";
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
let domainName = getDomain();
|
|
15
|
+
if (!domainName) {
|
|
16
|
+
throw new Error(`No domain found, set in querysub.json { domain: "example.com" }, or in --domain "example.com"`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await deployBlock();
|
|
20
|
+
|
|
21
|
+
// Mount, so we can write directly to the database
|
|
22
|
+
await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
|
|
23
|
+
|
|
24
|
+
let folderRoot = path.resolve(".").replaceAll("\\", "/");
|
|
25
|
+
const deployPath = path.resolve("./deploy.ts");
|
|
26
|
+
await import(deployPath);
|
|
27
|
+
|
|
28
|
+
const querysubRoot = path.resolve(__dirname + "/../").replaceAll("\\", "/");
|
|
29
|
+
|
|
30
|
+
let gitDir = folderRoot;
|
|
31
|
+
if (!fs.existsSync(gitDir + "/.git")) {
|
|
32
|
+
throw new Error(`No .git folder found at ${JSON.stringify(gitDir + "/.git")}`);
|
|
33
|
+
}
|
|
34
|
+
let gitURL = await getGitURLLive(gitDir);
|
|
35
|
+
if (!gitURL) {
|
|
36
|
+
throw new Error(`Git repo has no remote url, at ${JSON.stringify(gitDir + "/.git")}`);
|
|
37
|
+
}
|
|
38
|
+
let gitRef = await getGitRefLive(gitDir);
|
|
39
|
+
|
|
40
|
+
let usedModules = new Map<string, string>();
|
|
41
|
+
for (let module of Object.values(require.cache)) {
|
|
42
|
+
if (!module) continue;
|
|
43
|
+
|
|
44
|
+
// NOTE: We don't want dependencies to deploy schemas. This would result in using an
|
|
45
|
+
// API also deploying it.
|
|
46
|
+
// - Also, this add more security, by preventing 3rd party libraries from secretly adding apis
|
|
47
|
+
// (maybe even just innocently, for diagnostics), which might have vulnerabilities.
|
|
48
|
+
let isAllowedToDeploy = (
|
|
49
|
+
!module.filename.includes("node_modules")
|
|
50
|
+
&& module.filename.startsWith(folderRoot)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// We DO allow querysub to deploy, so we can deploy diagnostic functions, such as
|
|
54
|
+
// for the managment interfaces. It only works if the user opts in (by calling
|
|
55
|
+
// registerManagementPages2), so it is actually quite safe to do this.
|
|
56
|
+
if (module.filename.startsWith(querysubRoot)) {
|
|
57
|
+
isAllowedToDeploy = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isAllowedToDeploy) continue;
|
|
61
|
+
let schema = getSchemaObject(module);
|
|
62
|
+
if (!schema) continue;
|
|
63
|
+
// If it's local, we don't need to deploy it.
|
|
64
|
+
// (And shouldn't?, as we don't want to run local functions by request?)
|
|
65
|
+
if (schema.domainName === LOCAL_DOMAIN) continue;
|
|
66
|
+
|
|
67
|
+
let filePath = getModuleRelativePath(module);
|
|
68
|
+
|
|
69
|
+
let moduleId = getPathStr2(domainName, filePath);
|
|
70
|
+
let otherUsed = usedModules.get(moduleId);
|
|
71
|
+
if (otherUsed) {
|
|
72
|
+
throw new Error(`Overlapping module ids for ${domainName}.${filePath}, at ${otherUsed} and ${filePath}`);
|
|
73
|
+
}
|
|
74
|
+
usedModules.set(moduleId, filePath);
|
|
75
|
+
|
|
76
|
+
const functionIds = Object.keys(schema.rawFunctions);
|
|
77
|
+
functionIds.push(PERMISSIONS_FUNCTION_ID);
|
|
78
|
+
for (let functionId of functionIds) {
|
|
79
|
+
const exportPathStr = getExportPath(functionId);
|
|
80
|
+
let spec: FunctionSpec = {
|
|
81
|
+
DomainName: domainName,
|
|
82
|
+
ModuleId: schema.moduleId,
|
|
83
|
+
FilePath: filePath,
|
|
84
|
+
FunctionId: functionId,
|
|
85
|
+
exportPathStr,
|
|
86
|
+
gitURL,
|
|
87
|
+
gitRef,
|
|
88
|
+
};
|
|
89
|
+
console.log(`FUNCTION_SPEC:${JSON.stringify(spec)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch(e => console.error("ERROR:" + String(e?.stack || e).replaceAll("\n", "\\n"))).finally(() => process.exit(0));
|
|
@@ -25,6 +25,8 @@ import { preloadUI } from "./edgeNodes";
|
|
|
25
25
|
import { shutdown } from "../diagnostics/periodic";
|
|
26
26
|
import { delay } from "socket-function/src/batching";
|
|
27
27
|
import { waitForImportBlockers } from "../3-path-functions/pathFunctionLoader";
|
|
28
|
+
import { deployFunctions, deployGetFunctions } from "./deployFunctions";
|
|
29
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
28
30
|
|
|
29
31
|
let yargObj = yargs(process.argv)
|
|
30
32
|
.option("deployonlycode", { type: "boolean", desc: "Only deploy code, not ui" })
|
|
@@ -41,88 +43,10 @@ export async function deployMain() {
|
|
|
41
43
|
//quietCoreMode();
|
|
42
44
|
//ClientWatcher.DEBUG_READS = true;
|
|
43
45
|
//ClientWatcher.DEBUG_WRITES = true;
|
|
44
|
-
let domainName = getDomain();
|
|
45
|
-
if (!domainName) {
|
|
46
|
-
throw new Error(`No domain found, set in querysub.json { domain: "example.com" }, or in --domain "example.com"`);
|
|
47
|
-
}
|
|
48
|
-
await deployBlock();
|
|
49
|
-
|
|
50
|
-
// Mount, so we can write directly to the database
|
|
51
|
-
await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
|
|
52
|
-
|
|
53
|
-
let folderRoot = path.resolve(".").replaceAll("\\", "/");
|
|
54
|
-
const deployPath = path.resolve("./deploy.ts");
|
|
55
|
-
require(deployPath);
|
|
56
|
-
|
|
57
|
-
// Wait for Promise.resolve imports to import
|
|
58
|
-
await waitForImportBlockers();
|
|
59
|
-
|
|
60
|
-
const srcRoot = path.resolve(__dirname + "/../").replaceAll("\\", "/");
|
|
61
|
-
|
|
62
|
-
let currentFunctions: FunctionSpec[] = [];
|
|
63
|
-
|
|
64
|
-
let gitDir = folderRoot;
|
|
65
|
-
if (!fs.existsSync(gitDir + "/.git")) {
|
|
66
|
-
throw new Error(`No .git folder found at ${JSON.stringify(gitDir + "/.git")}`);
|
|
67
|
-
}
|
|
68
|
-
let gitURL = await getGitURLLive(gitDir);
|
|
69
|
-
if (!gitURL) {
|
|
70
|
-
throw new Error(`Git repo has no remote url, at ${JSON.stringify(gitDir + "/.git")}`);
|
|
71
|
-
}
|
|
72
|
-
let gitRef = await getGitRefLive(gitDir);
|
|
73
|
-
|
|
74
|
-
let usedModules = new Map<string, string>();
|
|
75
46
|
|
|
76
|
-
|
|
77
|
-
if (!module) continue;
|
|
47
|
+
await Querysub.hostService("deployMain");
|
|
78
48
|
|
|
79
|
-
|
|
80
|
-
// API also deploying it.
|
|
81
|
-
// - Also, this add more security, by preventing 3rd party libraries from adding apis
|
|
82
|
-
// (maybe even just innocent, for diagnostics), which might have vulnerabilities.
|
|
83
|
-
let isAllowedToDeploy = (
|
|
84
|
-
!module.filename.includes("node_modules")
|
|
85
|
-
&& module.filename.startsWith(folderRoot)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// We DO allow shard to deploy, so we can deploy diagnostic functions, such as
|
|
89
|
-
// for the managment interfaces. It only works if the user opts in (by calling
|
|
90
|
-
// registerManagementPages2), so it is actually quite safe to do this.
|
|
91
|
-
if (module.filename.startsWith(srcRoot)) {
|
|
92
|
-
isAllowedToDeploy = true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!isAllowedToDeploy) continue;
|
|
96
|
-
let schema = getSchemaObject(module);
|
|
97
|
-
if (!schema) continue;
|
|
98
|
-
// If it's local, we don't need to deploy it.
|
|
99
|
-
// (And shouldn't?, as we don't want to run local functions by request?)
|
|
100
|
-
if (schema.domainName === LOCAL_DOMAIN) continue;
|
|
101
|
-
|
|
102
|
-
let filePath = getModuleRelativePath(module);
|
|
103
|
-
|
|
104
|
-
let moduleId = getPathStr2(domainName, filePath);
|
|
105
|
-
let otherUsed = usedModules.get(moduleId);
|
|
106
|
-
if (otherUsed) {
|
|
107
|
-
throw new Error(`Overlapping module ids for ${domainName}.${filePath}, at ${otherUsed} and ${filePath}`);
|
|
108
|
-
}
|
|
109
|
-
usedModules.set(moduleId, filePath);
|
|
110
|
-
|
|
111
|
-
const functionIds = Object.keys(schema.rawFunctions);
|
|
112
|
-
functionIds.push(PERMISSIONS_FUNCTION_ID);
|
|
113
|
-
for (let functionId of functionIds) {
|
|
114
|
-
const exportPathStr = getExportPath(functionId);
|
|
115
|
-
currentFunctions.push({
|
|
116
|
-
DomainName: domainName,
|
|
117
|
-
ModuleId: schema.moduleId,
|
|
118
|
-
FilePath: filePath,
|
|
119
|
-
FunctionId: functionId,
|
|
120
|
-
exportPathStr,
|
|
121
|
-
gitURL,
|
|
122
|
-
gitRef,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
49
|
+
const currentFunctions = await deployGetFunctions();
|
|
126
50
|
for (let fnc of currentFunctions) {
|
|
127
51
|
console.log(blue(`${fnc.DomainName}.${fnc.ModuleId}.${fnc.FunctionId}`));
|
|
128
52
|
}
|
|
@@ -131,37 +55,13 @@ export async function deployMain() {
|
|
|
131
55
|
return;
|
|
132
56
|
}
|
|
133
57
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
preloadUI(gitRef),
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
console.log();
|
|
143
|
-
console.log();
|
|
144
|
-
console.log(magenta(`Deploying ${currentFunctions.length} functions`));
|
|
145
|
-
|
|
146
|
-
let refreshThresholdTime = Date.now() + yargObj.notifyrefreshdelay;
|
|
147
|
-
|
|
148
|
-
await proxyWatcher.commitFunction({
|
|
149
|
-
debugName: "setLiveDeployedHash",
|
|
150
|
-
inlineNestedWatchers: true,
|
|
151
|
-
watchFunction: () => {
|
|
152
|
-
if (yargObj.deployonlycode) {
|
|
153
|
-
void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
|
|
154
|
-
} else if (yargObj.deployonlyui) {
|
|
155
|
-
void replaceFunctions({ domainName, functions: currentFunctions, });
|
|
156
|
-
} else {
|
|
157
|
-
void replaceFunctions({ domainName, functions: currentFunctions, });
|
|
158
|
-
void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
|
|
159
|
-
}
|
|
160
|
-
},
|
|
58
|
+
await deployFunctions({
|
|
59
|
+
functionSpecs: currentFunctions,
|
|
60
|
+
notifyRefreshDelay: yargObj.notifyrefreshdelay,
|
|
61
|
+
deployOnlyCode: yargObj.deployonlycode,
|
|
62
|
+
deployOnlyUI: yargObj.deployonlyui,
|
|
161
63
|
});
|
|
162
64
|
|
|
163
|
-
console.log(magenta(`FINISHED DEPLOY (${gitRef})`));
|
|
164
|
-
|
|
165
65
|
await shutdown();
|
|
166
66
|
}
|
|
167
67
|
|
|
@@ -429,6 +429,9 @@ async function edgeNodeFunction(config: {
|
|
|
429
429
|
let host = edgeNodeConfig.host;
|
|
430
430
|
if (!host.endsWith("/")) host += "/";
|
|
431
431
|
for (let entryPath of edgeNodeConfig.entryPaths) {
|
|
432
|
+
if (entryPath.startsWith("/")) {
|
|
433
|
+
entryPath = entryPath.slice("/".length);
|
|
434
|
+
}
|
|
432
435
|
await require("https://" + host + entryPath);
|
|
433
436
|
}
|
|
434
437
|
}
|
|
@@ -24,6 +24,9 @@ export function startEdgeNotifier() {
|
|
|
24
24
|
if (liveHash === lastHashServer) return;
|
|
25
25
|
let refreshThresholdTime = atomicObjectRead(deploySchema()[getDomain()].deploy.live.refreshThresholdTime) || timeInMinute;
|
|
26
26
|
lastHashServer = liveHash;
|
|
27
|
+
if (!curHash) {
|
|
28
|
+
curHash = liveHash;
|
|
29
|
+
}
|
|
27
30
|
void notifyClients(liveHash, refreshThresholdTime);
|
|
28
31
|
});
|
|
29
32
|
}
|
|
@@ -52,6 +55,7 @@ function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
|
|
|
52
55
|
let prevHash = curHash;
|
|
53
56
|
let notifyIntervals = [0, 0.1, 0.5, 1];
|
|
54
57
|
console.log(blue(`Client liveHash changed ${liveHash}, prev hash: ${prevHash}`));
|
|
58
|
+
// If we are replacing an already existing notification, don't show immediately
|
|
55
59
|
let skipFirst = false;
|
|
56
60
|
if (currentNotification) {
|
|
57
61
|
currentNotification.close();
|
|
@@ -25,6 +25,7 @@ import { Querysub } from "../4-querysub/QuerysubController";
|
|
|
25
25
|
import { onEdgeNodesChanged } from "./edgeBootstrap";
|
|
26
26
|
import { startEdgeNotifier } from "./edgeClientWatcher";
|
|
27
27
|
import { getGitRefLive, getGitURLLive } from "./git";
|
|
28
|
+
import { DeployProgress } from "./deployFunctions";
|
|
28
29
|
|
|
29
30
|
const UPDATE_POLL_INTERVAL = timeInMinute * 15;
|
|
30
31
|
const DEAD_NODE_COUNT_THRESHOLD = 15;
|
|
@@ -259,12 +260,17 @@ async function updateEdgeNodesFile() {
|
|
|
259
260
|
await edgeNodeStorage.set(edgeNodeIndexFile, Buffer.from(JSON.stringify(newEdgeNodeIndex)));
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
export async function preloadUI(hash: string) {
|
|
263
|
+
export async function preloadUI(hash: string, progress?: DeployProgress) {
|
|
264
|
+
progress?.({ section: "Finding EdgeNodeControllers", progress: 0 });
|
|
263
265
|
let nodeIds = await getControllerNodeIdList(EdgeNodeController);
|
|
266
|
+
progress?.({ section: "Finding EdgeNodeControllers", progress: 1 });
|
|
264
267
|
await Promise.allSettled(nodeIds.map(async nodeId => {
|
|
265
268
|
let controller = EdgeNodeController.nodes[nodeId.nodeId];
|
|
269
|
+
let section = `${nodeId.entryPoint}:${nodeId.nodeId}|Preloading UI`;
|
|
270
|
+
progress?.({ section, progress: 0 });
|
|
266
271
|
console.log(blue(`Preloading UI on ${String(nodeId)}, hash: ${hash}`));
|
|
267
272
|
await errorToUndefined(controller.preloadUI({ hash }));
|
|
273
|
+
progress?.({ section, progress: 1 });
|
|
268
274
|
console.log(blue(`Finished preloading UI on ${String(nodeId)}`));
|
|
269
275
|
}));
|
|
270
276
|
}
|
|
@@ -6,14 +6,14 @@ import "../inject";
|
|
|
6
6
|
import { shimDateNow } from "socket-function/time/trueTimeShim";
|
|
7
7
|
shimDateNow();
|
|
8
8
|
|
|
9
|
-
import { isNode, isNodeTrue, timeInMinute } from "socket-function/src/misc";
|
|
9
|
+
import { isNode, isNodeTrue, timeInMinute, timeInSecond, timeoutToUndefined } from "socket-function/src/misc";
|
|
10
10
|
|
|
11
11
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
12
12
|
import { isHotReloading, watchFilesAndTriggerHotReloading } from "socket-function/hot/HotReloadController";
|
|
13
13
|
import { RequireController, setRequireBootRequire } from "socket-function/require/RequireController";
|
|
14
14
|
import { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
15
15
|
import { getOwnMachineId, getThreadKeyCert, verifyMachineIdForPublicKey } from "../-a-auth/certs";
|
|
16
|
-
import { getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
|
|
16
|
+
import { getHostedIP, getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
|
|
17
17
|
import { LOCAL_DOMAIN, nodePathAuthority } from "../0-path-value-core/NodePathAuthorities";
|
|
18
18
|
import { debugCoreMode, registerGetCompressNetwork, encodeParentFilter, registerGetCompressDisk, authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
19
19
|
import { clientWatcher, ClientWatcher } from "../1-path-client/pathValueClientWatcher";
|
|
@@ -67,6 +67,8 @@ let yargObj = parseArgsFactory()
|
|
|
67
67
|
.option("verbosenetwork", { type: "boolean", desc: "Log all network activity" })
|
|
68
68
|
.option("verboseframework", { type: "boolean", desc: "Log internal SocketFunction framework" })
|
|
69
69
|
.option("nodelay", { type: "boolean", desc: "Don't delay committing functions, even ones that are marked to be delayed." })
|
|
70
|
+
// TODO: The bootstrapper is a single file. Maybe we shouldn't run the entire service just for that. Although... maybe it's fine, as services are light?
|
|
71
|
+
.option("bootstraponly", { type: "boolean", desc: "Don't register as an edge node, so we serve the bootstrap files, but we don't need up to date code because we are not used for endpoints or the UI." })
|
|
70
72
|
.argv
|
|
71
73
|
;
|
|
72
74
|
setImmediate(() => {
|
|
@@ -765,6 +767,13 @@ export class Querysub {
|
|
|
765
767
|
allowHostnames.push(domain);
|
|
766
768
|
}
|
|
767
769
|
allowHostnames.push("127-0-0-1." + getDomain());
|
|
770
|
+
|
|
771
|
+
if (yargObj.bootstraponly && isPublic()) {
|
|
772
|
+
if (config.port !== 443) {
|
|
773
|
+
throw new Error(`--bootstraponly requires you to set port 443. There can only be one bootstrap node per server.`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
768
777
|
await SocketFunction.mount({
|
|
769
778
|
public: isPublic(),
|
|
770
779
|
port: config.port,
|
|
@@ -780,10 +789,38 @@ export class Querysub {
|
|
|
780
789
|
|
|
781
790
|
let { ip, ipDomain } = await publishMachineARecords();
|
|
782
791
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
792
|
+
if (!yargObj.bootstraponly) {
|
|
793
|
+
await registerEdgeNode({
|
|
794
|
+
host: ipDomain + ":" + config.port,
|
|
795
|
+
entryPaths,
|
|
796
|
+
});
|
|
797
|
+
} else {
|
|
798
|
+
// bootstraponly mode. Setup cloudflare proxy. If they are developing (localhost), we can't proxy, so don't (but still setup domain). Using the cloudflare proxy should prevent the site from entirely breaking if the bootstrapper goes down.
|
|
799
|
+
let ip = await getHostedIP();
|
|
800
|
+
let existingRecords = await getRecords("A", getDomain());
|
|
801
|
+
if (ip !== "127.0.0.1") {
|
|
802
|
+
let validRecords: string[] = [];
|
|
803
|
+
await Promise.all(existingRecords.map(async (record) => {
|
|
804
|
+
let isListening = await timeoutToUndefined(timeInSecond * 10, testTCPIsListening(record, 443));
|
|
805
|
+
if (isListening) {
|
|
806
|
+
validRecords.push(record);
|
|
807
|
+
}
|
|
808
|
+
}));
|
|
809
|
+
// It's hard to manage multiple bootstrappers, so... just don't.
|
|
810
|
+
if (validRecords.length > 0) {
|
|
811
|
+
console.error(`Found existing bootstrapper at ${JSON.stringify(validRecords)}, so why are we even running? Terminating shortly`);
|
|
812
|
+
// Give logs time to write
|
|
813
|
+
await shutdown();
|
|
814
|
+
}
|
|
815
|
+
await setRecord("A", getDomain(), ip, "proxied");
|
|
816
|
+
} else {
|
|
817
|
+
if (existingRecords.length === 0) {
|
|
818
|
+
await setRecord("A", ip, getDomain());
|
|
819
|
+
} else {
|
|
820
|
+
console.log(`Not clobbering existing A record for ${getDomain()} of ${JSON.stringify(existingRecords)}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
787
824
|
}
|
|
788
825
|
private static async addSourceMapCheck(config: {
|
|
789
826
|
sourceCheck?: MachineSourceCheck;
|
|
@@ -1279,4 +1316,6 @@ import { getCountPerPaint } from "../functional/onNextPaint";
|
|
|
1279
1316
|
import { addEpsilons } from "../bits";
|
|
1280
1317
|
import { blue } from "socket-function/src/formatting/logColors";
|
|
1281
1318
|
import { MachineController } from "../deployManager/machineController";
|
|
1319
|
+
import { getRecords, setRecord } from "../-b-authorities/dnsAuthority";
|
|
1320
|
+
import { testTCPIsListening } from "socket-function/src/networking";
|
|
1282
1321
|
|
|
@@ -8,6 +8,7 @@ import { MachinesListPage } from "./components/MachinesListPage";
|
|
|
8
8
|
import { ServiceDetailPage } from "./components/ServiceDetailPage";
|
|
9
9
|
import { MachineDetailPage } from "./components/MachineDetailPage";
|
|
10
10
|
import { Anchor } from "../library-components/ATag";
|
|
11
|
+
import { DeployPage } from "./components/DeployPage";
|
|
11
12
|
|
|
12
13
|
export class MachinesPage extends qreact.Component {
|
|
13
14
|
private renderTabs() {
|
|
@@ -18,6 +19,7 @@ export class MachinesPage extends qreact.Component {
|
|
|
18
19
|
{[
|
|
19
20
|
{ key: "machines", label: "Machines", otherKeys: ["machine-detail"] },
|
|
20
21
|
{ key: "services", label: "Services", otherKeys: ["service-detail"] },
|
|
22
|
+
{ key: "deploy", label: "Deploy", otherKeys: [] },
|
|
21
23
|
].map(tab => {
|
|
22
24
|
let isActive = currentViewParam.value === tab.key || tab.otherKeys.includes(currentViewParam.value);
|
|
23
25
|
return <Anchor noStyles key={tab.key}
|
|
@@ -44,6 +46,7 @@ export class MachinesPage extends qreact.Component {
|
|
|
44
46
|
{currentViewParam.value === "machines" && <MachinesListPage />}
|
|
45
47
|
{currentViewParam.value === "service-detail" && <ServiceDetailPage />}
|
|
46
48
|
{currentViewParam.value === "machine-detail" && <MachineDetailPage />}
|
|
49
|
+
{currentViewParam.value === "deploy" && <DeployPage />}
|
|
47
50
|
</div>
|
|
48
51
|
</div>;
|
|
49
52
|
}
|