Skip to content

Commit af7e930

Browse files
committed
feat(cubejs-cli): Save deploy credentials
1 parent c523bbb commit af7e930

File tree

5 files changed

+204
-24
lines changed

5 files changed

+204
-24
lines changed

packages/cubejs-cli/Config.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const inquirer = require('inquirer');
2+
const fs = require('fs-extra');
3+
const os = require('os');
4+
const path = require('path');
5+
const jwt = require('jsonwebtoken');
6+
const rp = require('request-promise');
7+
8+
class Config {
9+
async loadConfig() {
10+
const { configFile } = this.configFile();
11+
if (await fs.exists(configFile)) {
12+
return await fs.readJson(configFile);
13+
}
14+
return {};
15+
}
16+
17+
async writeConfig(config) {
18+
const { cubeCloudConfigPath, configFile } = this.configFile();
19+
await fs.mkdirp(cubeCloudConfigPath);
20+
await fs.writeJson(configFile, config);
21+
}
22+
23+
configFile() {
24+
const cubeCloudConfigPath = this.cubeCloudConfigPath();
25+
const configFile = path.join(cubeCloudConfigPath, `config.json`);
26+
return { cubeCloudConfigPath, configFile };
27+
}
28+
29+
cubeCloudConfigPath() {
30+
return path.join(os.homedir(), '.cubecloud');
31+
}
32+
33+
async deployAuth(url) {
34+
if (process.env.CUBE_CLOUD_DEPLOY_AUTH) {
35+
const payload = jwt.decode(process.env.CUBE_CLOUD_DEPLOY_AUTH);
36+
if (!payload.url) {
37+
throw new Error(`Malformed token in CUBE_CLOUD_DEPLOY_AUTH`);
38+
}
39+
if (url && payload.url !== url) {
40+
throw new Error(`CUBE_CLOUD_DEPLOY_AUTH token doesn't match url in .cubecloud`);
41+
}
42+
return {
43+
[payload.url]: {
44+
auth: process.env.CUBE_CLOUD_DEPLOY_AUTH
45+
}
46+
};
47+
}
48+
const config = await this.loadConfig();
49+
if (config.auth) {
50+
return config.auth;
51+
} else {
52+
const auth = await inquirer.prompt([{
53+
name: 'auth',
54+
message: `Cube Cloud Auth Token${url ? ` for ${url}` : ''}`
55+
}]);
56+
const authToken = auth.auth;
57+
return (await this.addAuthToken(authToken, config)).auth;
58+
}
59+
}
60+
61+
async addAuthToken(authToken, config) {
62+
if (!config) {
63+
config = await this.loadConfig();
64+
}
65+
const payload = jwt.decode(authToken);
66+
if (!payload || !payload.url) {
67+
throw `Malformed Cube Cloud token`;
68+
}
69+
config.auth = config.auth || {};
70+
config.auth[payload.url] = {
71+
auth: authToken
72+
};
73+
await this.writeConfig(config);
74+
return config;
75+
}
76+
77+
async deployAuthForCurrentDir() {
78+
const dotCubeCloud = await this.loadDotCubeCloud();
79+
if (dotCubeCloud.url && dotCubeCloud.deploymentId) {
80+
const deployAuth = await this.deployAuth(dotCubeCloud.url);
81+
if (!deployAuth[dotCubeCloud.url]) {
82+
throw new Error(`Provided token isn't for ${dotCubeCloud.url}`);
83+
}
84+
return {
85+
...deployAuth[dotCubeCloud.url],
86+
url: dotCubeCloud.url,
87+
deploymentId: dotCubeCloud.deploymentId
88+
}
89+
}
90+
const auth = await this.deployAuth();
91+
let url = Object.keys(auth)[0];
92+
if (Object.keys(auth).length > 1) {
93+
url = (await inquirer.prompt([{
94+
type: 'list',
95+
name: 'url',
96+
message: 'Please select an organization',
97+
choices: Object.keys(auth)
98+
}])).url;
99+
}
100+
const authToken = auth[url];
101+
const deployments = await this.cloudReq({
102+
url: () => `build/deploy/deployments`,
103+
method: 'GET',
104+
auth: { ...authToken, url }
105+
});
106+
const { deployment } = await inquirer.prompt([{
107+
type: 'list',
108+
name: 'deployment',
109+
message: 'Please select a deployment to deploy to',
110+
choices: deployments
111+
}]);
112+
const deploymentId = deployments.find(d => d.name === deployment).id;
113+
await this.writeDotCubeCloud({
114+
url,
115+
deploymentId
116+
});
117+
return {
118+
...authToken,
119+
url,
120+
deploymentId
121+
}
122+
}
123+
124+
async loadDeployAuth() {
125+
this.preLoadDeployAuth = await this.deployAuthForCurrentDir();
126+
}
127+
128+
dotCubeCloudFile() {
129+
return '.cubecloud';
130+
}
131+
132+
async loadDotCubeCloud() {
133+
if (await fs.exists(this.dotCubeCloudFile())) {
134+
return await fs.readJson(this.dotCubeCloudFile());
135+
}
136+
return {};
137+
}
138+
139+
async writeDotCubeCloud(config) {
140+
await fs.writeJson(this.dotCubeCloudFile(), config);
141+
}
142+
143+
async cloudReq(options) {
144+
const { url, auth, ...restOptions } = options;
145+
const authorization = auth || this.preLoadDeployAuth;
146+
if (!authorization) {
147+
throw new Error('Auth isn\'t set');
148+
}
149+
return rp({
150+
headers: {
151+
authorization: authorization.auth
152+
},
153+
...restOptions,
154+
url: `${authorization.url}/${url(authorization.deploymentId)}`,
155+
json: true
156+
});
157+
}
158+
}
159+
160+
module.exports = Config;

packages/cubejs-cli/cubejsCli.js

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

15+
const Config = require('./Config');
1516
const templates = require('./templates');
1617
const { deploy } = require('./deploy');
1718
const { token, defaultExpiry, collect } = require('./token');
@@ -274,6 +275,21 @@ program
274275
console.log(' $ cubejs deploy');
275276
});
276277

278+
program
279+
.command('authenticate <token>')
280+
.description('Authenticate access to Cube Cloud')
281+
.action(
282+
(token) => new Config().addAuthToken(token)
283+
.catch(e => displayError(e.stack || e))
284+
)
285+
.on('--help', () => {
286+
console.log('');
287+
console.log('Examples:');
288+
console.log('');
289+
console.log(' $ cubejs authenticate eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBsb3ltZW50SWQiOiIxIiwidXJsIjoiaHR0cHM6Ly9leGFtcGxlcy5jdWJlY2xvdWQuZGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ.La3MiuqfGigfzADl1wpxZ7jlb6dY60caezgqIOoHt-c');
290+
console.log(' $ cubejs deploy');
291+
});
292+
277293
if (!process.argv.slice(2).length) {
278294
program.help();
279295
}

packages/cubejs-cli/deploy.js

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,11 @@ const path = require('path');
55
const cliProgress = require('cli-progress');
66
const DeployDir = require('./DeployDir');
77
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-
if (!authorization) {
13-
throw new Error('CUBE_CLOUD_DEPLOY_AUTH isn\'t set');
14-
}
15-
const payload = jwt.decode(authorization);
16-
if (!payload.url || !payload.deploymentId) {
17-
throw new Error(`Malformed token: ${authorization}`);
18-
}
19-
return rp({
20-
headers: {
21-
authorization
22-
},
23-
...restOptions,
24-
url: `${payload.url}/${url(payload.deploymentId)}`,
25-
json: true
26-
});
27-
};
8+
const Config = require('./Config');
289

2910
exports.deploy = async ({ directory, auth }) => {
11+
const config = new Config();
12+
await config.loadDeployAuth();
3013
const bar = new cliProgress.SingleBar({
3114
format: '- Uploading files | {bar} | {percentage}% || {value} / {total} | {file}',
3215
barCompleteChar: '\u2588',
@@ -36,12 +19,12 @@ exports.deploy = async ({ directory, auth }) => {
3619

3720
const deployDir = new DeployDir({ directory });
3821
const fileHashes = await deployDir.fileHashes();
39-
const upstreamHashes = await cloudReq({
22+
const upstreamHashes = await config.cloudReq({
4023
url: (deploymentId) => `build/deploy/${deploymentId}/files`,
4124
method: 'GET',
4225
auth
4326
});
44-
const { transaction, deploymentName } = await cloudReq({
27+
const { transaction, deploymentName } = await config.cloudReq({
4528
url: (deploymentId) => `build/deploy/${deploymentId}/start-upload`,
4629
method: 'POST',
4730
auth
@@ -59,7 +42,7 @@ exports.deploy = async ({ directory, auth }) => {
5942
const file = files[i];
6043
bar.update(i, { file });
6144
if (!upstreamHashes[file] || upstreamHashes[file].hash !== fileHashes[file].hash) {
62-
await cloudReq({
45+
await config.cloudReq({
6346
url: (deploymentId) => `build/deploy/${deploymentId}/upload-file`,
6447
method: 'POST',
6548
formData: {
@@ -78,7 +61,7 @@ exports.deploy = async ({ directory, auth }) => {
7861
}
7962
}
8063
bar.update(files.length, { file: 'Post processing...' });
81-
await cloudReq({
64+
await config.cloudReq({
8265
url: (deploymentId) => `build/deploy/${deploymentId}/finish-upload`,
8366
method: 'POST',
8467
body: {

packages/cubejs-cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"templates.js",
2828
"deploy.js",
2929
"DeployDir.js",
30+
"Config.js",
3031
"yarn.lock",
3132
"README.md",
3233
"LICENSE"
@@ -37,6 +38,7 @@
3738
"commander": "^2.19.0",
3839
"cross-spawn": "^7.0.1",
3940
"fs-extra": "^8.1.0",
41+
"inquirer": "^7.1.0",
4042
"jsonwebtoken": "^8.5.1",
4143
"node-fetch": "^2.6.0",
4244
"node-machine-id": "^1.1.10",

packages/cubejs-cli/yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,6 +1941,25 @@ inquirer@^7.0.0:
19411941
strip-ansi "^6.0.0"
19421942
through "^2.3.6"
19431943

1944+
inquirer@^7.1.0:
1945+
version "7.1.0"
1946+
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
1947+
integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
1948+
dependencies:
1949+
ansi-escapes "^4.2.1"
1950+
chalk "^3.0.0"
1951+
cli-cursor "^3.1.0"
1952+
cli-width "^2.0.0"
1953+
external-editor "^3.0.3"
1954+
figures "^3.0.0"
1955+
lodash "^4.17.15"
1956+
mute-stream "0.0.8"
1957+
run-async "^2.4.0"
1958+
rxjs "^6.5.3"
1959+
string-width "^4.1.0"
1960+
strip-ansi "^6.0.0"
1961+
through "^2.3.6"
1962+
19441963
invariant@^2.2.4:
19451964
version "2.2.4"
19461965
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"

0 commit comments

Comments
 (0)