cob-cli 2.51.0 → 2.52.0-beta-2
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/bin/cob-cli.js +31 -0
- package/lib/commands/cleanup.js +116 -0
- package/lib/commands/deploy.js +33 -26
- package/lib/commands/package.js +20 -0
- package/lib/commands/tagDefs.js +67 -0
- package/lib/commands/test.js +29 -9
- package/lib/commands/updateFromServer.js +149 -13
- package/lib/task_lists/common_definitions.js +26 -0
- package/lib/task_lists/common_enviromentHandler.js +14 -8
- package/lib/task_lists/common_helpers.js +95 -17
- package/lib/task_lists/common_operationState.js +28 -0
- package/lib/task_lists/common_releaseManager.js +10 -8
- package/lib/task_lists/common_syncFiles.js +9 -4
- package/lib/task_lists/package_execute.js +45 -0
- package/lib/task_lists/tagDefs_execute.js +121 -0
- package/lib/task_lists/test_customUIsContinuosReload.js +36 -7
- package/lib/task_lists/test_otherFilesContiousReload.js +44 -22
- package/lib/task_lists/test_syncFile.js +5 -4
- package/lib/task_lists/test_validate.js +3 -2
- package/lib/task_lists/updateFromServer_definitions.js +135 -0
- package/lib/task_lists/updateFromServer_domains.js +50 -0
- package/lib/task_lists/updateFromServer_groups.js +111 -0
- package/lib/task_lists/updateFromServer_instances.js +76 -0
- package/lib/task_lists/updateFromServer_kibana.js +88 -0
- package/lib/task_lists/updateFromServer_validate.js +3 -3
- package/lib/webpack/webpack.config.js +1 -1
- package/package.json +5 -1
|
@@ -7,6 +7,7 @@ const { SERVER_COB_CLI_DIRECTORY } = require("./common_helpers");
|
|
|
7
7
|
const { confirmExecutionOfChanges, resolveCobPath } = require("./common_syncFiles");
|
|
8
8
|
const { syncFile } = require("./test_syncFile");
|
|
9
9
|
const { getLastDeployedSha } = require("./common_releaseManager");
|
|
10
|
+
const { getSshArgs } = require("./common_helpers");
|
|
10
11
|
|
|
11
12
|
const IN_PROGRESS_TEST_FILE = "test.in.execution"
|
|
12
13
|
const SERVER_IN_PROGRESS_TEST_FILE = SERVER_COB_CLI_DIRECTORY + IN_PROGRESS_TEST_FILE
|
|
@@ -18,7 +19,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
18
19
|
let changedFiles = new Set()
|
|
19
20
|
|
|
20
21
|
let currentTestsFilesList = []
|
|
21
|
-
await execa('ssh', [cmdEnv.server, "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
22
|
+
await execa('ssh', [...getSshArgs(cmdEnv.server), "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
22
23
|
.then( value => currentTestsFilesList = value.stdout.split("\n").map(l => l.substring(0,l.indexOf(" "))) )
|
|
23
24
|
.catch( () => { } ) /* grep returns error if no match and execa throws an excepcions when return is error */
|
|
24
25
|
|
|
@@ -29,22 +30,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
29
30
|
let restoreChanges = async () => {
|
|
30
31
|
if(watcher.process) watcher.process.close();
|
|
31
32
|
if(changedFiles.size > 0) {
|
|
32
|
-
|
|
33
|
-
let stash = true;
|
|
34
|
-
await git().env('LC_ALL', 'C').stash(["--include-untracked"]).then( value => value.indexOf("Saved") == 0 || (stash = false))
|
|
35
|
-
let lastSha = await getLastDeployedSha(cmdEnv.server)
|
|
36
|
-
await git().checkout(lastSha)
|
|
37
|
-
|
|
38
|
-
await cmdEnv.applyLastEnvironmentDeployedToServerChanges()
|
|
39
|
-
for(let changedFile of changedFiles) {
|
|
40
|
-
await removeFileFromCurrentTest(cmdEnv.server, changedFile);
|
|
41
|
-
}
|
|
42
|
-
await cmdEnv.unApplyLastEnvironmentDeployedToServerChanges()
|
|
43
|
-
|
|
44
|
-
await execa('ssh', [cmdEnv.server,"find " + SERVER_IN_PROGRESS_TEST_FILE + " -size 0 -delete "]) //Remove file if empty
|
|
45
|
-
|
|
46
|
-
await git().checkout("-")
|
|
47
|
-
if (stash) await git().stash(["pop"])
|
|
33
|
+
await restoreServerFilesToLastDeployed(cmdEnv, changedFiles);
|
|
48
34
|
}
|
|
49
35
|
fs.unlinkSync("."+IN_PROGRESS_TEST_FILE)
|
|
50
36
|
}
|
|
@@ -71,7 +57,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
71
57
|
let productDir = resolveCobPath(cmdEnv.server, "serverLive", product).split(":")[1];
|
|
72
58
|
let productScopeFilePath = changedFile.substring(changedFile.indexOf("/") + 1); // remove o directorio inicial do producto
|
|
73
59
|
|
|
74
|
-
let isRemoteFile = () => execa('ssh', [cmdEnv.server, "[[ -f " + productDir + productScopeFilePath + " ]] && echo 'file' || echo '' " ])
|
|
60
|
+
let isRemoteFile = () => execa('ssh', [...getSshArgs(cmdEnv.server), "[[ -f " + productDir + productScopeFilePath + " ]] && echo 'file' || echo '' " ])
|
|
75
61
|
.then( r => {
|
|
76
62
|
return r.stdout == "file"
|
|
77
63
|
}).catch( () => {
|
|
@@ -114,10 +100,45 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
114
100
|
}
|
|
115
101
|
exports.otherFilesContiousReload = otherFilesContiousReload
|
|
116
102
|
|
|
103
|
+
/* ************************************ */
|
|
104
|
+
async function restoreServerFilesToLastDeployed(cmdEnv, changedFiles) {
|
|
105
|
+
console.log("\nRestoring changed files...".yellow);
|
|
106
|
+
|
|
107
|
+
// Clean up any env-swap markers left by applyCurrentCommandEnvironmentChanges before
|
|
108
|
+
// stashing. These files are gitignored, so git stash --include-untracked ignores them.
|
|
109
|
+
// If they survive into _undoSpecific(lastEnv) they create ghost env files that are
|
|
110
|
+
// untracked at lastSha, causing "git checkout -" to abort.
|
|
111
|
+
await cmdEnv.unApplyCurrentCommandEnvironmentChanges();
|
|
112
|
+
|
|
113
|
+
let stashed = false;
|
|
114
|
+
await git().env('LC_ALL', 'C').stash(["--include-untracked"])
|
|
115
|
+
.then(value => { stashed = value.indexOf("Saved") === 0; })
|
|
116
|
+
.catch(() => {});
|
|
117
|
+
|
|
118
|
+
const lastSha = await getLastDeployedSha(cmdEnv.server);
|
|
119
|
+
if (!lastSha) throw new Error("Cannot restore server files: no previous deploy found on server.");
|
|
120
|
+
await git().checkout(lastSha);
|
|
121
|
+
|
|
122
|
+
await cmdEnv.applyLastEnvironmentDeployedToServerChanges();
|
|
123
|
+
|
|
124
|
+
for (const changedFile of changedFiles) {
|
|
125
|
+
await removeFileFromCurrentTest(cmdEnv.server, changedFile);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await cmdEnv.unApplyLastEnvironmentDeployedToServerChanges();
|
|
129
|
+
|
|
130
|
+
await execa('ssh', [...getSshArgs(cmdEnv.server), "find " + SERVER_IN_PROGRESS_TEST_FILE + " -size 0 -delete"])
|
|
131
|
+
.catch(() => {});
|
|
132
|
+
|
|
133
|
+
await git().checkout("-");
|
|
134
|
+
if (stashed) await git().stash(["pop"]);
|
|
135
|
+
}
|
|
136
|
+
exports.restoreServerFilesToLastDeployed = restoreServerFilesToLastDeployed
|
|
137
|
+
|
|
117
138
|
/* ************************************ */
|
|
118
139
|
async function addFileToCurrentTest(server, changedFile, changedFiles, changeType, restoreChanges) {
|
|
119
140
|
let inUse = "";
|
|
120
|
-
await execa('ssh', [server, "grep -F '" + changedFile + " ' " + SERVER_IN_PROGRESS_TEST_FILE ])
|
|
141
|
+
await execa('ssh', [...getSshArgs(server), "grep -F '" + changedFile + " ' " + SERVER_IN_PROGRESS_TEST_FILE ])
|
|
121
142
|
.then( value => inUse = value.stdout )
|
|
122
143
|
.catch( () => { } ) /* grep returns error if no match and execa throws an excepcions when return is error */
|
|
123
144
|
.finally( async () => {
|
|
@@ -133,7 +154,7 @@ async function addFileToCurrentTest(server, changedFile, changedFiles, changeTyp
|
|
|
133
154
|
|
|
134
155
|
if(!changedFiles.has(changedFile)) {
|
|
135
156
|
const user = require("os").userInfo().username;
|
|
136
|
-
execa('ssh', [server, "echo '" + changedFile + " by " + user + "' >> " + SERVER_IN_PROGRESS_TEST_FILE]);
|
|
157
|
+
execa('ssh', [...getSshArgs(server), "echo '" + changedFile + " by " + user + "' >> " + SERVER_IN_PROGRESS_TEST_FILE]);
|
|
137
158
|
changedFiles.add(changedFile);
|
|
138
159
|
}
|
|
139
160
|
console.log(" " + changeType.replace(/(e)([^e]*)$/, "ed$2 ") + changedFile);
|
|
@@ -143,13 +164,14 @@ async function addFileToCurrentTest(server, changedFile, changedFiles, changeTyp
|
|
|
143
164
|
/* ************************************ */
|
|
144
165
|
async function removeFileFromCurrentTest(server, changedFile) {
|
|
145
166
|
await syncFile(server, changedFile);
|
|
146
|
-
await execa('ssh', [server, "sed -i '/" + changedFile.split("/").join("\\/") + "/d' " + SERVER_IN_PROGRESS_TEST_FILE]).catch( () => {/* ignore files removed by user */} );
|
|
167
|
+
await execa('ssh', [...getSshArgs(server), "sed -i '/" + changedFile.split("/").join("\\/") + "/d' " + SERVER_IN_PROGRESS_TEST_FILE]).catch( () => {/* ignore files removed by user */} );
|
|
147
168
|
console.log(" reset ".brightGreen + changedFile);
|
|
148
169
|
}
|
|
170
|
+
exports.removeFileFromCurrentTest = removeFileFromCurrentTest
|
|
149
171
|
|
|
150
172
|
/* ************************************ */
|
|
151
173
|
function checkNoTestsRunningOnServer(server) {
|
|
152
|
-
return execa('ssh', [server, "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
174
|
+
return execa('ssh', [...getSshArgs(server), "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
153
175
|
.then(value => {
|
|
154
176
|
throw new Error(
|
|
155
177
|
"Aborted:".red + " there are tests running with files changed on the server:\n "
|
|
@@ -2,6 +2,7 @@ const execa = require('execa');
|
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { resolveCobPath } = require("./common_syncFiles");
|
|
5
|
+
const { getSshArgs, getRsyncSsh } = require("./common_helpers");
|
|
5
6
|
|
|
6
7
|
const DEBUG = false;
|
|
7
8
|
const RSYNC_RETRIES = 10;
|
|
@@ -18,7 +19,7 @@ async function syncFile(server, localFile) {
|
|
|
18
19
|
|
|
19
20
|
let count = 0;
|
|
20
21
|
while (count < RSYNC_RETRIES) {
|
|
21
|
-
await execa('ssh', [server, "mkdir -p " + remoteFileDir]).catch({});
|
|
22
|
+
await execa('ssh', [...getSshArgs(server), "mkdir -p " + remoteFileDir]).catch({});
|
|
22
23
|
await execa('rsync', [localFile, remoteProductPath + productScopeFilePath,
|
|
23
24
|
"-acz",
|
|
24
25
|
"--prune-empty-dirs",
|
|
@@ -28,17 +29,17 @@ async function syncFile(server, localFile) {
|
|
|
28
29
|
"--no-t",
|
|
29
30
|
"--filter='merge " + path.resolve(__dirname,"rsyncFilter-pre.txt") + "'",
|
|
30
31
|
"--filter='merge " + path.resolve(__dirname,"rsyncFilter-post.txt") + "'"],
|
|
31
|
-
{ shell: true, env: { "RSYNC_RSH":
|
|
32
|
+
{ shell: true, env: { "RSYNC_RSH": getRsyncSsh() } })
|
|
32
33
|
.then(count = RSYNC_RETRIES)
|
|
33
34
|
.catch(async (err) => {
|
|
34
35
|
// exitCode 23 indicam que o ficheiro não existe na origem. Temod de apagar no destino.
|
|
35
36
|
if (err.exitCode == 23) {
|
|
36
|
-
await execa('ssh', [server, "rm -fd " + remoteProductPath.split(":")[1] + productScopeFilePath]).catch({});
|
|
37
|
+
await execa('ssh', [...getSshArgs(server), "rm -fd " + remoteProductPath.split(":")[1] + productScopeFilePath]).catch({});
|
|
37
38
|
|
|
38
39
|
let localFileDir = localFile.substring(0, localFile.lastIndexOf("/"));
|
|
39
40
|
if (!fs.existsSync(localFileDir)) {
|
|
40
41
|
if (DEBUG) console.log(remoteFileDir + " needs to be removed");
|
|
41
|
-
await execa('ssh', [server, "rm -rf " + remoteFileDir]).catch({});
|
|
42
|
+
await execa('ssh', [...getSshArgs(server), "rm -rf " + remoteFileDir]).catch({});
|
|
42
43
|
}
|
|
43
44
|
count = RSYNC_RETRIES;
|
|
44
45
|
} else if ((count + 1)> RSYNC_RETRIES) {
|
|
@@ -9,6 +9,7 @@ const { checkConnectivity } = require("./common_helpers");
|
|
|
9
9
|
const { getCurrentBranch, getLastDeployedSha } = require("./common_releaseManager");
|
|
10
10
|
const { testEquality } = require("./common_syncFiles");
|
|
11
11
|
const { SERVER_IN_PROGRESS_TEST_FILE } = require("../task_lists/test_otherFilesContiousReload");
|
|
12
|
+
const { getSshArgs } = require("./common_helpers");
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
async function validateTestingConditions(cmdEnv, args) {
|
|
@@ -79,7 +80,7 @@ async function _handleTestEquality(ctx, changes, server) {
|
|
|
79
80
|
let offendingFiles = changes.map(f => f.substring(17)) //17 é a posição onde começa o nome dos ficheiros em cada linha
|
|
80
81
|
let problemFiles = [];
|
|
81
82
|
for(let changedFile of offendingFiles) {
|
|
82
|
-
await execa('ssh', [server, "grep -F " + changedFile + " " + SERVER_IN_PROGRESS_TEST_FILE ])
|
|
83
|
+
await execa('ssh', [...getSshArgs(server), "grep -F " + changedFile + " " + SERVER_IN_PROGRESS_TEST_FILE ])
|
|
83
84
|
.catch( () => {
|
|
84
85
|
try {
|
|
85
86
|
if (fs.lstatSync(changedFile.trim()).isFile()) {
|
|
@@ -94,7 +95,7 @@ async function _handleTestEquality(ctx, changes, server) {
|
|
|
94
95
|
if(problemFiles.length > 0) throw new Error("Aborted:".red + " server was changed since last deploy:\n " + problemFiles.join("\n ") + "\n" );
|
|
95
96
|
|
|
96
97
|
let inTest = "";
|
|
97
|
-
await execa('ssh', [server, "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
98
|
+
await execa('ssh', [...getSshArgs(server), "cat " + SERVER_IN_PROGRESS_TEST_FILE])
|
|
98
99
|
.then( value => inTest = value.stdout )
|
|
99
100
|
.catch( () => { } ) /* grep returns error if no match and execa throws an excepcions when return is error */
|
|
100
101
|
.finally( async () => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const Listr = require('listr');
|
|
3
|
+
const UpdaterRenderer = require('listr-update-renderer');
|
|
4
|
+
const VerboseRenderer = require('listr-verbose-renderer');
|
|
5
|
+
const { getFilteredDefinitions } = require('./common_definitions.js')
|
|
6
|
+
const axios = require("axios");
|
|
7
|
+
const fs = require("fs/promises");
|
|
8
|
+
|
|
9
|
+
async function getRefs(servername, def) {
|
|
10
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/definitions/${def.id}`
|
|
11
|
+
const response = await axios.get(uri);
|
|
12
|
+
|
|
13
|
+
if(response.status != 200){
|
|
14
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fields = response.data.fieldDefinitions;
|
|
18
|
+
return [...new Set(get$Refs(fields))];
|
|
19
|
+
}
|
|
20
|
+
function get$Refs(fields){
|
|
21
|
+
let refs = [];
|
|
22
|
+
for(f of fields){
|
|
23
|
+
if(f.configuration.keys.Reference){
|
|
24
|
+
refs.push(f.configuration.keys.Reference.args.definition);
|
|
25
|
+
}
|
|
26
|
+
if(f.fields) {
|
|
27
|
+
const childRefs = get$Refs(f.fields);
|
|
28
|
+
if(childRefs.length > 0) refs.push(...childRefs)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return refs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function exportDefinitionChanges(servername, def, dataPath) {
|
|
35
|
+
|
|
36
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/definitions/${def.id}/changes`
|
|
37
|
+
const response = await axios.get(uri);
|
|
38
|
+
|
|
39
|
+
if(response.status == 204){
|
|
40
|
+
// this is probably an error
|
|
41
|
+
throw new Error(`Definition "${def.name}" [${def.id}] has no tagged Changes.`);
|
|
42
|
+
|
|
43
|
+
} else if(response.status != 200){
|
|
44
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// write every change on a single line
|
|
48
|
+
const stringified = "[\n" + response.data.map(change => " " + JSON.stringify(change)).join(',\n') + "\n]\n";
|
|
49
|
+
|
|
50
|
+
await fs.writeFile(dataPath, stringified, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function exportDefinitionIdx(defs, dataPath) {
|
|
54
|
+
await fs.writeFile(dataPath, defs.map(d => d.name).join('\n'), 'utf-8')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getViewsForDef(servername, definition, dataPath) {
|
|
58
|
+
const definitionId = definition.id
|
|
59
|
+
const uri = `https://${servername}.cultofbits.com/recordm/user/settings/${definition.views}/definitions-${definitionId}/views?`
|
|
60
|
+
|
|
61
|
+
const response = await axios.get(uri);
|
|
62
|
+
|
|
63
|
+
if(response.status != 200){
|
|
64
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let userViews = response.data.filter(v => v.isShared)
|
|
68
|
+
|
|
69
|
+
if(response.data.length == 0)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
userViews = response.data.map(v => {
|
|
73
|
+
v.value = JSON.stringify( JSON.parse(v.value).filter(f => f.visible == true) )
|
|
74
|
+
return v
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const jsonLines = userViews.map(v => JSON.stringify({
|
|
78
|
+
key: v.key,
|
|
79
|
+
value: v.value,
|
|
80
|
+
user: v.user,
|
|
81
|
+
})) .join('\n');
|
|
82
|
+
|
|
83
|
+
await fs.writeFile(dataPath, jsonLines)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getDefinitionsFromServer(servername, defs, definitionsPath, viewsPath, customization, args) {
|
|
87
|
+
|
|
88
|
+
return new Listr(
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
title: `(${customization}) Definitions: `.bold + "searching for definitions matching queries",
|
|
92
|
+
task: async ctx => ctx.defs = await getFilteredDefinitions(servername, defs)
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: `(${customization}) Definitions: `.bold + "enriching metadata",
|
|
96
|
+
task: async ctx => {
|
|
97
|
+
const definitions = await Promise.all(ctx.defs
|
|
98
|
+
.map(async d => ({id: d.id, name: d.name, views: d.views, refs: await getRefs(servername, d)}))
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// we'll sort by refs, Defs that reference others must come after them
|
|
102
|
+
definitions.sort((a, b) => {
|
|
103
|
+
if (a.refs.includes(b.name)){
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if(b.refs.includes(a.name)){
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return a.refs.length - b.refs.length;
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
ctx.defs = definitions
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
title: `(${customization}) Definitions: `.bold + "exporting changes",
|
|
119
|
+
task: async ctx => {
|
|
120
|
+
for( d of ctx.defs ){ await exportDefinitionChanges(servername, d, definitionsPath + `${d.name}.json`)}
|
|
121
|
+
await exportDefinitionIdx(ctx.defs, definitionsPath + "_index")
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: `(${customization}) Definitions: `.bold + "exporting views",
|
|
126
|
+
task: async ctx => { for (d of ctx.defs.filter(d => d.views )){ await getViewsForDef(servername, d, viewsPath + `${d.name}.jsonl`)} }
|
|
127
|
+
},
|
|
128
|
+
],{
|
|
129
|
+
renderer: args.verbose ? VerboseRenderer : UpdaterRenderer,
|
|
130
|
+
collapse: false
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
exports.getDefinitionsFromServer = getDefinitionsFromServer
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const Listr = require('listr');
|
|
3
|
+
const UpdaterRenderer = require('listr-update-renderer');
|
|
4
|
+
const VerboseRenderer = require('listr-verbose-renderer');
|
|
5
|
+
const axios = require("axios");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
|
|
8
|
+
async function domainsFromServer(servername, domainsFilter, exportPath, customization, args) {
|
|
9
|
+
await new Listr(
|
|
10
|
+
[
|
|
11
|
+
{ title: `(${customization}) Domains: `.bold + "searching for domains matching the queries" ,
|
|
12
|
+
task: async ctx => ctx.domains = await getDomains(servername, domainsFilter, exportPath) },
|
|
13
|
+
{ title: `(${customization}) Domains: `.bold + "exporting domains " ,
|
|
14
|
+
task: ctx => ctx.domains.forEach( d => exportDomain(d, exportPath)) }
|
|
15
|
+
],{
|
|
16
|
+
renderer: args.verbose ? VerboseRenderer : UpdaterRenderer,
|
|
17
|
+
collapse: false
|
|
18
|
+
})
|
|
19
|
+
.run()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function match(query, domain) {
|
|
23
|
+
// simple matching for now
|
|
24
|
+
return (domain.name && domain.name.includes(query)) ||
|
|
25
|
+
(domain.description && domain.description.includes(query) )
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getDomains(servername, filters, exportPath) {
|
|
29
|
+
|
|
30
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/domains`
|
|
31
|
+
const response = await axios.get( uri );
|
|
32
|
+
|
|
33
|
+
if(response.status != 200){
|
|
34
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const matchesAny = (domain) => filters.filter(q => match(q, domain)).length > 0;
|
|
38
|
+
|
|
39
|
+
return response.data.filter( matchesAny )
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function exportDomain(domain, rootPath) {
|
|
43
|
+
const d = {
|
|
44
|
+
name: domain.name,
|
|
45
|
+
definitions: domain.definitions.map(d => d.name).sort()
|
|
46
|
+
}
|
|
47
|
+
await fs.writeFile(rootPath + domain.name + ".json", JSON.stringify(d, null, 2) )
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.getDomainsFromServer = domainsFromServer
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const Listr = require('listr');
|
|
3
|
+
const UpdaterRenderer = require('listr-update-renderer');
|
|
4
|
+
const VerboseRenderer = require('listr-verbose-renderer');
|
|
5
|
+
const axios = require("axios");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
|
|
8
|
+
async function getGroups(query, servername) {
|
|
9
|
+
|
|
10
|
+
const uri = `https://${servername}.cultofbits.com/userm/userm/search/userm/group?`
|
|
11
|
+
const response = await axios.get( uri + new URLSearchParams({
|
|
12
|
+
q: query,
|
|
13
|
+
size: "100" // probably excessive
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
if(response.status != 200){
|
|
17
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const groupIds = response.data.hits.hits.map((h) => h._id)
|
|
20
|
+
return groupIds;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const sortByName = (a, b) => a.name?.localeCompare(b.name);
|
|
24
|
+
|
|
25
|
+
const exportGroups = async function(ids, servername, exportPath){
|
|
26
|
+
|
|
27
|
+
const uri = `https://${servername}.cultofbits.com/userm/userm/group/export/${ids.join(',')}`
|
|
28
|
+
|
|
29
|
+
const response = await axios.get( uri );
|
|
30
|
+
if(response.status != 200 ){
|
|
31
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
const data = response.data;
|
|
34
|
+
|
|
35
|
+
// sort everything to be easier to diff
|
|
36
|
+
data.groups?.sort(sortByName);
|
|
37
|
+
data.groups?.forEach(group => group.roles?.sort())
|
|
38
|
+
data.roles?.sort(sortByName);
|
|
39
|
+
data.roles?.forEach(role => role.perms?.sort())
|
|
40
|
+
data.perms?.sort(sortByName)
|
|
41
|
+
const re = /:(\d+)/g
|
|
42
|
+
defnameCache.servername = servername;
|
|
43
|
+
|
|
44
|
+
for(r of data.roles) {
|
|
45
|
+
for(let i = 0; i < r.perms.length; i++){
|
|
46
|
+
r.perms[i] = await replaceAsync(r.perms[i], re, replaceIdsWithName)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for(p of data.perms){
|
|
50
|
+
if( p.product == 'recordm') {
|
|
51
|
+
p.name = await replaceAsync(p.name, re, replaceIdsWithName)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fs.writeFile(exportPath, JSON.stringify(data, null, 2))
|
|
56
|
+
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async function replaceIdsWithName(_m, g1){
|
|
60
|
+
return ":" + ( "§§" + await defnameCache.get(g1) + "§§")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getGroupsFromServer(servername, groupsQueries, exportPath, customization, args) {
|
|
64
|
+
const searchTasks = groupsQueries.map( gp =>
|
|
65
|
+
({ title: `(${customization}) Permissions: `.bold + "searching for groups with query: " + gp,
|
|
66
|
+
task: async ctx => ctx.groupIds = ctx.groupIds
|
|
67
|
+
? ctx.groupIds.concat(await getGroups(gp, servername))
|
|
68
|
+
: await getGroups(gp, servername) }))
|
|
69
|
+
|
|
70
|
+
await new Listr(
|
|
71
|
+
[
|
|
72
|
+
...searchTasks,
|
|
73
|
+
{
|
|
74
|
+
title: `(${customization}) Permissions: `.bold + "exporting groups and subsequent roles/permissions",
|
|
75
|
+
task: ctx => exportGroups(ctx.groupIds, servername, exportPath)
|
|
76
|
+
},
|
|
77
|
+
],{
|
|
78
|
+
renderer: args.verbose ? VerboseRenderer : UpdaterRenderer,
|
|
79
|
+
collapse: false
|
|
80
|
+
})
|
|
81
|
+
.run()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defnameCache = {
|
|
85
|
+
servername: '',
|
|
86
|
+
resolved: new Map(),
|
|
87
|
+
get: async function(id) {
|
|
88
|
+
if(!this.resolved.has(id)){
|
|
89
|
+
const uri = `https://${this.servername}.cultofbits.com/recordm/recordm/definitions/${id}`
|
|
90
|
+
const response = await axios.get( uri );
|
|
91
|
+
|
|
92
|
+
if(response.status != 200){
|
|
93
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.resolved.set(id, response.data.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.resolved.get(id)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function replaceAsync(string, regexp, replacerFunction) {
|
|
104
|
+
const replacements = await Promise.all(
|
|
105
|
+
Array.from(string.matchAll(regexp),
|
|
106
|
+
match => replacerFunction(...match)));
|
|
107
|
+
let i = 0;
|
|
108
|
+
return string.replace(regexp, () => replacements[i++]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
exports.getGroupsFromServer = getGroupsFromServer
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const Listr = require('listr');
|
|
3
|
+
const UpdaterRenderer = require('listr-update-renderer');
|
|
4
|
+
const VerboseRenderer = require('listr-verbose-renderer');
|
|
5
|
+
const axios = require("axios");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async function getInstancesFromServer(servername, definitionsForInstances, rootPath, customization, args) {
|
|
10
|
+
|
|
11
|
+
const taskList = definitionsForInstances.map( def => ({
|
|
12
|
+
title: `(${customization}) Instances: `.bold + `Exporting instances from definition ${def.definition}`,
|
|
13
|
+
task: async () => getInstancesOfDefinition(servername, await getDefinition(servername, def.definition), def.query, def.crop, rootPath)
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
taskList.push({
|
|
17
|
+
title: `(${customization}) Instances: `.bold + 'Exporting _index',
|
|
18
|
+
task: async () => await exportIndex(definitionsForInstances, rootPath + "/_index")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
await new Listr(taskList , {
|
|
22
|
+
renderer: args.verbose ? VerboseRenderer : UpdaterRenderer,
|
|
23
|
+
collapse: false
|
|
24
|
+
}).run()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function flattenFields(fieldArray) {
|
|
28
|
+
if (!fieldArray || fieldArray.length == 0)
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
return fieldArray.flatMap(field => flattenFields(field.fields).concat([field]))
|
|
32
|
+
}
|
|
33
|
+
function getFieldList(fields) {
|
|
34
|
+
return Array.from(new Set(fields.map(f => f.name))).join(',')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async function getInstancesOfDefinition(servername, definition, query, crop, rootPath) {
|
|
39
|
+
|
|
40
|
+
const definitionId = definition.id
|
|
41
|
+
const fields = flattenFields(definition.fieldDefinitions)
|
|
42
|
+
|
|
43
|
+
if(!fields.some(f => (f.description || '').includes('$instanceLabel'))){
|
|
44
|
+
throw new Error("Should not export instances of a Definition without an $instanceLabel field, it would generate duplicates on import")
|
|
45
|
+
}
|
|
46
|
+
if(crop && !fields.some(f => (f.description || '').includes('$importer.last'))){
|
|
47
|
+
throw new Error("Can not crop instances of a Definition without an $importer.last field")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/definitions/search/download/${definitionId}?`
|
|
51
|
+
const response = await axios.get(uri + new URLSearchParams({
|
|
52
|
+
q: query,
|
|
53
|
+
vcn: "id," + getFieldList(fields)
|
|
54
|
+
}), {
|
|
55
|
+
responseType: 'arraybuffer',
|
|
56
|
+
headers: {'export-files': 'true'}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
fs.writeFile(rootPath + `${definition.name}.xlsx`, response.data)
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getDefinition(servername, definitionName) {
|
|
64
|
+
|
|
65
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/definitions/name/${encodeURIComponent(definitionName)}`
|
|
66
|
+
const response = await axios.get(uri);
|
|
67
|
+
|
|
68
|
+
return response.data
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function exportIndex(defs, dataPath) {
|
|
72
|
+
await fs.writeFile(dataPath, defs.map(d => d.definition + ":" + d.crop).join('\n'), 'utf-8')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
exports.getInstancesFromServer = getInstancesFromServer
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const Listr = require('listr');
|
|
3
|
+
const UpdaterRenderer = require('listr-update-renderer');
|
|
4
|
+
const VerboseRenderer = require('listr-verbose-renderer');
|
|
5
|
+
const axios = require("axios");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
|
|
8
|
+
const KIBANA_TYPES = ['config','url','index-pattern','query','tag','canvas-element','canvas-workpad','action','alert','visualization','dashboard','map','lens','cases','search'];
|
|
9
|
+
|
|
10
|
+
async function exportSavedObjectsOfSpace(servername, space, filePath, args) {
|
|
11
|
+
const uri = `https://${servername}.cultofbits.com/kibana/s/${space.identifier}/api/saved_objects/_export`
|
|
12
|
+
try {
|
|
13
|
+
const response = await axios.post(uri, {
|
|
14
|
+
type: KIBANA_TYPES,
|
|
15
|
+
excludeExportDetails: true,
|
|
16
|
+
}, {
|
|
17
|
+
headers: { 'kbn-xsrf': 'true' },
|
|
18
|
+
responseType: 'text',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const allObjects = response.data
|
|
22
|
+
.split('\n')
|
|
23
|
+
.filter(line => line.trim())
|
|
24
|
+
.map(line => JSON.parse(line));
|
|
25
|
+
|
|
26
|
+
await Promise.all(allObjects
|
|
27
|
+
.filter(e => e.type === 'index-pattern')
|
|
28
|
+
.map(async element => await replaceIndexPatternsIds(servername, element))
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
await fs.writeFile(filePath, allObjects.map(o => JSON.stringify(o)).join('\n'))
|
|
32
|
+
} catch(err) {
|
|
33
|
+
if(args.verbose > 0) {
|
|
34
|
+
console.error("error downloading from kibana {status:", err.response?.status, ", uri:", uri)
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function exportIndex(spaces, dataPath) {
|
|
41
|
+
await fs.writeFile(dataPath, spaces.map(s => s.identifier + ":" + s.name).join('\n'), 'utf-8')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// replaces in place
|
|
46
|
+
async function replaceIndexPatternsIds(servername, indexPattern) {
|
|
47
|
+
if(indexPattern.type !== "index-pattern"){
|
|
48
|
+
throw new Error("Not an index pattern")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const definitionId = indexPattern.attributes.title.substring('recordm-'.length)
|
|
52
|
+
const name = await getDefinitionName(servername, definitionId)
|
|
53
|
+
|
|
54
|
+
indexPattern.attributes.title = `recordm-§§${name}§§`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getDefinitionName(servername, id) {
|
|
58
|
+
const uri = `https://${servername}.cultofbits.com/recordm/recordm/definitions/${id}`
|
|
59
|
+
const response = await axios.get( uri );
|
|
60
|
+
|
|
61
|
+
if(response.status != 200){
|
|
62
|
+
throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return response.data.name
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getKibanaFromServer(servername, spaces, roothpath, customization, args) {
|
|
69
|
+
|
|
70
|
+
const taskList = spaces.map( space => (
|
|
71
|
+
{
|
|
72
|
+
title: `(${customization}) Kibana: `.bold + `Exporting ${space.name}`,
|
|
73
|
+
task: async () => await exportSavedObjectsOfSpace(servername, space, roothpath + `/${space.name}.ndjson`, args)
|
|
74
|
+
}
|
|
75
|
+
))
|
|
76
|
+
|
|
77
|
+
taskList.push({
|
|
78
|
+
title: `(${customization}) Kibana: `.bold + 'Exporting _index',
|
|
79
|
+
task: async () => await exportIndex(spaces, roothpath + "/_index")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await new Listr( taskList, {
|
|
83
|
+
renderer: args.verbose ? VerboseRenderer : UpdaterRenderer,
|
|
84
|
+
collapse: false
|
|
85
|
+
}).run()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.getKibanaFromServer = getKibanaFromServer
|
|
@@ -8,7 +8,7 @@ function validateUpdateFromServerConditions(cmdEnv) {
|
|
|
8
8
|
return new Listr([
|
|
9
9
|
{ title: "Check connectivity and permissions".bold, task: () => checkConnectivity(cmdEnv.server) },
|
|
10
10
|
{ title: "Check there's no cob-cli test' running".bold, task: () => checkNoTestsRunningOnServer(cmdEnv.server) },
|
|
11
|
-
{ title: "Check git status".bold, task: () => checkWorkingCopyCleanliness() },
|
|
12
|
-
|
|
11
|
+
{ title: "Check git status".bold, task: () => checkWorkingCopyCleanliness() },
|
|
12
|
+
])
|
|
13
13
|
}
|
|
14
|
-
exports.validateUpdateFromServerConditions = validateUpdateFromServerConditions
|
|
14
|
+
exports.validateUpdateFromServerConditions = validateUpdateFromServerConditions
|