UNPKG

11.9 kBJavaScriptView Raw
1const AWS = require('aws-sdk');
2const ora = require('ora');
3const prompt = require('prompt');
4const request = require('request');
5const path = require('path');
6const crypto = require('crypto');
7const zip = require('deterministic-zip');
8const fs = require('fs-extra');
9const auth = require('../lib/auth');
10const backupCredentials = require('../lib/cred').backup;
11const loadCredentials = require('../lib/cred').load;
12const { removeToken } = require('../lib/cred');
13const saveCredentials = require('../lib/cred').save;
14const authorisify = require('../lib/authorisify');
15const environments = require('../lib/environments');
16const sites = require('../lib/sites');
17const users = require('../lib/users');
18const notice = require('../lib/notice');
19const config = require('../config/config.json');
20const assertPkg = require('../lib/package-json').assert;
21const packageOptions = require('../lib/pkgOptions');
22
23const TMP_DIR = '/tmp/';
24const DIST_DIR = 'dist';
25
26const LINC_API_SITES_ENDPOINT = `${config.Api.LincBaseEndpoint}/sites`;
27const BUCKET_NAME = config.S3.deployBucket;
28
29const IdentityPoolId = 'eu-central-1:a05922c7-303d-4b8c-9843-60f5e590a812';
30
31prompt.colors = false;
32prompt.message = '';
33prompt.delimiter = '';
34
35let reference;
36
37const spinner = ora();
38
39// Some progress message for publishing process
40const messages = [
41 'Preparing upload.',
42 'Creating upload package.',
43 'Authorising.',
44 'Uploading package.',
45];
46const msgStart = (n) => spinner.start(`${messages[n]} Please wait...`);
47const msgSucceed = (n, m) => {
48 spinner.succeed(`${messages[n]} Done.`);
49 if (m) msgStart(m);
50};
51
52/**
53 * Convenience function to create SHA1 of a string
54 * @param s
55 */
56const sha1 = (s) => crypto.createHash('sha1').update(s).digest('hex');
57
58/**
59 * Create a zip file from a source_dir, named <site_name>.zip
60 * @param tempDir
61 * @param sourceDir
62 * @param siteName
63 * @param opts
64 */
65const 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 // Check whether the directory actually exists
72 fs.stat(options.cwd, (err) => {
73 if (err) return reject(err);
74
75 // Create zipfile from directory
76 return zip(localdir, zipfile, options, _err => {
77 if (_err) return reject(_err);
78
79 return resolve(zipfile);
80 });
81 });
82});
83
84/**
85 * Create key for S3.
86 * @param userId
87 * @param _sha1
88 * @param siteName
89 */
90const createKey = (userId, _sha1, siteName) => `${userId}/${siteName}-${_sha1}.zip`;
91
92/**
93 * Upload zip file to S3.
94 * @param description
95 * @param codeId
96 * @param siteName
97 * @param zipfile
98 * @param jwtToken
99 */
100const 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 * Ask user for a publication description.
136 * @param descr
137 */
138const 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 * Get credentials
160 * @param token
161 */
162const 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 * Actually upload the site (upload to S3, let backend take it from there).
179 * @param siteName
180 * @param description
181 * @param credentials
182 */
183const 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 * Retrieve deployment status using reference
205 * @param siteName
206 */
207const 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 * Wait for deploy to finish (or to time out - timeout is set to three minutes)
228 * @param envs
229 * @param siteName
230 */
231const waitForDeployToFinish = (envs, siteName) => new Promise((resolve, reject) => {
232 const Count = 20;
233 let Timeout = 4;
234
235 /**
236 * Check whether deploy has finished
237 * @param count
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 // eslint-disable-line comma-dangle
254 );
255 };
256
257 // Start the checking
258 return checkForDeployToFinish(Count);
259});
260
261/**
262 * Publish site
263 */
264const 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 * Copy existing .linc/credentials to .linc/credentials.bak
288 */
289const moveCredentials = async () => {
290 console.log(`I found credentials in this folder, but no siteName.
291As 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 * Ask for user credentials
302 */
303const 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 * Log in user
330 */
331const login = async () => {
332 console.log(`Your site has a name, but I can't find any credentials. Please log
333in 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 * Check for existing site name in back end
344 * @param siteName
345 */
346const 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 * Main entry point for this module.
368 * @param argv
369 */
370const publish = async (argv) => {
371 const { siteName } = argv;
372
373 let credentials = null;
374 try {
375 credentials = await loadCredentials();
376 } catch (e) {
377 // Empty block
378 }
379
380 /*
381 * Existing site name
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
395the instructions below. Please use the same email address you used
396when you originally signed up this site.
397`);
398 }
399
400 /*
401 * New site name
402 */
403 if (credentials) {
404 // No site name but credentials found? Move credentials out of the way
405 await moveCredentials();
406 }
407
408 if (!suppressSignupMessage) {
409 console.log(`It looks like you haven't signed up for this site yet.
410Please 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
419exports.command = ['publish', 'deploy'];
420exports.desc = 'Publish your site';
421exports.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};