1 | const AWS = require('aws-sdk');
|
2 | const ora = require('ora');
|
3 | const prompt = require('prompt');
|
4 | const request = require('request');
|
5 | const path = require('path');
|
6 | const crypto = require('crypto');
|
7 | const zip = require('deterministic-zip');
|
8 | const fs = require('fs-extra');
|
9 | const auth = require('../lib/auth');
|
10 | const backupCredentials = require('../lib/cred').backup;
|
11 | const loadCredentials = require('../lib/cred').load;
|
12 | const { removeToken } = require('../lib/cred');
|
13 | const saveCredentials = require('../lib/cred').save;
|
14 | const authorisify = require('../lib/authorisify');
|
15 | const environments = require('../lib/environments');
|
16 | const sites = require('../lib/sites');
|
17 | const users = require('../lib/users');
|
18 | const notice = require('../lib/notice');
|
19 | const config = require('../config/config.json');
|
20 | const assertPkg = require('../lib/package-json').assert;
|
21 | const packageOptions = require('../lib/pkgOptions');
|
22 |
|
23 | const TMP_DIR = '/tmp/';
|
24 | const DIST_DIR = 'dist';
|
25 |
|
26 | const LINC_API_SITES_ENDPOINT = `${config.Api.LincBaseEndpoint}/sites`;
|
27 | const BUCKET_NAME = config.S3.deployBucket;
|
28 |
|
29 | const IdentityPoolId = 'eu-central-1:a05922c7-303d-4b8c-9843-60f5e590a812';
|
30 |
|
31 | prompt.colors = false;
|
32 | prompt.message = '';
|
33 | prompt.delimiter = '';
|
34 |
|
35 | let reference;
|
36 |
|
37 | const spinner = ora();
|
38 |
|
39 |
|
40 | const messages = [
|
41 | 'Preparing upload.',
|
42 | 'Creating upload package.',
|
43 | 'Authorising.',
|
44 | 'Uploading package.',
|
45 | ];
|
46 | const msgStart = (n) => spinner.start(`${messages[n]} Please wait...`);
|
47 | const msgSucceed = (n, m) => {
|
48 | spinner.succeed(`${messages[n]} Done.`);
|
49 | if (m) msgStart(m);
|
50 | };
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | const sha1 = (s) => crypto.createHash('sha1').update(s).digest('hex');
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | const createZipfile = (tempDir, sourceDir, siteName, opts) => new Promise((resolve, reject) => {
|
66 | const options = opts || { cwd: process.cwd() };
|
67 | const { cwd } = options;
|
68 | const localdir = path.join(cwd, sourceDir);
|
69 | const zipfile = path.join(tempDir, `${siteName}.zip`);
|
70 |
|
71 |
|
72 | fs.stat(options.cwd, (err) => {
|
73 | if (err) return reject(err);
|
74 |
|
75 |
|
76 | return zip(localdir, zipfile, options, _err => {
|
77 | if (_err) return reject(_err);
|
78 |
|
79 | return resolve(zipfile);
|
80 | });
|
81 | });
|
82 | });
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | const createKey = (userId, _sha1, siteName) => `${userId}/${siteName}-${_sha1}.zip`;
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | const uploadZipfile = (description, codeId, siteName, zipfile, jwtToken) => new Promise((resolve, reject) => {
|
101 | reference = sha1(`${siteName}${Math.floor(new Date() / 1000).toString()}`);
|
102 | const key = createKey(AWS.config.credentials.identityId, codeId, siteName);
|
103 |
|
104 | msgSucceed(2, 3);
|
105 |
|
106 | const stream = fs.createReadStream(zipfile);
|
107 | const params = {
|
108 | Body: stream,
|
109 | Bucket: BUCKET_NAME,
|
110 | Key: key,
|
111 | Metadata: {
|
112 | description,
|
113 | reference,
|
114 | jwt: jwtToken,
|
115 | },
|
116 | };
|
117 |
|
118 | const upload = new AWS.S3.ManagedUpload({ params });
|
119 | upload.on('httpUploadProgress', (progress) => {
|
120 | const loadedInKB = Math.floor(progress.loaded / 1024);
|
121 | spinner.start(`Uploading ${loadedInKB} KB. Please wait...`);
|
122 | });
|
123 | return upload.send((err) => {
|
124 | if (err) {
|
125 | spinner.fail('Upload failed.');
|
126 | return reject(err);
|
127 | }
|
128 |
|
129 | msgSucceed(3);
|
130 | return resolve();
|
131 | });
|
132 | });
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | const askDescription = (descr) => new Promise((resolve, reject) => {
|
139 | console.log('It\'s beneficial to provide a description for your deployment.');
|
140 |
|
141 | const schema = {
|
142 | properties: {
|
143 | description: {
|
144 | description: 'Description:',
|
145 | default: descr,
|
146 | required: false,
|
147 | },
|
148 | },
|
149 | };
|
150 | prompt.start();
|
151 | prompt.get(schema, (err, result) => {
|
152 | if (err) return reject(err);
|
153 |
|
154 | return resolve(result);
|
155 | });
|
156 | });
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | const getCredentials = (token) => new Promise((resolve, reject) => {
|
163 | AWS.config.region = 'eu-central-1';
|
164 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
165 | IdentityPoolId,
|
166 | Logins: {
|
167 | 'cognito-idp.eu-central-1.amazonaws.com/eu-central-1_fLLmXhVcs': token,
|
168 | },
|
169 | });
|
170 | return AWS.config.credentials.get((err) => {
|
171 | if (err) return reject(err);
|
172 |
|
173 | return resolve();
|
174 | });
|
175 | });
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | const uploadSite = async (siteName, description, credentials) => {
|
184 | const rendererPath = `${process.cwd()}/dist/lib/server-render.js`;
|
185 | const renderer = fs.readFileSync(rendererPath);
|
186 | const codeId = sha1(renderer);
|
187 |
|
188 | msgSucceed(0, 1);
|
189 |
|
190 | const tempDir = fs.mkdtempSync(`${TMP_DIR}linc-`);
|
191 |
|
192 | await createZipfile(tempDir, DIST_DIR, siteName);
|
193 | const zipFile = await createZipfile(TMP_DIR, '/', siteName, { cwd: tempDir });
|
194 |
|
195 | msgSucceed(1, 2);
|
196 |
|
197 | const jwtToken = await auth(credentials.accessKey, credentials.secretKey);
|
198 | const token = await auth.getIdToken();
|
199 | await getCredentials(token);
|
200 | return uploadZipfile(description, codeId, siteName, zipFile, jwtToken);
|
201 | };
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | const retrieveDeploymentStatus = (siteName) => (jwtToken) => new Promise((resolve, reject) => {
|
208 | const options = {
|
209 | method: 'GET',
|
210 | url: `${LINC_API_SITES_ENDPOINT}/${siteName}/deployments/${reference}`,
|
211 | headers: {
|
212 | 'Content-Type': 'application/json',
|
213 | Authorization: `X-Bearer ${jwtToken}`,
|
214 | },
|
215 | };
|
216 | request(options, (err, response, body) => {
|
217 | if (err) return reject(err);
|
218 |
|
219 | const json = JSON.parse(body);
|
220 | if (response.statusCode === 200) return resolve(json.statuses);
|
221 |
|
222 | return reject(new Error(`Error ${response.statusCode}: ${response.statusMessage}`));
|
223 | });
|
224 | });
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 | const waitForDeployToFinish = (envs, siteName) => new Promise((resolve, reject) => {
|
232 | const Count = 20;
|
233 | let Timeout = 4;
|
234 |
|
235 | |
236 |
|
237 |
|
238 |
|
239 | const checkForDeployToFinish = (count) => {
|
240 | if (count === 0) {
|
241 | return reject(new Error('The process timed out'));
|
242 | }
|
243 |
|
244 | return setTimeout(
|
245 | () => authorisify(retrieveDeploymentStatus(siteName))
|
246 | .then(s => {
|
247 | if (s.length === envs.length) return resolve(s);
|
248 |
|
249 | Timeout = 8;
|
250 | return checkForDeployToFinish(count - 1);
|
251 | })
|
252 | .catch(err => reject(err)),
|
253 | Timeout * 1000
|
254 | );
|
255 | };
|
256 |
|
257 |
|
258 | return checkForDeployToFinish(Count);
|
259 | });
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | const publishSite = async (credentials, siteName) => {
|
265 | spinner.start('Checking for profile package. Please wait...');
|
266 | await packageOptions(['buildProfile']);
|
267 | spinner.succeed('Profile package installed.');
|
268 |
|
269 | const { description } = await askDescription('');
|
270 | console.log();
|
271 |
|
272 | msgStart(0);
|
273 | await sites.authoriseSite(siteName);
|
274 |
|
275 | let envs = await environments.getAvailableEnvironments(siteName);
|
276 | const listOfEnvironments = envs.environments.map(x => x.name);
|
277 |
|
278 | await uploadSite(siteName, description, credentials);
|
279 | envs = await waitForDeployToFinish(listOfEnvironments, siteName);
|
280 | spinner.succeed('Deployment finished');
|
281 |
|
282 | console.log('\nThe following deploy URLs were created:');
|
283 | envs.forEach(e => console.log(` https://${e.url} (${e.env})`));
|
284 | };
|
285 |
|
286 |
|
287 |
|
288 |
|
289 | const moveCredentials = async () => {
|
290 | console.log(`I found credentials in this folder, but no siteName.
|
291 | As a precaution, I have moved your existing credentials:
|
292 |
|
293 | .linc/credentials.bak -> .linc/credentials.
|
294 | `);
|
295 |
|
296 | await backupCredentials();
|
297 | await removeToken();
|
298 | };
|
299 |
|
300 |
|
301 |
|
302 |
|
303 | const credentialsFromPrompt = () => new Promise((resolve, reject) => {
|
304 | const schema = {
|
305 | properties: {
|
306 | access_key_id: {
|
307 | description: 'Access key:',
|
308 | required: true,
|
309 | },
|
310 | secret_access_key: {
|
311 | description: 'Secret key:',
|
312 | hidden: true,
|
313 | },
|
314 | },
|
315 | };
|
316 |
|
317 | prompt.start();
|
318 | prompt.get(schema, (err, result) => {
|
319 | if (err) return reject(err);
|
320 |
|
321 | return resolve({
|
322 | access_key_id: result.access_key_id,
|
323 | secret_access_key: result.secret_access_key,
|
324 | });
|
325 | });
|
326 | });
|
327 |
|
328 |
|
329 |
|
330 |
|
331 | const login = async () => {
|
332 | console.log(`Your site has a name, but I can't find any credentials. Please log
|
333 | in by entering your access key and secret key when prompted.
|
334 | `);
|
335 |
|
336 | const credentials = await credentialsFromPrompt();
|
337 | await auth(credentials.access_key_id, credentials.secret_access_key);
|
338 | await backupCredentials();
|
339 | return saveCredentials(credentials.access_key_id, credentials.secret_access_key);
|
340 | };
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 | const existingSite = (siteName) => {
|
347 | const existingSites = [
|
348 | 'coffee-bean-ninja',
|
349 | 'linc-react-games',
|
350 | 'localised',
|
351 | 'fysho-web',
|
352 | 'geodash',
|
353 | 'armory-react',
|
354 | 'linc-demo-site',
|
355 | 'buildkite-www-t',
|
356 | 'bankwest-test',
|
357 | 'fysho',
|
358 | 'linc-demo',
|
359 | 'jetstar-cards',
|
360 | 'react-trello-board',
|
361 | 'cath-github-demo',
|
362 | ];
|
363 | return existingSites.indexOf(siteName) >= 0;
|
364 | };
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | const publish = async (argv) => {
|
371 | const { siteName } = argv;
|
372 |
|
373 | let credentials = null;
|
374 | try {
|
375 | credentials = await loadCredentials();
|
376 | } catch (e) {
|
377 |
|
378 | }
|
379 |
|
380 | |
381 |
|
382 |
|
383 | let suppressSignupMessage = false;
|
384 | if (siteName) {
|
385 | if (credentials) return publishSite(credentials, siteName);
|
386 |
|
387 | if (!existingSite(siteName)) {
|
388 | credentials = await login();
|
389 | return publishSite(credentials, siteName);
|
390 | }
|
391 |
|
392 | suppressSignupMessage = true;
|
393 |
|
394 | console.log(`Your existing site ${siteName} needs to be ported. Please follow
|
395 | the instructions below. Please use the same email address you used
|
396 | when you originally signed up this site.
|
397 | `);
|
398 | }
|
399 |
|
400 | |
401 |
|
402 |
|
403 | if (credentials) {
|
404 |
|
405 | await moveCredentials();
|
406 | }
|
407 |
|
408 | if (!suppressSignupMessage) {
|
409 | console.log(`It looks like you haven't signed up for this site yet.
|
410 | Please follow the steps to create your credentials.
|
411 | `);
|
412 | }
|
413 |
|
414 | credentials = await users.signup(siteName);
|
415 | const pkg = await packageOptions(['siteName']);
|
416 | return publishSite(credentials, pkg.linc.siteName);
|
417 | };
|
418 |
|
419 | exports.command = ['publish', 'deploy'];
|
420 | exports.desc = 'Publish your site';
|
421 | exports.handler = (argv) => {
|
422 | assertPkg();
|
423 |
|
424 | notice();
|
425 |
|
426 | publish(argv)
|
427 | .then(() => {})
|
428 | .catch(err => {
|
429 | spinner.stop();
|
430 | console.log(err);
|
431 | });
|
432 | };
|