kourou 1.2.0 → 1.4.0-dev.1
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 +5 -2
- package/lib/commands/app/scaffold.d.ts +21 -1
- package/lib/commands/app/scaffold.js +78 -3
- package/lib/common.js +8 -1
- package/lib/support/execute.js +3 -0
- package/lib/support/migrate/providers/bulk.d.ts +10 -0
- package/lib/support/migrate/providers/bulk.js +38 -0
- package/lib/support/migrate/providers/elasticsearch7.js +14 -6
- package/lib/support/migrate/providers/elasticsearch8.js +14 -6
- package/lib/support/migrate/providers/index.d.ts +1 -0
- package/lib/support/migrate/providers/index.js +1 -0
- package/package.json +5 -9
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ $ npm install -g kourou
|
|
|
27
27
|
$ kourou COMMAND
|
|
28
28
|
running command...
|
|
29
29
|
$ kourou (-v|--version|version)
|
|
30
|
-
kourou/1.
|
|
30
|
+
kourou/1.4.0-dev.1 linux-x64 node-v24.18.0
|
|
31
31
|
$ kourou --help [COMMAND]
|
|
32
32
|
USAGE
|
|
33
33
|
$ kourou COMMAND
|
|
@@ -364,8 +364,11 @@ ARGUMENTS
|
|
|
364
364
|
DESTINATION Directory to scaffold the app
|
|
365
365
|
|
|
366
366
|
OPTIONS
|
|
367
|
-
--flavor=flavor [default: generic] Template flavor ("generic", "iot").
|
|
367
|
+
--flavor=flavor [default: generic] Template flavor ("generic", "iot", "hypervision").
|
|
368
368
|
--help show CLI help
|
|
369
|
+
|
|
370
|
+
--token=token GitHub token used to clone private template repositories.
|
|
371
|
+
Defaults to the GITHUB_TOKEN environment variable, then to the GitHub CLI credentials.
|
|
369
372
|
```
|
|
370
373
|
|
|
371
374
|
_See code: [lib/commands/app/scaffold.js](lib/commands/app/scaffold.js)_
|
|
@@ -7,6 +7,7 @@ export default class AppScaffold extends Kommand {
|
|
|
7
7
|
static flags: {
|
|
8
8
|
help: import("@oclif/parser/lib/flags").IBooleanFlag<void>;
|
|
9
9
|
flavor: flags.IOptionFlag<string>;
|
|
10
|
+
token: flags.IOptionFlag<string | undefined>;
|
|
10
11
|
};
|
|
11
12
|
static args: {
|
|
12
13
|
name: string;
|
|
@@ -14,9 +15,28 @@ export default class AppScaffold extends Kommand {
|
|
|
14
15
|
required: boolean;
|
|
15
16
|
}[];
|
|
16
17
|
runSafe(): Promise<void>;
|
|
17
|
-
getRepo(flavor: string): "template-kuzzle-project" | "template-kiotp-project";
|
|
18
|
+
getRepo(flavor: string): "template-kuzzle-project" | "template-kiotp-project" | "template-hypervision-project";
|
|
18
19
|
checkDestination(destination: string): Promise<void>;
|
|
19
20
|
prepareTemplate(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Returns a GitHub token allowing to clone private template repositories,
|
|
23
|
+
* or undefined if none can be found.
|
|
24
|
+
*
|
|
25
|
+
* Looked up in the --token flag, then in the usual environment variables,
|
|
26
|
+
* then in the GitHub CLI credentials.
|
|
27
|
+
*/
|
|
28
|
+
getToken(): Promise<string | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the URLs to try to clone the template repository from, in order.
|
|
31
|
+
*
|
|
32
|
+
* Some templates are hosted in private repositories, so we need credentials:
|
|
33
|
+
* either a token, or the SSH key of the user.
|
|
34
|
+
*/
|
|
35
|
+
getCloneUrls(repo: string): Promise<string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Removes any token from a string before displaying it.
|
|
38
|
+
*/
|
|
39
|
+
hideToken(message: string): string;
|
|
20
40
|
cloneTemplate(flavor: string): Promise<void>;
|
|
21
41
|
copyTemplate(destination: string): Promise<void>;
|
|
22
42
|
cleanup(destination: string): Promise<void>;
|
|
@@ -47,6 +47,8 @@ class AppScaffold extends common_1.Kommand {
|
|
|
47
47
|
return "template-kuzzle-project";
|
|
48
48
|
case "iot":
|
|
49
49
|
return "template-kiotp-project";
|
|
50
|
+
case "hypervision":
|
|
51
|
+
return "template-hypervision-project";
|
|
50
52
|
default:
|
|
51
53
|
return "template-kuzzle-project";
|
|
52
54
|
}
|
|
@@ -69,12 +71,81 @@ class AppScaffold extends common_1.Kommand {
|
|
|
69
71
|
async prepareTemplate() {
|
|
70
72
|
await (0, execute_1.execute)("rm", "-rf", this.templatesDir);
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns a GitHub token allowing to clone private template repositories,
|
|
76
|
+
* or undefined if none can be found.
|
|
77
|
+
*
|
|
78
|
+
* Looked up in the --token flag, then in the usual environment variables,
|
|
79
|
+
* then in the GitHub CLI credentials.
|
|
80
|
+
*/
|
|
81
|
+
async getToken() {
|
|
82
|
+
const token = this.flags.token || process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
83
|
+
if (token) {
|
|
84
|
+
return token;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await (0, execute_1.execute)("gh", "auth", "token");
|
|
88
|
+
return stdout.trim() || undefined;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// GitHub CLI is not installed or not authenticated
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns the URLs to try to clone the template repository from, in order.
|
|
97
|
+
*
|
|
98
|
+
* Some templates are hosted in private repositories, so we need credentials:
|
|
99
|
+
* either a token, or the SSH key of the user.
|
|
100
|
+
*/
|
|
101
|
+
async getCloneUrls(repo) {
|
|
102
|
+
const urls = [];
|
|
103
|
+
const token = await this.getToken();
|
|
104
|
+
if (token) {
|
|
105
|
+
urls.push(`https://x-access-token:${token}@github.com/kuzzleio/${repo}`);
|
|
106
|
+
}
|
|
107
|
+
urls.push(`git@github.com:kuzzleio/${repo}`);
|
|
108
|
+
urls.push(`https://github.com/kuzzleio/${repo}`);
|
|
109
|
+
return urls;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Removes any token from a string before displaying it.
|
|
113
|
+
*/
|
|
114
|
+
hideToken(message) {
|
|
115
|
+
return message.replace(/x-access-token:[^@]*@/g, "x-access-token:***@");
|
|
116
|
+
}
|
|
72
117
|
async cloneTemplate(flavor) {
|
|
73
118
|
const repo = this.getRepo(flavor);
|
|
74
|
-
|
|
119
|
+
const urls = await this.getCloneUrls(repo);
|
|
120
|
+
let lastError;
|
|
121
|
+
for (const url of urls) {
|
|
122
|
+
try {
|
|
123
|
+
await (0, execute_1.execute)("git", "clone", "--depth=1", url, "--branch", "stable", "--single-branch", this.templatesDir, {
|
|
124
|
+
env: Object.assign(Object.assign({}, process.env), {
|
|
125
|
+
// Never prompt for credentials, we want to fail fast and try
|
|
126
|
+
// the next URL instead of hanging on a password prompt
|
|
127
|
+
GIT_TERMINAL_PROMPT: "0", GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ||
|
|
128
|
+
"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new" }),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Clone failed (e.g. the repository is private and those credentials
|
|
134
|
+
// are not allowed to read it), let's try the next URL
|
|
135
|
+
lastError = error;
|
|
136
|
+
await (0, execute_1.execute)("rm", "-rf", this.templatesDir);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Unable to clone the template repository "kuzzleio/${repo}". ` +
|
|
140
|
+
"If this repository is private, provide credentials with the --token flag, " +
|
|
141
|
+
"the GITHUB_TOKEN environment variable, the GitHub CLI (gh auth login) " +
|
|
142
|
+
`or a SSH key allowed to read it.\n${this.hideToken((lastError === null || lastError === void 0 ? void 0 : lastError.message) || "")}`);
|
|
75
143
|
}
|
|
76
144
|
async copyTemplate(destination) {
|
|
77
|
-
|
|
145
|
+
// -R (and not -r) so symlinks contained in the template are copied as
|
|
146
|
+
// symlinks instead of being dereferenced (BSD cp -r follows them and fails
|
|
147
|
+
// on dangling symlinks)
|
|
148
|
+
await (0, execute_1.execute)("cp", "-R", `${this.templatesDir}/`, `${destination}/`);
|
|
78
149
|
}
|
|
79
150
|
async cleanup(destination) {
|
|
80
151
|
await (0, execute_1.execute)("rm", "-rf", this.templatesDir);
|
|
@@ -88,7 +159,11 @@ AppScaffold.flags = {
|
|
|
88
159
|
help: command_1.flags.help(),
|
|
89
160
|
flavor: command_1.flags.string({
|
|
90
161
|
default: "generic",
|
|
91
|
-
description: `Template flavor ("generic", "iot").`,
|
|
162
|
+
description: `Template flavor ("generic", "iot", "hypervision").`,
|
|
163
|
+
}),
|
|
164
|
+
token: command_1.flags.string({
|
|
165
|
+
description: `GitHub token used to clone private template repositories.
|
|
166
|
+
Defaults to the GITHUB_TOKEN environment variable, then to the GitHub CLI credentials.`,
|
|
92
167
|
}),
|
|
93
168
|
};
|
|
94
169
|
AppScaffold.args = [
|
package/lib/common.js
CHANGED
|
@@ -70,6 +70,7 @@ class Kommand extends command_1.Command {
|
|
|
70
70
|
this.log(chalk_1.default.red(`[X] ${message}`));
|
|
71
71
|
}
|
|
72
72
|
async run() {
|
|
73
|
+
var _a, _b, _c, _d, _e;
|
|
73
74
|
const kommand = this.constructor;
|
|
74
75
|
const result = this.parse(kommand);
|
|
75
76
|
this.args = result.args;
|
|
@@ -102,7 +103,13 @@ class Kommand extends command_1.Command {
|
|
|
102
103
|
const errorLink = typeof error.id === "string" && error.id.split(".").length === 3
|
|
103
104
|
? ` (https://docs.kuzzle.io/core/2/api/errors/error-codes/${error.id.split(".")[0]})`
|
|
104
105
|
: "";
|
|
105
|
-
|
|
106
|
+
// Elasticsearch client errors keep their details in "meta", they would
|
|
107
|
+
// be lost otherwise
|
|
108
|
+
const esBody = (_b = (_a = error.meta) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : error.body;
|
|
109
|
+
const esDetails = esBody
|
|
110
|
+
? `\n\nElasticsearch response: ${JSON.stringify(esBody)}`
|
|
111
|
+
: "";
|
|
112
|
+
this.logKo(`Error stack: \n${stack || error.message}${esDetails}\n\nError status: ${(_d = (_c = error.status) !== null && _c !== void 0 ? _c : error.statusCode) !== null && _d !== void 0 ? _d : (_e = error.meta) === null || _e === void 0 ? void 0 : _e.statusCode}\n\nError id: ${error.id}${errorLink}`);
|
|
106
113
|
if (Array.isArray(error.errors)) {
|
|
107
114
|
for (const e of error.errors) {
|
|
108
115
|
this.logKo(`${e.document._id} : ${e.reason}`);
|
package/lib/support/execute.js
CHANGED
|
@@ -33,6 +33,9 @@ function execute(...args) {
|
|
|
33
33
|
process.stdout.on("data", (data) => (stdout += data.toString()));
|
|
34
34
|
process.stderr.on("data", (data) => (stderr += data.toString()));
|
|
35
35
|
const executor = new Promise((resolve, reject) => {
|
|
36
|
+
// Command cannot be spawned (e.g. binary not installed).
|
|
37
|
+
// Without this listener, Node throws the error instead of rejecting.
|
|
38
|
+
process.on("error", (error) => reject(error));
|
|
36
39
|
process.on("close", (code) => {
|
|
37
40
|
if (code === 0) {
|
|
38
41
|
resolve({ stdout, stderr, exitCode: code });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits documents into chunks so a bulk request never exceeds the
|
|
3
|
+
* Elasticsearch "http.max_content_length" limit (100mb by default).
|
|
4
|
+
*/
|
|
5
|
+
export declare function chunkDocuments(docs: any[], chunkSize?: number): Generator<any[]>;
|
|
6
|
+
/**
|
|
7
|
+
* A bulk request answers with a 200 even when some documents were rejected,
|
|
8
|
+
* so the per-item errors have to be checked explicitly.
|
|
9
|
+
*/
|
|
10
|
+
export declare function throwOnBulkErrors(response: any): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.throwOnBulkErrors = exports.chunkDocuments = void 0;
|
|
4
|
+
const DEFAULT_CHUNK_SIZE = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Splits documents into chunks so a bulk request never exceeds the
|
|
7
|
+
* Elasticsearch "http.max_content_length" limit (100mb by default).
|
|
8
|
+
*/
|
|
9
|
+
function* chunkDocuments(docs, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
10
|
+
const size = chunkSize > 0 ? chunkSize : DEFAULT_CHUNK_SIZE;
|
|
11
|
+
for (let i = 0; i < docs.length; i += size) {
|
|
12
|
+
yield docs.slice(i, i + size);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.chunkDocuments = chunkDocuments;
|
|
16
|
+
/**
|
|
17
|
+
* A bulk request answers with a 200 even when some documents were rejected,
|
|
18
|
+
* so the per-item errors have to be checked explicitly.
|
|
19
|
+
*/
|
|
20
|
+
function throwOnBulkErrors(response) {
|
|
21
|
+
if (!response || !response.errors) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const failures = (response.items || [])
|
|
25
|
+
.map((item) => item.index || item.create || item.update || item.delete)
|
|
26
|
+
.filter((item) => item && item.error);
|
|
27
|
+
if (failures.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const [first] = failures;
|
|
31
|
+
const error = new Error(`${failures.length} document(s) rejected by Elasticsearch. First failure on "${first._id}": ${first.error.type} - ${first.error.reason}`);
|
|
32
|
+
error.errors = failures.map((item) => ({
|
|
33
|
+
document: { _id: item._id },
|
|
34
|
+
reason: `${item.error.type} - ${item.error.reason}`,
|
|
35
|
+
}));
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
exports.throwOnBulkErrors = throwOnBulkErrors;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Elasticsearch7 = void 0;
|
|
4
4
|
const sdk_es7_1 = require("sdk-es7");
|
|
5
|
+
const bulk_1 = require("./bulk");
|
|
5
6
|
class Elasticsearch7 {
|
|
6
7
|
constructor(url, options) {
|
|
7
8
|
const { auth, url: cleanedUrl } = this.extractCredentials(url);
|
|
@@ -93,13 +94,20 @@ class Elasticsearch7 {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
async writeData(index, docs) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
let count = 0;
|
|
98
|
+
// Documents are sent by chunks, otherwise the bulk request may exceed
|
|
99
|
+
// the Elasticsearch "http.max_content_length" limit (100mb by default)
|
|
100
|
+
for (const chunk of (0, bulk_1.chunkDocuments)(docs, this.options.batchSize)) {
|
|
101
|
+
const bulk = [];
|
|
102
|
+
for (const doc of chunk) {
|
|
103
|
+
bulk.push({ index: { _index: index, _id: doc._id } });
|
|
104
|
+
bulk.push(doc._source);
|
|
105
|
+
}
|
|
106
|
+
const { body } = await this.client.bulk({ body: bulk });
|
|
107
|
+
(0, bulk_1.throwOnBulkErrors)(body);
|
|
108
|
+
count += bulk.length / 2;
|
|
100
109
|
}
|
|
101
|
-
|
|
102
|
-
return bulk.length / 2;
|
|
110
|
+
return count;
|
|
103
111
|
}
|
|
104
112
|
async clear() {
|
|
105
113
|
await this.client.indices.delete({ index: "_all" });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Elasticsearch8 = void 0;
|
|
4
4
|
const sdk_es8_1 = require("sdk-es8");
|
|
5
|
+
const bulk_1 = require("./bulk");
|
|
5
6
|
class Elasticsearch8 {
|
|
6
7
|
constructor(url, options) {
|
|
7
8
|
const { auth, url: cleanedUrl } = this.extractCredentials(url);
|
|
@@ -95,13 +96,20 @@ class Elasticsearch8 {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
async writeData(index, docs) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
let count = 0;
|
|
100
|
+
// Documents are sent by chunks, otherwise the bulk request may exceed
|
|
101
|
+
// the Elasticsearch "http.max_content_length" limit (100mb by default)
|
|
102
|
+
for (const chunk of (0, bulk_1.chunkDocuments)(docs, this.options.batchSize)) {
|
|
103
|
+
const bulk = [];
|
|
104
|
+
for (const doc of chunk) {
|
|
105
|
+
bulk.push({ index: { _index: index, _id: doc._id } });
|
|
106
|
+
bulk.push(doc._source);
|
|
107
|
+
}
|
|
108
|
+
const response = await this.client.bulk({ body: bulk });
|
|
109
|
+
(0, bulk_1.throwOnBulkErrors)(response);
|
|
110
|
+
count += bulk.length / 2;
|
|
102
111
|
}
|
|
103
|
-
|
|
104
|
-
return bulk.length / 2;
|
|
112
|
+
return count;
|
|
105
113
|
}
|
|
106
114
|
async clear() {
|
|
107
115
|
const indices = await this.listIndices();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isURL = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
(0, tslib_1.__exportStar)(require("./bulk"), exports);
|
|
5
6
|
(0, tslib_1.__exportStar)(require("./elasticsearch7"), exports);
|
|
6
7
|
(0, tslib_1.__exportStar)(require("./elasticsearch8"), exports);
|
|
7
8
|
(0, tslib_1.__exportStar)(require("./file"), exports);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kourou",
|
|
3
3
|
"description": "The CLI that helps you manage your Kuzzle instances",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0-dev.1",
|
|
5
5
|
"author": "The Kuzzle Team <support@kuzzle.io>",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kourou": "./bin/run"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@types/listr": "0.14.2",
|
|
58
58
|
"@types/mocha": "8.2.2",
|
|
59
59
|
"@types/ndjson": "2.0.0",
|
|
60
|
-
"@types/node": "
|
|
60
|
+
"@types/node": "20.14.15",
|
|
61
61
|
"@types/node-emoji": "1.8.1",
|
|
62
62
|
"@types/node-fetch": "2.6.1",
|
|
63
63
|
"@types/tar": "6.1.3",
|
|
@@ -75,15 +75,11 @@
|
|
|
75
75
|
"should": "13.2.3",
|
|
76
76
|
"source-map-support": "0.5.19",
|
|
77
77
|
"ts-node": "10.9.*",
|
|
78
|
-
"semantic-release
|
|
79
|
-
"semantic-release-
|
|
80
|
-
"@semantic-release/changelog": "6.0.3",
|
|
81
|
-
"@semantic-release/commit-analyzer": "13.0.0",
|
|
82
|
-
"@semantic-release/git": "10.0.1",
|
|
83
|
-
"@semantic-release/release-notes-generator": "14.0.1"
|
|
78
|
+
"semantic-release": "25.0.8",
|
|
79
|
+
"semantic-release-config-kuzzle": "1.7.0"
|
|
84
80
|
},
|
|
85
81
|
"engines": {
|
|
86
|
-
"node": ">=
|
|
82
|
+
"node": ">=20.0.0"
|
|
87
83
|
},
|
|
88
84
|
"files": [
|
|
89
85
|
"/bin",
|