Skip to content

Commit b34ba53

Browse files
committed
feat(cubejs-cli): Cube Cloud deploy implementation
1 parent e23f1fe commit b34ba53

File tree

6 files changed

+209
-2
lines changed

6 files changed

+209
-2
lines changed

packages/cubejs-cli/DeployDir.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const path = require('path');
2+
const fs = require('fs-extra');
3+
const crypto = require('crypto');
4+
5+
class DeployDir {
6+
constructor({ directory }) {
7+
this.directory = directory;
8+
}
9+
10+
filter(file) {
11+
let baseName = path.basename(file);
12+
return baseName !== 'node_modules' && baseName !== '.git' && baseName !== '.env';
13+
}
14+
15+
async fileHashes(directory) {
16+
directory = directory || this.directory;
17+
let result = {};
18+
19+
const files = await fs.readdir(directory);
20+
// eslint-disable-next-line no-restricted-syntax
21+
for (const file of files) {
22+
const filePath = path.resolve(directory, file);
23+
if (!this.filter(filePath)) {
24+
continue;
25+
}
26+
const stat = await fs.stat(filePath);
27+
if (stat.isDirectory()) {
28+
result = { ...result, ...await this.fileHashes(filePath) };
29+
} else {
30+
result[path.relative(this.directory, filePath)] = {
31+
hash: await this.fileHash(filePath)
32+
};
33+
}
34+
}
35+
return result;
36+
}
37+
38+
fileHash(file) {
39+
return new Promise((resolve, reject) => {
40+
const hash = crypto.createHash('sha1');
41+
return fs.createReadStream(file)
42+
.pipe(hash.setEncoding('hex'))
43+
.on('finish', () => {
44+
resolve(hash.digest('hex'))
45+
})
46+
.on('error', (err) => {
47+
reject(err);
48+
});
49+
})
50+
}
51+
}
52+
53+
module.exports = DeployDir;

packages/cubejs-cli/cubejsCli.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const spawn = require('cross-spawn');
1313
const crypto = require('crypto');
1414

1515
const templates = require('./templates');
16+
const { deploy } = require('./deploy');
1617
const { token, defaultExpiry, collect } = require('./token');
1718
const { requireFromPackage, event, displayError } = require('./utils');
1819

@@ -257,6 +258,22 @@ program
257258
console.log(' $ cubejs token -e "1 day" -p foo=bar -p cool=true');
258259
});
259260

261+
program
262+
.command('deploy')
263+
.option('-a, --auth [auth]', 'Cube Cloud Deploy Authentication Token. You can find it in Cube Cloud Deployment Settings')
264+
.description('Deploy project to Cube Cloud')
265+
.action(
266+
(options) => deploy({ directory: process.cwd(), ...options })
267+
.catch(e => displayError(e.stack || e))
268+
)
269+
.on('--help', () => {
270+
console.log('');
271+
console.log('Examples:');
272+
console.log('');
273+
console.log(' $ export CUBE_CLOUD_DEPLOY_AUTH=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBsb3ltZW50SWQiOiIxIiwidXJsIjoiaHR0cHM6Ly9leGFtcGxlcy5jdWJlY2xvdWQuZGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ.La3MiuqfGigfzADl1wpxZ7jlb6dY60caezgqIOoHt-c');
274+
console.log(' $ cubejs deploy');
275+
});
276+
260277
if (!process.argv.slice(2).length) {
261278
program.help();
262279
}

packages/cubejs-cli/deploy.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const rp = require('request-promise');
2+
const jwt = require('jsonwebtoken');
3+
const fs = require('fs-extra');
4+
const path = require('path');
5+
const DeployDir = require('./DeployDir');
6+
const cliProgress = require('cli-progress');
7+
const { logStage } = require('./utils');
8+
9+
const cloudReq = (options) => {
10+
const { url, auth, ...restOptions } = options;
11+
const authorization = auth || process.env.CUBE_CLOUD_DEPLOY_AUTH;
12+
const payload = jwt.decode(authorization);
13+
if (!payload.url || !payload.deploymentId) {
14+
throw new (`Malformed token: ${authorization}`);
15+
}
16+
return rp({
17+
headers: {
18+
authorization
19+
},
20+
...restOptions,
21+
url: `${payload.url}/${url(payload.deploymentId)}`,
22+
json: true
23+
});
24+
};
25+
26+
exports.deploy = async ({ directory, auth }) => {
27+
const bar = new cliProgress.SingleBar({
28+
format: '- Uploading files | {bar} | {percentage}% || {value} / {total} | {file}',
29+
barCompleteChar: '\u2588',
30+
barIncompleteChar: '\u2591',
31+
hideCursor: true
32+
});
33+
34+
const deployDir = new DeployDir({ directory });
35+
const fileHashes = await deployDir.fileHashes();
36+
const upstreamHashes = await cloudReq({
37+
url: (deploymentId) => `build/deploy/${deploymentId}/files`,
38+
method: 'GET',
39+
auth
40+
});
41+
const { transaction, deploymentName } = await cloudReq({
42+
url: (deploymentId) => `build/deploy/${deploymentId}/start-upload`,
43+
method: 'POST',
44+
auth
45+
});
46+
47+
await logStage(`Deploying ${deploymentName}...`, `Cube Cloud CLI Deploy`);
48+
49+
const files = Object.keys(fileHashes);
50+
bar.start(files.length, 0, {
51+
file: ''
52+
});
53+
54+
try {
55+
for (let i = 0; i < files.length; i++) {
56+
const file = files[i];
57+
bar.update(i, { file });
58+
if (!upstreamHashes[file] || upstreamHashes[file].hash !== fileHashes[file].hash) {
59+
await cloudReq({
60+
url: (deploymentId) => `build/deploy/${deploymentId}/upload-file`,
61+
method: 'POST',
62+
formData: {
63+
transaction: JSON.stringify(transaction),
64+
fileName: file,
65+
file: {
66+
value: fs.createReadStream(path.join(directory, file)),
67+
options: {
68+
filename: path.basename(file),
69+
contentType: 'application/octet-stream'
70+
}
71+
}
72+
},
73+
auth
74+
})
75+
}
76+
}
77+
bar.update(files.length, { file: 'Post processing...' });
78+
await cloudReq({
79+
url: (deploymentId) => `build/deploy/${deploymentId}/finish-upload`,
80+
method: 'POST',
81+
body: {
82+
transaction,
83+
files: fileHashes
84+
},
85+
auth
86+
});
87+
} finally {
88+
bar.stop();
89+
}
90+
await logStage(`Done 🎉`, `Cube Cloud CLI Deploy Success`);
91+
};

packages/cubejs-cli/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@
3131
],
3232
"dependencies": {
3333
"chalk": "^2.4.2",
34+
"cli-progress": "^3.8.2",
3435
"commander": "^2.19.0",
3536
"cross-spawn": "^7.0.1",
3637
"fs-extra": "^8.1.0",
3738
"jsonwebtoken": "^8.5.1",
3839
"node-fetch": "^2.6.0",
39-
"node-machine-id": "^1.1.10"
40+
"node-machine-id": "^1.1.10",
41+
"request-promise": "^4.2.5"
4042
},
4143
"devDependencies": {
4244
"eslint": "^6.8.0",

packages/cubejs-cli/utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,12 @@ exports.requireFromPackage = async (module) => {
5454
// eslint-disable-next-line global-require,import/no-dynamic-require
5555
return require(path.join(process.cwd(), 'node_modules', module));
5656
};
57+
58+
const logStage = async (stage, eventName, props) => {
59+
console.log(`- ${stage}`);
60+
if (eventName) {
61+
await event(eventName, props)
62+
}
63+
};
64+
65+
exports.logStage = logStage;

packages/cubejs-cli/yarn.lock

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,11 @@ bcrypt-pbkdf@^1.0.0:
649649
dependencies:
650650
tweetnacl "^0.14.3"
651651

652+
bluebird@^3.5.0:
653+
version "3.7.2"
654+
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
655+
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
656+
652657
brace-expansion@^1.1.7:
653658
version "1.1.11"
654659
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -793,6 +798,14 @@ cli-cursor@^3.1.0:
793798
dependencies:
794799
restore-cursor "^3.1.0"
795800

801+
cli-progress@^3.8.2:
802+
version "3.8.2"
803+
resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.8.2.tgz#abaf1fc6d6401351f16f068117a410554a0eb8c7"
804+
integrity sha512-qRwBxLldMSfxB+YGFgNRaj5vyyHe1yMpVeDL79c+7puGujdKJHQHydgqXDcrkvQgJ5U/d3lpf6vffSoVVUftVQ==
805+
dependencies:
806+
colors "^1.1.2"
807+
string-width "^4.2.0"
808+
796809
cli-width@^2.0.0:
797810
version "2.2.0"
798811
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
@@ -849,6 +862,11 @@ color-name@~1.1.4:
849862
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
850863
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
851864

865+
colors@^1.1.2:
866+
version "1.4.0"
867+
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
868+
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
869+
852870
combined-stream@^1.0.6, combined-stream@~1.0.6:
853871
version "1.0.7"
854872
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
@@ -3724,6 +3742,13 @@ [email protected]:
37243742
dependencies:
37253743
lodash "^4.17.11"
37263744

3745+
3746+
version "1.1.3"
3747+
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
3748+
integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
3749+
dependencies:
3750+
lodash "^4.17.15"
3751+
37273752
request-promise-native@^1.0.5:
37283753
version "1.0.7"
37293754
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
@@ -3733,6 +3758,16 @@ request-promise-native@^1.0.5:
37333758
stealthy-require "^1.1.1"
37343759
tough-cookie "^2.3.3"
37353760

3761+
request-promise@^4.2.5:
3762+
version "4.2.5"
3763+
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c"
3764+
integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==
3765+
dependencies:
3766+
bluebird "^3.5.0"
3767+
request-promise-core "1.1.3"
3768+
stealthy-require "^1.1.1"
3769+
tough-cookie "^2.3.3"
3770+
37363771
request@^2.87.0:
37373772
version "2.88.0"
37383773
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@@ -4205,7 +4240,7 @@ string-width@^3.0.0:
42054240
is-fullwidth-code-point "^2.0.0"
42064241
strip-ansi "^5.0.0"
42074242

4208-
string-width@^4.1.0:
4243+
string-width@^4.1.0, string-width@^4.2.0:
42094244
version "4.2.0"
42104245
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
42114246
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==

0 commit comments

Comments
 (0)