-
Notifications
You must be signed in to change notification settings - Fork 107
Expand file tree
/
Copy pathsandbox.js
More file actions
1819 lines (1668 loc) · 71.3 KB
/
sandbox.js
File metadata and controls
1819 lines (1668 loc) · 71.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
var request = require('request');
var util = require('util');
var {table} = require('table');
var open = require('open');
var auth = require('./auth');
var instance = require('./instance');
var console = require('./log');
var dwjson = require('./dwjson').init();
var ocapi = require('./ocapi');
var readline = require('readline');
const API_HOST_DEFAULT = 'admin.dx.commercecloud.salesforce.com';
const API_HOST = getAPIHost();
const API_BASE = API_HOST + '/api/v1';
const API_SANDBOXES = API_BASE + '/sandboxes';
const API_SYSTEM = API_BASE + '/system';
const SANDBOX_API_POLLING_TIMEOUT = 1000 * 60 * 15; // 15 minutes
// ocapi settings to apply to sandbox at provisioning time, CLIENTID to be set beforehand
const SANDBOX_OCAPI_SETTINGS = [{ client_id: "CLIENTID",
resources: [
{ resource_id: "/code_versions", methods: ["get"], read_attributes: "(**)", write_attributes: "(**)" },
{ resource_id: "/code_versions/*", methods: ["patch","delete"],
read_attributes: "(**)", write_attributes: "(**)" },
{ resource_id: "/jobs/*/executions", methods: ["post"], read_attributes: "(**)", write_attributes: "(**)" },
{ resource_id: "/jobs/*/executions/*", methods: ["get"], read_attributes: "(**)", write_attributes: "(**)" },
{ resource_id: "/sites/*/cartridges", methods: ["post"], read_attributes: "(**)", write_attributes: "(**)" }
]
}];
// webdav permissions to apply to sandbox at provisioning time, CLIENTID to be set beforehand
const SANDBOX_WEBDAV_PERMISSIONS = [{ client_id: "CLIENTID",
permissions: [
{ path: "/impex", operations: ["read_write"] },
{ path: "/cartridges", operations: ["read_write"] },
{ path: "/static", operations: ["read_write"] }
]
}];
const SANDBOX_STATUS_POLL_TIMEOUT = 5000;
const SANDBOX_STATUS_POLL_ERROR_THRESHOLD = 3;
const SANDBOX_STATUS_UP_AND_RUNNING = 'started';
const SANDBOX_STATUS_DOWN = 'stopped';
const SANDBOX_STATUS_FAILED = 'failed';
const BM_PATH = '/on/demandware.store/Sites-Site';
const SANDBOX_RESOURCE_PROFILES = [ 'medium', 'large', 'xlarge' ];
// enable request debugging
if ( process.env.DEBUG ) {
require('request-debug')(request);
}
/**
* Utility function to lookup the sandbox API host name. By default it is the host name
* defined as API_HOST_DEFAULT. The default API host can be overwritten using the environment
* variable SFCC_SANDBOX_API_HOST.
*
* @return {String} the API host name
*/
function getAPIHost() {
// check on env var and return it if set
if ( process.env.SFCC_SANDBOX_API_HOST ) {
console.debug('Using alternative sandbox API host %s defined in env var `SFCC_SANDBOX_API_HOST`',
process.env.SFCC_SANDBOX_API_HOST);
return process.env.SFCC_SANDBOX_API_HOST;
}
// return the default host otherwise
return API_HOST_DEFAULT;
}
/**
* Utility function to wait for target sandbox to have a specific status. SetInterval calls
* internal getSandox() at the rate defined in SANDBOX_STATUS_POLL_TIMEOUT until the target
* status is found, or an error occurs.
*
* @param {Object} targetSandbox - sandbox to target status checks against
* @param {String} targetStatus - sandbox status to check for, e.g. started, stopped
*/
function waitForSandboxStatus(targetSandbox, targetStatus) {
var errorThreshold = SANDBOX_STATUS_POLL_ERROR_THRESHOLD;
var startTime = Date.now();
var finished = false;
var fault = null;
function pollSandbox() {
// check for auth token before anything
// ocapi.retryableCall will exit without a callback causing an infinite loop if auth expires
if (!auth.getToken()) {
clearInterval(timeout);
console.error('Authorization no longer valid. Please (re-)authenticate first by running ' +
'`sfcc-ci auth:login` or `sfcc-ci client:auth`');
return;
}
// in case we come back around
if (finished) {
clearInterval(timeout);
return;
}
// check if operation timeout has been exceeded
if ((Date.now() - startTime) > getSandboxAPIPollingTimeout()) {
// report an exceeded operation timeout
clearInterval(timeout);
console.error('Sandbox polling timeout has been exceeded.');
return;
}
getSandbox(targetSandbox.id, null, function(err, sandbox) {
// check if operation timeout has been exceeded
if (err && errorThreshold > 0) {
// in case status retrieval failed
// decrease the error threshold
errorThreshold--;
// don't set an error and allow the polling to continue
console.debug('Polling sandbox status failed. Polling error threshold not reached. Continuing.');
} else if (err && errorThreshold === 0) {
// report a reached error threshold during polling
fault = {
message : 'Polling sandbox status failed. Error threshold reached. Stop polling, ' +
'the instantiating command may still be running. Detailed error was ' + err.message
};
} else if (!err) {
// update the status
if (sandbox && sandbox.state === targetStatus) {
finished = true;
} else if (sandbox && sandbox.state === SANDBOX_STATUS_FAILED) {
// if sandbox status is failed, we should exit
finished = true;
// report a failed sandbox
fault = {
message : 'Sandbox has a `failed` status. Please investigate.'
};
}
}
if (!finished && !fault) {
// continue polling
return;
}
// update duration
var duration = Date.now() - startTime;
// clear polling
clearInterval(timeout);
// resturn result
if (fault) {
// polling expired or sandbox state is failed
// treat this as error
console.error(fault.message);
} else {
console.info(util.format('Sandbox %s-%s with ID %s, was found with status `%s` (%s ms).',
sandbox.realm, sandbox.instance, sandbox.id, sandbox.state,duration));
console.info('You may use `sfcc-ci sandbox:list` to check the status of all sandboxes.');
}
});
}
// setup recurring
var timeout = setInterval(pollSandbox, SANDBOX_STATUS_POLL_TIMEOUT);
// trigger immediately
pollSandbox();
}
/**
* Utility function to lookup the sandbox API polling timeout. By default it is defined as
* SANDBOX_API_POLLING_TIMEOUT. The timeout can be overwritten using the environment
* variable SFCC_SANDBOX_API_POLLING_TIMEOUT.
*
* @return {Number} the polling timeout in milliseconds
*/
function getSandboxAPIPollingTimeout() {
// check on env var and return it if set
if ( process.env.SFCC_SANDBOX_API_POLLING_TIMEOUT ) {
// convert to milliseconds
return ( 1000 * 60 * process.env.SFCC_SANDBOX_API_POLLING_TIMEOUT );
}
// return the default polling timeout
return SANDBOX_API_POLLING_TIMEOUT;
}
/**
* Helper to capture most-common responses due to errors which occur across resources. In case a well-known issue
* was identified, the function returns an Error object holding detailed information about the error. A callback
* function can be passed optionally, the error and the response are passed as parameters to the callback function.
*
* @param {Object} err
* @param {Object} response
* @param {Function} callback
* @return {Error} the error or null
*/
function captureCommonErrors(err, response, callback) {
var error = null;
if (err && !response) {
error = new Error('The operation could not be performed properly. ' + ( process.env.DEBUG ? err : '' ));
} else if (response.statusCode === 401) {
error = new Error('Authorization invalid. Please (re-)authenticate first by running ' +
'´sfcc-ci auth:login´ or ´sfcc-ci client:auth´');
} else if (response.statusCode >= 400 && response['body'] && response['body']['error'] ) {
error = new Error(response['body']['error']['message']);
}
// just return the error, in case no callback is passed
if (!callback) {
return error;
}
callback(error, response);
}
/**
* Prints the given query on the console and waits for user input (<Enter key>).
*
* @param query query to print on console
*/
function askQuestion(query) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => rl.question(query, ans => {
rl.close();
resolve(ans);
}))
}
/**
* Prints the inbound IP addresses of the cluster on the console.
*/
function printInboundIPs(realm, json, cli) {
var endpoint = API_SYSTEM;
if (realm !== null) {
endpoint = API_BASE + `/realms/${realm}/system`;
}
var options = ocapi.getOptions('GET', endpoint);
ocapi.retryableCall('GET', options, function (err, res) {
if (err) {
console.error(err)
} else if (res.statusCode >= 400) {
console.error(res.body)
} else {
if (json) {
if (cli) {
console.json({'inbound': res.body.data.inboundIps});
} else {
console.json(res.body.data.inboundIps);
}
return;
}
console.log('');
console.log('Inbound IP addresses' + ( realm !== null ? ` (for realm ${realm})` : '' ) + ':');
// table fields
var data = [['address']];
for (var i in res.body.data.inboundIps) {
data.push([res.body.data.inboundIps[i]]);
}
console.table(data);
}
});
}
/**
* Prints the outbound IP addresses of the cluster on the console.
*/
function printOutboundIPs(realm, json, cli) {
var endpoint = API_SYSTEM;
if (realm !== null) {
endpoint = API_BASE + `/realms/${realm}/system`;
}
var options = ocapi.getOptions('GET', endpoint);
ocapi.retryableCall('GET', options, function (err, res) {
if (err) {
console.error(err)
} else if (res.statusCode >= 400) {
console.error(res.body)
} else {
if (json) {
if (cli) {
console.json({'outbound': res.body.data.outboundIps});
} else {
console.json(res.body.data.outboundIps);
}
return;
}
console.log('');
console.log('Outbound IP addresses' + ( realm !== null ? ` (for realm ${realm})` : '' ) + ':');
// table fields
var data = [['address']];
for (var i in res.body.data.outboundIps) {
data.push([res.body.data.outboundIps[i]]);
}
console.table(data);
}
});
}
/**
* Attempts the merge API settings. Checks the passed settingsAsJSON string for validity.
* Checks if the JSON string is syntactically correct and does a basic semantic check for
* the same API client as used by the CLI client. Throws an error if a check failed.
*
* Note, we allow to pass API settings for the same API client as granted to in the base
* settings, since we amend additional API resources, we don't replace them.
*
* @param {Object} baseSettings the base API settings as object
* @param {String} resourceProp the property of the API permissions that holds the actual perm set
* @param {String} settingsAsJSON the settings to merge into the base settings as JSON string
* @return {Object} the merged API settings
*/
function mergeAPISettings(baseSettings, resourceProp, settingsAsJSON) {
var settingsToMerge;
// syntactical check
try {
settingsToMerge = JSON.parse(settingsAsJSON);
} catch (err) {
console.debug(`Invalid JSON: ${err.message}`);
throw new Error(`Invalid JSON`);
}
var finalSettings = [];
// sematical check
if (settingsToMerge['client_id'] && settingsToMerge.client_id === auth.getClient()) {
console.debug(`Patch existing permissions. Amending API resources.`);
baseSettings[0][resourceProp] = baseSettings[0][resourceProp].concat(settingsToMerge[resourceProp]);
finalSettings = baseSettings;
} else if (Array.isArray(settingsToMerge)) {
// in case of a multiple clients
settingsToMerge.forEach(function(client) {
if (client['client_id'] && client.client_id === auth.getClient()) {
console.debug(`Patch existing permissions. Amending API resources.`);
baseSettings[0][resourceProp] = baseSettings[0][resourceProp].concat(client[resourceProp]);
} else {
// simply concat
finalSettings = finalSettings.concat(client);
}
});
finalSettings = baseSettings.concat(finalSettings);
} else {
// simply concat
finalSettings = finalSettings.concat(settingsToMerge);
}
console.debug(`Merged API settings: ${JSON.stringify(finalSettings)}`);
return finalSettings;
}
/**
* Retrieves all realms and returns them as array.
*
* @param {Function} callback the callback to execute, the error and the list of realms are available as arguments to the callback function
*/
function getRealms(callback) {
ocapi.retryableCall('GET', API_BASE + '/me', function(err, res) {
if ( err ) {
callback(new Error(util.format('Getting realms failed: %s', err)), []);
} else if ( res.statusCode >= 400 ) {
callback(new Error(util.format('Getting realms failed: %s', res.statusCode)));
} else {
callback(undefined, res.body['data']['realms']);
}
});
}
/**
* Retrieves details of a realm.
*
* @param {String} realmID the id of the realm
* @param {String} topic the topic to retrieve details about
* @param {Function} callback the callback to execute, the error and the realm are available as arguments to the callback function
*/
function getRealm(realmID, topic, callback) {
// build the request options
// use some default expansions
var extension = '?expand=configuration,usage';
// optionally retrieve details from different endpoints
if ( topic !== null ) {
extension = '/' + topic
// for retrieving usage data, always retrieve full usage
if ( topic === 'usage' ) {
extension += '?from=2019-01-01';
}
}
ocapi.retryableCall('GET', API_BASE + '/realms/' + realmID + extension, function(err, res) {
if ( err ) {
callback(new Error(util.format('Getting realm failed: %s', err)), []);
return;
} else if ( res.statusCode >= 400 ) {
callback(new Error(util.format('Getting realm failed: %s', res.statusCode)));
return;
}
callback(undefined, res.body['data']);
});
}
/**
* Update realm settings
*
* @param {String} realmID the realm (id) to update
* @param {Number} maxSandboxTTL the new maximum sandbox ttl
* @param {Number} defaultSandboxTTL the new default sandbox ttl
* @param {Function} callback the callback to execute, the error and the realm details are available as arguments to the callback function
*/
function updateRealm(realmID, maxSandboxTTL, defaultSandboxTTL, startScheduler, stopScheduler, callback) {
// build the request options
var options = ocapi.getOptions('PATCH', API_BASE + '/realms/' + realmID + '/configuration');
// the payload
options['body'] = { sandbox : { sandboxTTL : {} } };
if (maxSandboxTTL) {
options['body']['sandbox']['sandboxTTL']['maximum'] = maxSandboxTTL.toFixed();
}
if (defaultSandboxTTL) {
options['body']['sandbox']['sandboxTTL']['defaultValue'] = defaultSandboxTTL.toFixed();
}
//We need to set explicit "null" to remove the existing schedule
if (startScheduler) {
if ( startScheduler === "null" ) {
options['body']['sandbox']['startScheduler'] = null;
} else {
options['body']['sandbox']['startScheduler'] = startScheduler;
}
}
if (stopScheduler) {
if ( stopScheduler === "null" ) {
options['body']['sandbox']['stopScheduler'] = null;
} else {
options['body']['sandbox']['stopScheduler'] = stopScheduler;
}
}
ocapi.retryableCall('PATCH', options, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Updating realm settings failed: %s', body.message));
} else if (err) {
errback = new Error(util.format('Updating realm settings failed: %s', err));
}
}
callback(errback, res.body['data']);
});
}
/**
* Retrieves all known sandboxes and returns them as array.
*
* @param {Boolean} includeDeleted whether or not to include deleted sandboxes, false by default
* @param {Function} callback the callback to execute, the error and the list of sandboxes are available as arguments to the callback function
*/
function getAllSandboxes(includeDeleted, callback) {
ocapi.retryableCall('GET', API_SANDBOXES + ( includeDeleted ? '?include_deleted=true' : '' ), function(err, res) {
var list = [];
if ( err ) {
callback(new Error(util.format('Retrieving list of sandboxes failed: %s', err)), []);
return;
} else if ( res.statusCode >= 400 ) {
callback(new Error(util.format('Retrieving list of sandboxes failed: %s', res.statusCode)));
return;
}
callback(undefined, res.body.data);
});
}
/**
* Convenience function to lookup a sandbox by its alias, host or by its realm along with the instance.
* Throws an error if there was more than one sandbox found.
*
* @param {Object} spec an object containing properties alias, host, realm and instance
* @param {Function} callback callback function with err and the sandbox as parameters passed
*/
function lookupSandbox(spec, callback) {
var host = null;
if (spec['alias']) {
// attempt to lookup by alias
host = instance.lookupInstance(spec['alias']);
} else if (spec['host']) {
// or use the passed host
host = spec['host'];
}
// all sandboxes and filter them
getAllSandboxes(false, function(err, list) {
if (err) {
callback(err);
return;
}
if ( typeof(list) === 'undefined' || list.length === 0 ) {
callback(new Error('Cannot lookup sandbox.'), null);
return;
}
var filtered = list.filter(function(cand) {
// check on filter criterias
return ( cand.id === spec['id'] || cand.realm === spec['realm'] && cand.instance === spec['instance'] ||
cand.hostName === host );
});
if ( filtered.length === 0 ) {
callback(new Error('Cannot find sandbox with given ID.'), null);
return;
} else if ( filtered.length === 1 ) {
callback(undefined, filtered[0]);
return;
}
// multiple sandboxes should be the very exception and in those cases the most recently created
// sandbox should be addressed with the requested operation. in all other cases the operation should
// be skipped and error messages returned.
filtered = filtered.sort( function sortFilteredSandboxesDesc(sb1, sb2) {
return sb1.createdAt.localeCompare(sb2.createdAt) * -1;
});
var sandboxIndex = 1;
filtered.forEach( function(sbxInfo) {
if (sandboxIndex === 1) {
console.warn('More than one sandbox found. Executing the operation only for '
+ 'the sandbox that has been created on ' + sbxInfo.createdAt);
callback(undefined , sbxInfo);
} else {
callback(new Error('Ambigious sandbox ID. Skipping operation for the sandbox that has '
+ 'been created on ' + sbxInfo.createdAt));
}
sandboxIndex++;
});
});
}
/**
* Creates a sandbox for given realm. Realm ID can be passed as parameter or provided dw.json file located in the current working directory.
*
* @param {String} realm the realm to create the sandbox for
* @param {String} ttl the ttl of the sandbox in hours
* @param {String} profile the resource profile of the sandbox
* @param {Boolean} autoScheduled sets the sandbox as auto scheduled
* @param {String} startScheduler start schedule for the sandbox
* @param {String} stopScheduler stop schedule for the sandbox
* @param {String} additionalOcapiSettings JSON string holding additonal OCAPI settings to pass
* @param {String} additionalWebdavSettings JSON string holding additonal WebDAV permissions to pass
* @param {Function} callback the callback to execute, the error and the created sandbox are available as arguments to the callback function
*/
function createSandbox(realm, ttl, profile, autoScheduled, startScheduler, stopScheduler,
additionalOcapiSettings, additionalWebdavSettings, callback) {
if (!realm && dwjson['realm']) {
realm = dwjson['realm'];
console.debug('Using realm id %s from dw.json at %s', dwjson['realm'], process.cwd());
}
// build the request options
var options = ocapi.getOptions('POST', API_SANDBOXES);
// prep initial ocapi settings
var ocapiSettings = SANDBOX_OCAPI_SETTINGS;
ocapiSettings[0]['client_id'] = auth.getClient();
// amend with additional settings
if (additionalOcapiSettings) {
try {
ocapiSettings = mergeAPISettings(ocapiSettings, 'resources', additionalOcapiSettings);
} catch (err) {
callback(new Error(`Invalid OCAPI settings: ${err.message}`));
return;
}
}
// prep initial webdav permissions
var webdavPermissions = SANDBOX_WEBDAV_PERMISSIONS;
webdavPermissions[0]['client_id'] = auth.getClient();
// amend with additional settingss
if (additionalWebdavSettings) {
try {
webdavPermissions = mergeAPISettings(webdavPermissions, 'permissions', additionalWebdavSettings);
} catch (err) {
callback(new Error(`Invalid WebDAV settings: ${err.message}`));
return;
}
}
// the payload
options['body'] = {
realm : realm,
settings : {
ocapi : ocapiSettings,
webdav : webdavPermissions
}
};
// the ttl, if passed
if (ttl !== null && !isNaN(ttl)) {
options['body']['ttl'] = ttl.toFixed();
}
// set auto scheduled, if passed
if (autoScheduled) {
options['body']['autoScheduled'] = true;
}
if (startScheduler) {
options['body']['startScheduler'] = startScheduler;
}
if (stopScheduler) {
options['body']['stopScheduler'] = stopScheduler;
}
// the profile, if passed
if (profile !== null) {
if (SANDBOX_RESOURCE_PROFILES.indexOf(profile) === -1) {
callback(new Error(`Invalid resource profile '${profile}', use one of:
${SANDBOX_RESOURCE_PROFILES.join(', ')}`));
return;
}
options['body']['resourceProfile'] = profile;
}
ocapi.retryableCall('POST', options, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Creating sandbox for realm %s failed: %s', realm,
formatError(res.body.error, res.statusCode)));
} else if (err) {
errback = new Error(util.format('Creating sandbox for realm %s failed: %s', realm, err));
}
}
callback(errback, res.body['data']);
});
}
/**
* Retrieves details of a single sandboxes by id.
*
* @param {String} id the sandbox to get details for
* @param {String} topic the topic to retrieve details about
* @param {Function} callback the callback to execute, the error and the sandbox details are available as arguments to the callback function
*/
function getSandbox(id, topic, callback) {
// build the request options
var extension = '';
if ( topic !== null ) {
extension = '/' + topic
}
// do the request
ocapi.retryableCall('GET', API_SANDBOXES + '/' + id + extension, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Retrieving details for sandbox failed: %s',
formatError(res.body.error, res.statusCode)));
} else if (err) {
errback = new Error(util.format('Retrieving details for sandbox failed: %s', err));
}
}
callback(errback, res.body['data']);
});
}
/**
* Update sandbox details
*
* @param {String} id the sandbox update
* @param {Number} ttl the ttl to update (value will not overwrite the existing ttl, but added to the ttl = prolonged)
* @param {Boolean} autoScheduled sets the sandbox as auto scheduled
* @param {String} startScheduler start schedule for the sandbox
* @param {String} stopScheduler stop schedule for the sandbox
* @param {Function} callback the callback to execute, the error and the sandbox details are available as arguments to the callback function
*/
function updateSandbox(id, ttl, autoScheduled, startScheduler, stopScheduler, callback) {
// build the request options
var options = ocapi.getOptions('PATCH', API_SANDBOXES + '/' + id);
options['body'] = {};
// the ttl, if passed
if (ttl !== null && !isNaN(ttl)) {
options['body']['ttl'] = ttl.toFixed();
}
// sets auto scheduled
if ( autoScheduled !== null ) {
options['body']['autoScheduled'] = autoScheduled;
}
//We need to set explicit "null" to remove the existing schedule
if (startScheduler) {
if ( startScheduler === "null" ) {
options['body']['startScheduler'] = null;
} else {
options['body']['startScheduler'] = startScheduler;
}
}
if (stopScheduler) {
if ( stopScheduler === "null" ) {
options['body']['stopScheduler'] = null;
} else {
options['body']['stopScheduler'] = stopScheduler;
}
}
ocapi.retryableCall('PATCH', options, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Updating sandbox failed: %s',
formatError(res.body.error, res.statusCode)));
} else if (err) {
errback = new Error(util.format('Updating sandbox failed: %s', err));
}
}
callback(errback, res.body['data']);
});
}
/**
* Delete a sandbox by id
*
* @param {String} id the id of the sandbox to delete
* @param {Function} callback the callback to execute, the error and the result details are available as arguments to the callback function
*/
function deleteSandbox(id, callback) {
ocapi.retryableCall('DELETE', API_SANDBOXES + '/' + id, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Removing sandbox failed: %s',
formatError(res.body.error, res.statusCode)));
} else if (err) {
errback = new Error(util.format('Removing sandbox failed: %s', err));
}
}
callback(errback, res.body);
});
}
/**
* Trigger the given operation on a sandbox.
*
* @param {String} id the id of the sandbox to trigger the operation on
* @param {String} operation the operation to trigger (one of: start, stop, restart, reset)
* @param {Function} callback the callback to execute, the error and the result details are available as arguments to the callback function
*/
function triggerOperation(id, operation, callback) {
var options = ocapi.getOptions('POST', API_SANDBOXES + '/' + id + '/operations');
// the payload
options['body'] = {
operation : operation
};
ocapi.retryableCall('POST', options, function(err, res) {
var errback = captureCommonErrors(err, res);
if ( !errback ) {
if (res.statusCode >= 400) {
errback = new Error(util.format('Operation failed: %s', formatError(res.body.error, res.statusCode)));
} else if (err) {
errback = new Error(util.format('Operation failed: %s', err));
}
}
callback(errback, res.body);
});
}
/**
* Calls the given alias registration link in the browser after printing the inbound cluster IPs.
*
* @param {String} realm realm to lookup ips for
* @param {String} link registration link
* @param {String} host host name
*/
function doCookieRegistration(realm, link, host) {
if (!link) {
console.warn("No registration link provided.");
return
}
printInboundIPs(realm);
(async() => {
await askQuestion('Please point the domain (' + host + ') in your etc/hosts to one of the inbound IP ' +
'addresses and set the alias in your instance\'s site alias configuration (in Business Manager at: ' +
'Merchant Tools > SEO > Aliases). Press <Enter> when ready:');
open(link)
})();
}
/**
* Register a hostname alias for a sandbox.
*
* @param sbxID ID of the sandbox to create alias for
* @param alias name of the alias to create
* @param unique true, if the alias is unique across aliases, false by default
* @param requestLetsncrypt true, if need to request for Letsencrypt certificate, false by default
* @param {Function} callback the callback to execute, the error and the created alias are available as arguments to the callback function
*/
function registerForSandbox(sbxID, alias, unique, requestLetsncrypt, callback) {
// the payload
var options = ocapi.getOptions('POST', API_SANDBOXES + '/' + sbxID + '/aliases');
options['body'] = {name: alias, unique: !!unique, requestLetsEncryptCertificate : !!requestLetsncrypt};
ocapi.retryableCall('POST', options, function (err, res) {
if (err) {
callback(new Error(util.format('Creating sandbox alias failed: %s', err)));
} else if (res.statusCode >= 400) {
callback(new Error(util.format('Creating sandbox alias failed: %s', res.body.error.message)));
} else {
callback(undefined, res.body.data);
}
});
}
/**
* Read a hostname alias for a sandbox.
*
* @param sbxID ID of the sandbox to read alias for
* @param aliasID ID of the alias to read
* @param {Function} callback the callback to execute, the error and the created alias are available as arguments to the callback function
*/
function readAliasConfig(sbxID, aliasID, callback) {
ocapi.retryableCall('GET', API_SANDBOXES + '/' + sbxID + '/aliases/' + aliasID, function (err, res) {
if (err) {
callback(new Error(util.format('Reading sandbox alias failed: %s', err)), []);
} else if (res.statusCode >= 400) {
callback(new Error(util.format('Reading sandbox alias failed: %s', res.body.error.message)));
} else {
callback(undefined, res.body.data);
}
});
}
/**
* List hostname aliases for a sandbox.
*
* @param sbxID ID of the realm to list aliases for
* @param {Function} callback the callback to execute, the error and the list of aliases are available as arguments to the callback function
*/
function listForSandbox(sbxID, callback) {
ocapi.retryableCall('GET', API_SANDBOXES + '/' + sbxID + '/aliases', function (err, res) {
if (err) {
callback(new Error(util.format('Getting sandbox aliases failed: %s', err)), []);
} else if (res.statusCode >= 400) {
callback(new Error(util.format('Getting sandbox aliases failed: %s', res.body.error.message)));
} else {
callback(undefined, res.body.data);
}
});
}
/**
* Delete a hostname alias for a sandbox. Successful if the alias is not existent after this method call (will ignore
* already non-existing alias).
*
* @param sbxID ID of the realm to delete alias for
* @param aliasID ID of the CNAME alias to delete
* @param {Function} callback the callback to execute, the error is available as argument to the callback function
*/
function unregisterForSandbox(sbxID, aliasID, callback) {
ocapi.retryableCall('DELETE', API_SANDBOXES + '/' + sbxID + '/aliases/' + aliasID, function (err, res) {
if (res.statusCode === 404) {
callback(undefined);
} else if (err) {
callback(new Error(util.format('Deleting sandbox alias failed: %s', err)));
} else if (res.statusCode >= 400) {
if (res.body && res.body.error) {
callback(new Error(util.format('Deleting sandbox alias failed: %s', res.body.error.message)));
} else {
callback(new Error('Invalid alias id'));
}
} else {
callback(undefined);
}
});
}
/**
* Runs a callback function for a sandbox, which is defined by a specification object. This specification has to
* either contain the sandbox UUID as field 'id' or it's 'realm' and 'instance'. The callback function then gets
* the sandbox as a parameter.
* NOTE that the callback function is NOT called, if there was no sandbox found.
*
* @param spec specification object with sandbox ID or tenant information
* @param asJson true, for json logging enabled
* @param callback callback function which gets the sandbox as parameter
*/
function runForSandbox(spec, asJson, callback) {
if ( !( spec['id'] || ( spec['realm'] && spec['instance'] ) ) ) {
console.error('Provide either an id or a realm and an instance of the sandbox.');
return;
}
lookupSandbox(spec, function(err, foundSandbox) {
if (err) {
if (asJson) {
console.json({error: err.message});
} else {
console.error(err.message);
}
return;
}
callback(foundSandbox);
});
}
function formatError(error, statusCode) {
return error ? error.message : "unknown error (code " + statusCode + ")";
}
module.exports.cli = {
realm : {
/**
* Lists realms eligible to manage sandboxes for.
*
* @param {String} realm the realm id or null if all realms should be returned (optional)
* @param {String} topic topic to retrieve details for (optional)
* @param {Boolean} asJson optional flag to force output in json, false by default
* @param {String} sortBy optional field to sort the list by
*/
list : function(realm, topic, asJson, sortBy) {
// get details of a single realm if realm id was passed
if ( typeof(realm) !== 'undefined' && realm !== null ) {
getRealm(realm, topic, function (err, realm) {
if (err) {
if (asJson) {
console.json({error: err.message});
} else {
console.error(err.message);
}
return;
}
if (asJson) {
console.json(realm);
return;
}
console.prettyPrint(realm);
});
return;
}
// get all realms
getRealms(function(err, list) {
if (err) {
console.error(err.message);
return;
}
if (sortBy) {
list = require('./json').sort(list, sortBy);
}
if (asJson) {
console.json(list);
return;
}
if (list.length === 0) {
console.info('No realms found');
return;
}
// table fields
var data = [['id']];
for (var i of list) {
data.push([i]);
}
console.table(data);
});
},
/**
* Update realm settings
*
* @param {String} realm realm to update
* @param {Number} maxSandboxTTL max number of hours a sandbox can live in the realm
* @param {Number} defaultSandboxTTL number of hours a sandbox lives in the realm by default
* @param {String} startScheduler start schedule for all sandboxes under this realm
* @param {String} stopScheduler stop schedule for all sandboxes under this realm
* @param {Boolean} asJson optional flag to force output in json, false by default
*/
update : function(realm, maxSandboxTTL, defaultSandboxTTL, startScheduler, stopScheduler, asJson) {
// lookup realm to update
getRealm(realm, null, function (err, realm) {
if (err) {
// error
console.error(err.message);
return;
}
// realm found, now update
updateRealm(realm.id, maxSandboxTTL, defaultSandboxTTL,