@kumologica/sdk 3.0.0-alpha4
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/README.md +52 -0
- package/bin/kl.js +2 -0
- package/cli/KumologicaError.js +17 -0
- package/cli/cli.js +7 -0
- package/cli/commands/build-commands/aws.js +49 -0
- package/cli/commands/build-commands/azure.js +43 -0
- package/cli/commands/build-commands/kumohub.js +49 -0
- package/cli/commands/build.js +6 -0
- package/cli/commands/create-commands/create-project-iteratively.js +49 -0
- package/cli/commands/create-commands/index.js +5 -0
- package/cli/commands/create.js +66 -0
- package/cli/commands/deploy-commands/kumohub.js +114 -0
- package/cli/commands/deploy.js +6 -0
- package/cli/commands/doc-commands/html.js +60 -0
- package/cli/commands/doc.js +6 -0
- package/cli/commands/export-commands/cloudformation.js +371 -0
- package/cli/commands/export-commands/serverless.js +164 -0
- package/cli/commands/export-commands/terraform-commands/aws.js +193 -0
- package/cli/commands/export-commands/terraform-commands/azure.js +148 -0
- package/cli/commands/export-commands/terraform.js +6 -0
- package/cli/commands/export-commands/utils/validator.js +195 -0
- package/cli/commands/export.js +6 -0
- package/cli/commands/list-templates.js +24 -0
- package/cli/commands/open.js +53 -0
- package/cli/commands/start.js +165 -0
- package/cli/commands/test/TestSuiteRunner.js +76 -0
- package/cli/commands/test.js +123 -0
- package/cli/utils/download-template-from-repo.js +346 -0
- package/cli/utils/download-test.js +12 -0
- package/cli/utils/download.js +119 -0
- package/cli/utils/fs/copy-dir-contents-sync.js +15 -0
- package/cli/utils/fs/create-zip-file.js +39 -0
- package/cli/utils/fs/dir-exists-sync.js +14 -0
- package/cli/utils/fs/dir-exists.js +17 -0
- package/cli/utils/fs/file-exists-sync.js +14 -0
- package/cli/utils/fs/file-exists.js +12 -0
- package/cli/utils/fs/get-tmp-dir-path.js +22 -0
- package/cli/utils/fs/parse.js +40 -0
- package/cli/utils/fs/read-file-sync.js +11 -0
- package/cli/utils/fs/read-file.js +10 -0
- package/cli/utils/fs/safe-move-file.js +58 -0
- package/cli/utils/fs/walk-dir-sync.js +34 -0
- package/cli/utils/fs/write-file-sync.js +31 -0
- package/cli/utils/fs/write-file.js +32 -0
- package/cli/utils/logger.js +26 -0
- package/cli/utils/rename-service.js +49 -0
- package/package.json +72 -0
- package/src/api/core/comms.js +141 -0
- package/src/api/core/context.js +296 -0
- package/src/api/core/flows.js +286 -0
- package/src/api/core/index.js +29 -0
- package/src/api/core/library.js +106 -0
- package/src/api/core/nodes.js +476 -0
- package/src/api/core/projects.js +426 -0
- package/src/api/core/rest/context.js +42 -0
- package/src/api/core/rest/flow.js +53 -0
- package/src/api/core/rest/flows.js +53 -0
- package/src/api/core/rest/index.js +171 -0
- package/src/api/core/rest/nodes.js +164 -0
- package/src/api/core/rest/util.js +53 -0
- package/src/api/core/settings.js +287 -0
- package/src/api/tools/base/DesignerTool.js +108 -0
- package/src/api/tools/core/flow.js +58 -0
- package/src/api/tools/core/index.js +18 -0
- package/src/api/tools/core/node.js +77 -0
- package/src/api/tools/debugger/index.js +193 -0
- package/src/api/tools/filemanager/index.js +127 -0
- package/src/api/tools/git/index.js +103 -0
- package/src/api/tools/index.js +13 -0
- package/src/api/tools/test/index.js +56 -0
- package/src/api/tools/test/lib/TestCaseRunner.js +105 -0
- package/src/api/tools/test/lib/fixtures/example3-flow.json +148 -0
- package/src/api/tools/test/lib/fixtures/package.json +6 -0
- package/src/api/tools/test/lib/fixtures/s3-event.js +43 -0
- package/src/api/tools/test/lib/reporters/index.js +120 -0
- package/src/server/DesignerServer.js +141 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This command should be used to start the kumologica runtime in local mode. And it will
|
|
3
|
+
* be assisting the UI on all aspects of the project development.
|
|
4
|
+
*
|
|
5
|
+
* Example:
|
|
6
|
+
* kl start ./myproject
|
|
7
|
+
* kl start ./myproject/flow.json
|
|
8
|
+
* kl start (this will look for a flow.json in the current directory)
|
|
9
|
+
*
|
|
10
|
+
* If not flow is found, the process will be exited (with code 1) for the time being.
|
|
11
|
+
* Ideally, we want to start the server and serve the Runtime API to allow the user to
|
|
12
|
+
* open/create new projects fromt he UI.
|
|
13
|
+
*
|
|
14
|
+
* The Runtime API can be found in: packages/runtime/src/runtime/lib/api/rest/index.js
|
|
15
|
+
* The actual runtime is in: packages/runtime/src/runtime/lib/index.js
|
|
16
|
+
*
|
|
17
|
+
* Naming convention used during the code:
|
|
18
|
+
* - AdminApp (express app) is the server that serves the Runtime API
|
|
19
|
+
* - NodeApp (express app) is the server that runs the flow
|
|
20
|
+
*
|
|
21
|
+
* WARNING:
|
|
22
|
+
* Current implementation seems to be mounting the runtime api in the NodeApp (see runtime/lib/index.js:534)
|
|
23
|
+
* Ideally we wouldl like to separate the two apps and have the AdminApp running on a different port.
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const { codegen } = require('@kumologica/builder');
|
|
30
|
+
const { DesignerServer } = require('../../src/server/DesignerServer');
|
|
31
|
+
const { logError, logNotice, logInfo, logFatal } = require('../utils/logger');
|
|
32
|
+
// const opn = require('better-opn');
|
|
33
|
+
|
|
34
|
+
exports.command = 'start [project_directory]';
|
|
35
|
+
exports.desc = `Run kumologica runtime in local mode`;
|
|
36
|
+
|
|
37
|
+
exports.builder = (yargs) => {
|
|
38
|
+
yargs.positional(`project_directory`, {
|
|
39
|
+
type: 'string',
|
|
40
|
+
describe:
|
|
41
|
+
'Path to a valid kumologica project directory or flow file. (Optional)',
|
|
42
|
+
});
|
|
43
|
+
yargs.option(`loglevel`, {
|
|
44
|
+
describe: 'Logging level: [error, warn, info, debug, trace]',
|
|
45
|
+
type: 'string',
|
|
46
|
+
nargs: 1,
|
|
47
|
+
});
|
|
48
|
+
yargs.option(`port`, {
|
|
49
|
+
describe: 'Specifies the listening port utilized by the application',
|
|
50
|
+
type: 'number',
|
|
51
|
+
nargs: 1,
|
|
52
|
+
});
|
|
53
|
+
yargs.option(`adminport`, {
|
|
54
|
+
describe: 'Specifies the listening port utilized by the admin API',
|
|
55
|
+
type: 'number',
|
|
56
|
+
nargs: 1,
|
|
57
|
+
});
|
|
58
|
+
yargs.option(`secured`, {
|
|
59
|
+
describe: 'Enable security control to the admin API',
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
nargs: 1,
|
|
62
|
+
});
|
|
63
|
+
yargs.option(`noadmin`, {
|
|
64
|
+
describe: 'Start the runtime with admin API disabled',
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
nargs: 0,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
exports.desc = 'Starting Kumologica Runtime for Development';
|
|
71
|
+
|
|
72
|
+
exports.handler = ({
|
|
73
|
+
project_directory,
|
|
74
|
+
loglevel,
|
|
75
|
+
port,
|
|
76
|
+
adminport,
|
|
77
|
+
secured,
|
|
78
|
+
noadmin,
|
|
79
|
+
env
|
|
80
|
+
}) => {
|
|
81
|
+
logNotice(`Launching Kumologica Runtime...`);
|
|
82
|
+
let absProjectDirectory;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
absProjectDirectory = fs.realpathSync(project_directory);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logFatal(`Project directory not found: ${project_directory}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Resolve the path to the flow path
|
|
92
|
+
const [projectFlowDirname, projectFlowFullPath] = resolveProjectFlowPath(absProjectDirectory);
|
|
93
|
+
logInfo(`> Flow file found: ${projectFlowFullPath}`);
|
|
94
|
+
|
|
95
|
+
// Gather all cli params
|
|
96
|
+
const cliParams = {
|
|
97
|
+
env,
|
|
98
|
+
loglevel,
|
|
99
|
+
port,
|
|
100
|
+
adminport,
|
|
101
|
+
secured,
|
|
102
|
+
noadmin
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Start a server
|
|
106
|
+
let server = new DesignerServer(projectFlowFullPath, true, cliParams);
|
|
107
|
+
server.listen();
|
|
108
|
+
if (!noadmin) {
|
|
109
|
+
server.listenAdminServer();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// opn("http://localhost:1880/hello");
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.log(e);
|
|
115
|
+
logFatal(e.message);
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a given path to a new path that points to the project flow json file.
|
|
122
|
+
* Scenarios:
|
|
123
|
+
* - If path undefined will resolve using current directory
|
|
124
|
+
* - If path directory, it should look up for a valid flow.json file, if it does not exist create it
|
|
125
|
+
* - If path is pointing to an actual flowFile, just return it.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} projectDir
|
|
128
|
+
*/
|
|
129
|
+
function resolveProjectFlowPath(projectDir) {
|
|
130
|
+
// output
|
|
131
|
+
let projectFlowFullPath;
|
|
132
|
+
let projectFlowDirname;
|
|
133
|
+
|
|
134
|
+
let projectDirOrFile = projectDir || process.cwd();
|
|
135
|
+
|
|
136
|
+
let isDir = isDirectory(projectDirOrFile);
|
|
137
|
+
if (isDir) {
|
|
138
|
+
let flowFileName = codegen.findFlowFile(projectDirOrFile); // returns only the flowname
|
|
139
|
+
|
|
140
|
+
if (!flowFileName) {
|
|
141
|
+
logError(`No flow found in directory: ${projectDirOrFile}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
} else {
|
|
144
|
+
projectFlowDirname = projectDirOrFile;
|
|
145
|
+
projectFlowFullPath = path.join(projectDirOrFile, flowFileName);
|
|
146
|
+
}
|
|
147
|
+
} else if (isDir === false) {
|
|
148
|
+
projectFlowDirname = path.dirname(projectDirOrFile);
|
|
149
|
+
projectFlowFullPath = projectDirOrFile;
|
|
150
|
+
} else {
|
|
151
|
+
logError(`Directory does not exist: ${project_directory}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [projectFlowDirname, projectFlowFullPath];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isDirectory(dir) {
|
|
159
|
+
try {
|
|
160
|
+
let stats = fs.statSync(dir);
|
|
161
|
+
return stats.isDirectory();
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { performance } = require('perf_hooks');
|
|
2
|
+
const { InMemoryReporter, TerminalReporter } = require("../../../src/api/tools/test/lib/reporters");
|
|
3
|
+
const { TestCaseRunner } = require('../../../src/api/tools/test/lib/TestCaseRunner');
|
|
4
|
+
const { logError, logNotice, logInfo, logFatal } = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestSuiteRunner {
|
|
8
|
+
constructor(designerServer) {
|
|
9
|
+
this.designerServer = designerServer;
|
|
10
|
+
this.flowServer = this.designerServer.getFlowServer();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {*} testcases
|
|
15
|
+
* return an array containing found errors during execution, or undefined if successful
|
|
16
|
+
*/
|
|
17
|
+
async runAll(testcases) {
|
|
18
|
+
logInfo(`TestCases - Executing\n`);
|
|
19
|
+
let testSuiteTimeStart = performance.now();
|
|
20
|
+
|
|
21
|
+
let totalPassedTestCases = 0;
|
|
22
|
+
let totalFailedCases = 0;
|
|
23
|
+
const totalTestCases = testcases.length;
|
|
24
|
+
for (let i = 0; i <= totalTestCases - 1; i++) {
|
|
25
|
+
let success = await this.runTestCase(testcases[i]);
|
|
26
|
+
if (success) {
|
|
27
|
+
totalPassedTestCases = totalPassedTestCases + 1;
|
|
28
|
+
} else {
|
|
29
|
+
totalFailedCases = totalFailedCases + 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let testSuiteTimeEnd = performance.now();
|
|
33
|
+
let testSuiteExecutionTimeInMs = testSuiteTimeEnd - testSuiteTimeStart;
|
|
34
|
+
this.printSummary(testSuiteExecutionTimeInMs, totalTestCases, totalFailedCases, totalPassedTestCases);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @param {*} testcase
|
|
41
|
+
* @returns true if success, otherwise false
|
|
42
|
+
*/
|
|
43
|
+
async runTestCase(testcase) {
|
|
44
|
+
let testcaseid = testcase.id;
|
|
45
|
+
|
|
46
|
+
const inMemReporter = new InMemoryReporter;
|
|
47
|
+
const terminalReporter = new TerminalReporter;
|
|
48
|
+
|
|
49
|
+
const testcaseRunner = new TestCaseRunner(this.flowServer, testcaseid, [inMemReporter, terminalReporter]);
|
|
50
|
+
try {
|
|
51
|
+
await testcaseRunner.runAsync();
|
|
52
|
+
return !inMemReporter.isFailedStatus()
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.log(err);
|
|
55
|
+
logFatal(`Unexpected error occurred while running testcase: "${testcase.name}" due to: ${err.message}.`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
printSummary(totalExecutionTimeInMs, totalTestCases, totalFailedCases, totalPassedTestCases) {
|
|
60
|
+
logInfo(`TestCases - Completed
|
|
61
|
+
|
|
62
|
+
Final Execution Statistics
|
|
63
|
+
-------------------------------
|
|
64
|
+
Execution Time: ${Math.floor(totalExecutionTimeInMs)} ms
|
|
65
|
+
Total Test Cases: ${totalTestCases}
|
|
66
|
+
Failed: ${totalFailedCases}
|
|
67
|
+
Passed: ${totalPassedTestCases}
|
|
68
|
+
-------------------------------
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
TestSuiteRunner
|
|
76
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { Select } = require('enquirer');
|
|
3
|
+
const wcmatch = require('wildcard-match');
|
|
4
|
+
const { util } = require('@kumologica/runtime');
|
|
5
|
+
const { codegen } = require('@kumologica/builder');
|
|
6
|
+
|
|
7
|
+
const { TestSuiteRunner } = require('./test/TestSuiteRunner');
|
|
8
|
+
const { DesignerServer } = require('../../src/server/DesignerServer');
|
|
9
|
+
const { logError, logNotice, logInfo, logFatal } = require('../utils/logger');
|
|
10
|
+
|
|
11
|
+
const isDirectory = util.isDirectorySync;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
exports.command = 'test [project_directory]';
|
|
15
|
+
exports.desc = `Run test suite`;
|
|
16
|
+
exports.builder = (yargs) => {
|
|
17
|
+
yargs.positional(`project_directory`, {
|
|
18
|
+
type: 'string',
|
|
19
|
+
describe: 'Path to a valid kumologica project directory or flow file. (Optional)'
|
|
20
|
+
})
|
|
21
|
+
yargs.option(`testcase`, {
|
|
22
|
+
describe: "Testcase name to run",
|
|
23
|
+
type: 'string',
|
|
24
|
+
alias: 't',
|
|
25
|
+
nargs: 1
|
|
26
|
+
});
|
|
27
|
+
yargs.option(`iterative`, {
|
|
28
|
+
describe: "Manually select the testcase to run from all available testcases",
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
alias: 'i'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
exports.handler = ({ project_directory, testcase, iterative }) => {
|
|
34
|
+
let projectDirOrFile = project_directory || process.cwd();
|
|
35
|
+
let projectFlowPath = projectDirOrFile;
|
|
36
|
+
|
|
37
|
+
let isDir = isDirectory(projectDirOrFile);
|
|
38
|
+
if (isDir) {
|
|
39
|
+
let flowFileName = codegen.findFlowFile(projectDirOrFile); // returns only the flowname
|
|
40
|
+
if (!flowFileName) {
|
|
41
|
+
logFatal(`No flow found in directory: ${projectDirOrFile}`);
|
|
42
|
+
} else {
|
|
43
|
+
projectFlowPath = path.join(projectDirOrFile, flowFileName);
|
|
44
|
+
}
|
|
45
|
+
} else if (isDir === false) {
|
|
46
|
+
// do nothing as it was assumed to be a file
|
|
47
|
+
} else {
|
|
48
|
+
logFatal(`Directory does not exist: ${project_directory}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
runTestOnNewServer(projectFlowPath, testcase, iterative);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
logFatal(e.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runTestOnNewServer(flowFilePath, testcaseSelected, iterative) {
|
|
59
|
+
let designerServer = new DesignerServer(
|
|
60
|
+
flowFilePath,
|
|
61
|
+
false,
|
|
62
|
+
{
|
|
63
|
+
loglevel: "error",
|
|
64
|
+
noadmin: true
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await designerServer.listen();
|
|
69
|
+
logInfo(`> Flow file: ${path.resolve(flowFilePath)} \n`);
|
|
70
|
+
let testSuiteRunner = new TestSuiteRunner(designerServer);
|
|
71
|
+
|
|
72
|
+
// If testcase is null, default to universal wildcard
|
|
73
|
+
testcaseSelected = testcaseSelected || "**";
|
|
74
|
+
|
|
75
|
+
// Find out all testcases available on the flow
|
|
76
|
+
let testcasesAvailable = codegen.findTestCasesFromFlow(flowFilePath);
|
|
77
|
+
if (!testcasesAvailable || (testcasesAvailable && testcasesAvailable.length === 0)) {
|
|
78
|
+
logFatal(`No testcases found on flow file: ${flowFileAbsPath}`);
|
|
79
|
+
};
|
|
80
|
+
let testcaseAvailableNames = testcasesAvailable.map(tc => tc.name);
|
|
81
|
+
|
|
82
|
+
// Capture the testcase from user on iterative mode
|
|
83
|
+
if (iterative) {
|
|
84
|
+
const prompt = new Select({
|
|
85
|
+
name: 'testcase',
|
|
86
|
+
message: 'What testcase do you want to run?',
|
|
87
|
+
choices: [...testcaseAvailableNames, 'Run all TestCases...']
|
|
88
|
+
});
|
|
89
|
+
await prompt.run()
|
|
90
|
+
.then(tc => {
|
|
91
|
+
if (tc === 'Run all TestCases...') {
|
|
92
|
+
testcaseSelected = "**";
|
|
93
|
+
} else {
|
|
94
|
+
testcaseSelected = tc;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch(err => {
|
|
98
|
+
logFatal(`Error found while running tests on iterative mode due to: `, err.message);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Filter all testcases to be part of the test suite
|
|
103
|
+
const isMatch = wcmatch(testcaseSelected);
|
|
104
|
+
let testCasesSelected = [];
|
|
105
|
+
testcasesAvailable.forEach(async tc => {
|
|
106
|
+
if (isMatch(tc.name)) {
|
|
107
|
+
testCasesSelected.push({ name: tc.name, id: tc.id });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Execute the testcasesIds if any, otherwise throw an error
|
|
112
|
+
if (testCasesSelected.length === 0) {
|
|
113
|
+
logFatal(`No matched testcases found`);
|
|
114
|
+
} else {
|
|
115
|
+
const errors = await testSuiteRunner.runAll(testCasesSelected);
|
|
116
|
+
process.exit(errors > 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.log(err);
|
|
121
|
+
logFatal(`Unexpected error occurred while starting server due to <${err.message}>`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const URL = require('url');
|
|
5
|
+
const BbPromise = require('bluebird');
|
|
6
|
+
const fse = require('fs-extra');
|
|
7
|
+
const qs = require('querystring');
|
|
8
|
+
const spawn = require('child-process-ext/spawn');
|
|
9
|
+
const renameService = require('./rename-service').renameService;
|
|
10
|
+
const KumologicaError = require('../KumologicaError');
|
|
11
|
+
const copyDirContentsSync = require('./fs/copy-dir-contents-sync');
|
|
12
|
+
const dirExistsSync = require('./fs/dir-exists-sync');
|
|
13
|
+
const walkDirSync = require('./fs/walk-dir-sync');
|
|
14
|
+
const { getTmpDirPath, getBaseTmpDirPath } = require('./fs/get-tmp-dir-path');
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const OFFICAL_TEMPLATES_REPO = 'https://github.com/KumologicaHQ/kumologica-templates.git';
|
|
18
|
+
/**
|
|
19
|
+
* Returns directory path
|
|
20
|
+
* @param {Number} length
|
|
21
|
+
* @param {Array} parts
|
|
22
|
+
* @returns {String} directory path
|
|
23
|
+
*/
|
|
24
|
+
function getPathDirectory(length, parts) {
|
|
25
|
+
if (!parts) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return parts.slice(length).filter(Boolean).join(path.sep);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates URL
|
|
33
|
+
* @param {Object} url
|
|
34
|
+
* @param {String} hostname
|
|
35
|
+
* @param {String} service
|
|
36
|
+
* @param {String} owner
|
|
37
|
+
* @param {String} repo
|
|
38
|
+
*/
|
|
39
|
+
function validateUrl({ url, hostname, service, owner, repo }) {
|
|
40
|
+
// validate if given url is a valid url
|
|
41
|
+
if (url.hostname !== hostname || !owner || !repo) {
|
|
42
|
+
const errorMessage = `The URL must be a valid ${service} URL in the following format: https://${hostname}/serverless/serverless`;
|
|
43
|
+
throw new KumologicaError(errorMessage, 'INVALID_TEMPLATE_URL');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if the URL is pointing to a Git repository
|
|
49
|
+
* @param {String} url
|
|
50
|
+
*/
|
|
51
|
+
function isPlainGitURL(url) {
|
|
52
|
+
return (url.startsWith('https') || url.startsWith('git@')) && url.endsWith('.git');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} url
|
|
57
|
+
* @returns {Object}
|
|
58
|
+
*/
|
|
59
|
+
function parseGitHubURL(url) {
|
|
60
|
+
const pathLength = 4;
|
|
61
|
+
const parts = url.pathname.split('/');
|
|
62
|
+
const isSubdirectory = parts.length > pathLength;
|
|
63
|
+
const owner = parts[1];
|
|
64
|
+
const repo = parts[2];
|
|
65
|
+
const branch = isSubdirectory ? parts[pathLength] : 'master';
|
|
66
|
+
const isGitHubEnterprise = url.hostname !== 'github.com';
|
|
67
|
+
|
|
68
|
+
if (!isGitHubEnterprise) {
|
|
69
|
+
// validate if given url is a valid GitHub url
|
|
70
|
+
validateUrl({ url, hostname: 'github.com', service: 'GitHub', owner, repo });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const downloadUrl = `https://${
|
|
74
|
+
isGitHubEnterprise ? url.hostname : 'github.com'
|
|
75
|
+
}/${owner}/${repo}/archive/${branch}.zip`;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
owner,
|
|
79
|
+
repo,
|
|
80
|
+
branch,
|
|
81
|
+
downloadUrl,
|
|
82
|
+
isSubdirectory,
|
|
83
|
+
pathToDirectory: getPathDirectory(pathLength + 1, parts),
|
|
84
|
+
username: url.username || '',
|
|
85
|
+
password: url.password || '',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object} url
|
|
91
|
+
* @returns {Object}
|
|
92
|
+
*/
|
|
93
|
+
function parseBitbucketURL(url) {
|
|
94
|
+
const pathLength = 4;
|
|
95
|
+
const parts = url.pathname.split('/');
|
|
96
|
+
const isSubdirectory = parts.length > pathLength;
|
|
97
|
+
const owner = parts[1];
|
|
98
|
+
const repo = parts[2];
|
|
99
|
+
|
|
100
|
+
const query = qs.parse(url.query);
|
|
101
|
+
const branch = 'at' in query ? query.at : 'master';
|
|
102
|
+
|
|
103
|
+
// validate if given url is a valid Bitbucket url
|
|
104
|
+
validateUrl({ url, hostname: 'bitbucket.org', service: 'Bitbucket', owner, repo });
|
|
105
|
+
|
|
106
|
+
const downloadUrl = `https://bitbucket.org/${owner}/${repo}/get/${branch}.zip`;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
owner,
|
|
110
|
+
repo,
|
|
111
|
+
branch,
|
|
112
|
+
downloadUrl,
|
|
113
|
+
isSubdirectory,
|
|
114
|
+
pathToDirectory: getPathDirectory(pathLength + 1, parts),
|
|
115
|
+
username: url.username || '',
|
|
116
|
+
password: url.password || '',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseBitbucketServerURL(url) {
|
|
121
|
+
const pathLength = 9;
|
|
122
|
+
const parts = url.pathname.split('/');
|
|
123
|
+
const isSubdirectory = parts.length > pathLength;
|
|
124
|
+
const owner = parts[5];
|
|
125
|
+
const repo = parts[7];
|
|
126
|
+
|
|
127
|
+
const query = qs.parse(url.query);
|
|
128
|
+
const branch = 'at' in query ? decodeURIComponent(query.at) : 'master';
|
|
129
|
+
|
|
130
|
+
const downloadUrl = `${url.protocol}//${url.hostname}/rest/api/latest/projects/${owner}/repos/${repo}/archive${url.search}&format=zip`;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
branch,
|
|
136
|
+
downloadUrl,
|
|
137
|
+
isSubdirectory,
|
|
138
|
+
pathToDirectory: getPathDirectory(pathLength + 1, parts),
|
|
139
|
+
username: url.username || '',
|
|
140
|
+
password: url.password || '',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Call `/rest/api/1.0/application-properties` to retrieve server info
|
|
146
|
+
* @param {Object} url
|
|
147
|
+
* @returns {Boolean}
|
|
148
|
+
*/
|
|
149
|
+
async function retrieveBitbucketServerInfo(url) {
|
|
150
|
+
let requestOpts = {
|
|
151
|
+
url: `${url.protocol}//${url.hostname}/rest/api/1.0/application-properties`,
|
|
152
|
+
method: 'GET',
|
|
153
|
+
throwHttpErrors: false,
|
|
154
|
+
retry: 0
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
let resp = await got(requestOpts)
|
|
158
|
+
return resp.body.displayName === 'Bitbucket';
|
|
159
|
+
}catch(err){
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {Object} url
|
|
167
|
+
* @returns {Object}
|
|
168
|
+
*/
|
|
169
|
+
function parseGitlabURL(url) {
|
|
170
|
+
const pathLength = 4;
|
|
171
|
+
const parts = url.pathname.split('/');
|
|
172
|
+
const isSubdirectory = parts.length > pathLength;
|
|
173
|
+
const owner = parts[1];
|
|
174
|
+
const repo = parts[2];
|
|
175
|
+
|
|
176
|
+
const branch = isSubdirectory ? parts[pathLength] : 'master';
|
|
177
|
+
|
|
178
|
+
// validate if given url is a valid GitLab url
|
|
179
|
+
validateUrl({ url, hostname: 'gitlab.com', service: 'Bitbucket', owner, repo });
|
|
180
|
+
|
|
181
|
+
const downloadUrl = `https://gitlab.com/${owner}/${repo}/-/archive/${branch}/${repo}-${branch}.zip`;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
owner,
|
|
185
|
+
repo,
|
|
186
|
+
branch,
|
|
187
|
+
downloadUrl,
|
|
188
|
+
isSubdirectory,
|
|
189
|
+
pathToDirectory: getPathDirectory(pathLength + 1, parts),
|
|
190
|
+
username: url.username || '',
|
|
191
|
+
password: url.password || '',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parses a URL which points to a plain Git repository
|
|
197
|
+
* such as https://example.com/jdoe/project.git
|
|
198
|
+
*
|
|
199
|
+
* @param {String} url
|
|
200
|
+
* @returns {Object}
|
|
201
|
+
*/
|
|
202
|
+
function parsePlainGitURL(url) {
|
|
203
|
+
const branch = 'master';
|
|
204
|
+
const downloadUrl = url;
|
|
205
|
+
const isSubdirectory = false;
|
|
206
|
+
const repo = url.match(/.+\/(.+)\.git/)[1];
|
|
207
|
+
return {
|
|
208
|
+
repo,
|
|
209
|
+
branch,
|
|
210
|
+
downloadUrl,
|
|
211
|
+
isSubdirectory,
|
|
212
|
+
username: url.username || '',
|
|
213
|
+
password: url.password || '',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse URL and call the appropriate adaptor
|
|
219
|
+
*
|
|
220
|
+
* @param {string} inputUrl
|
|
221
|
+
* @throws {KumologicaError}
|
|
222
|
+
* @returns {Promise}
|
|
223
|
+
*/
|
|
224
|
+
async function parseRepoURL(inputUrl) {
|
|
225
|
+
return new BbPromise((resolve, reject) => {
|
|
226
|
+
if (!inputUrl) {
|
|
227
|
+
return reject(new KumologicaError('URL is required', 'MISSING_TEMPLATE_URL'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const url = URL.parse(inputUrl.replace(/\/$/, ''));
|
|
231
|
+
if (url.auth) {
|
|
232
|
+
const [username, password] = url.auth.split(':');
|
|
233
|
+
url.username = username;
|
|
234
|
+
url.password = password;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// check if url parameter is a valid url
|
|
238
|
+
if (!url.host && !url.href.startsWith('git@')) {
|
|
239
|
+
return reject(new KumologicaError('The URL you passed is not valid', 'INVALID_TEMPLATE_URL'));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (isPlainGitURL(url.href)) {
|
|
243
|
+
return resolve(parsePlainGitURL(inputUrl));
|
|
244
|
+
} else if (url.hostname === 'github.com' || url.hostname.indexOf('github.') !== -1) {
|
|
245
|
+
return resolve(parseGitHubURL(url));
|
|
246
|
+
} else if (url.hostname === 'bitbucket.org') {
|
|
247
|
+
return resolve(parseBitbucketURL(url));
|
|
248
|
+
} else if (url.hostname === 'gitlab.com') {
|
|
249
|
+
return resolve(parseGitlabURL(url));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const msg =
|
|
253
|
+
'The URL you passed is not one of the valid providers: "GitHub", "GitHub Entreprise", "Bitbucket", "Bitbucket Server" or "GitLab".';
|
|
254
|
+
const err = new KumologicaError(msg, 'INVALID_TEMPLATE_PROVIDER');
|
|
255
|
+
// test if it's a private bitbucket server
|
|
256
|
+
return retrieveBitbucketServerInfo(url)
|
|
257
|
+
.then((isBitbucket) => {
|
|
258
|
+
if (!isBitbucket) {
|
|
259
|
+
return reject(err);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// build download URL
|
|
263
|
+
return resolve(parseBitbucketServerURL(url));
|
|
264
|
+
})
|
|
265
|
+
.catch(() => reject(err));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Downloads the content of the template into the projectName directory
|
|
271
|
+
* @param {string} [repoUrl] - the git repository
|
|
272
|
+
* @param {string} [templateName] - the name of the project (optional), in case we are only interested in a particular directory within the repo
|
|
273
|
+
* @param {string} projectName - the local project name / directory to be created
|
|
274
|
+
* @returns {Promise}
|
|
275
|
+
*/
|
|
276
|
+
async function downloadTemplateFromRepo(repoUrl, templateName, projectPath, projectName) {
|
|
277
|
+
try{
|
|
278
|
+
repoUrl = repoUrl || OFFICAL_TEMPLATES_REPO;
|
|
279
|
+
const repoInformation = await parseRepoURL(repoUrl);
|
|
280
|
+
|
|
281
|
+
const tempBaseDirectory = getBaseTmpDirPath();
|
|
282
|
+
// clean up temp directory
|
|
283
|
+
fse.removeSync(tempBaseDirectory);
|
|
284
|
+
const tempRepoDirectory = getTmpDirPath();
|
|
285
|
+
// const { username, password } = repoInformation;
|
|
286
|
+
|
|
287
|
+
const renamed = templateName !== projectName;
|
|
288
|
+
|
|
289
|
+
// Source project directory
|
|
290
|
+
const srcProjectDir = templateName? path.join(tempRepoDirectory, templateName) : tempRepoDirectory;
|
|
291
|
+
|
|
292
|
+
// Target project directory
|
|
293
|
+
const targetProjectDir = path.join(projectPath, projectName);
|
|
294
|
+
|
|
295
|
+
if (dirExistsSync(targetProjectDir)) {
|
|
296
|
+
const errorMessage = `A project already exist in path: "${targetProjectDir}".`;
|
|
297
|
+
throw new KumologicaError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isPlainGitURL(repoUrl)) {
|
|
301
|
+
return spawn('git', ['clone', repoUrl, tempRepoDirectory]).then(() => {
|
|
302
|
+
try {
|
|
303
|
+
copyDirContentsSync(srcProjectDir, targetProjectDir);
|
|
304
|
+
fse.removeSync(tempBaseDirectory);
|
|
305
|
+
if (renamed) renameService(targetProjectDir, projectName);
|
|
306
|
+
}catch(err){
|
|
307
|
+
throw new KumologicaError(`Template name: "${templateName}" does not exist. Run "kl list-templates" to see list of available templates.`);
|
|
308
|
+
}
|
|
309
|
+
return "";
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
throw new KumologicaError(`Git repo URL is incorrect`);
|
|
313
|
+
}
|
|
314
|
+
}catch(err){
|
|
315
|
+
throw new KumologicaError(err.message);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function listAvailableTemplates() {
|
|
320
|
+
try{
|
|
321
|
+
const tempBaseDirectory = getBaseTmpDirPath();
|
|
322
|
+
const tempRepoDirectory = getTmpDirPath();
|
|
323
|
+
|
|
324
|
+
await parseRepoURL(OFFICAL_TEMPLATES_REPO);
|
|
325
|
+
|
|
326
|
+
const listTemplates = spawn('git', ['clone', OFFICAL_TEMPLATES_REPO, tempRepoDirectory]).then(() => {
|
|
327
|
+
try {
|
|
328
|
+
const projects = walkDirSync(tempRepoDirectory);
|
|
329
|
+
fse.removeSync(tempBaseDirectory);
|
|
330
|
+
return projects;
|
|
331
|
+
}catch(err){
|
|
332
|
+
throw new KumologicaError(err.message);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
// ListTemplates: ["t1", "t2"]
|
|
336
|
+
return listTemplates;
|
|
337
|
+
} catch(err){
|
|
338
|
+
throw new KumologicaError(err.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
downloadTemplateFromRepo,
|
|
344
|
+
listAvailableTemplates,
|
|
345
|
+
isPlainGitURL
|
|
346
|
+
};
|