Compare commits

...

180 Commits

Author SHA1 Message Date
Leo Lamprecht
5e1b099285 12.0.0-canary.84 2018-09-11 18:34:26 +02:00
Matheus Fernandes
87b1f5a5fb Added new iad1 data center (#1575) 2018-09-11 18:34:08 +02:00
Leo Lamprecht
ae8cbdce0a 12.0.0-canary.83 2018-09-10 19:09:42 +02:00
Cygnusfear
95de48c03f Changed error to log to avoid breaking automatic deployments (#1572)
throwing an error re-opened this issue:
https://github.com/zeit/now-cli/issues/1044

breaks automation when no deployments are found or deployment name is changed
2018-09-10 19:09:36 +02:00
Nathan Rajlich
5ea4fd9a5e Add now whoami to --help (#1573) 2018-09-10 19:06:19 +02:00
Leo Lamprecht
1f3d9e7e0f 12.0.0-canary.82 2018-09-06 21:00:04 +02:00
Jaga Santagostino
05e4528b1c Add changelog link when new version is available (#1530)
* add changelog link if new version is available

* Linked directly
2018-09-06 20:59:24 +02:00
Jarmo Isotalo
40789776bc Prefer URL over deployment ID as the ID not shown in now ls (#1554) 2018-09-06 20:59:19 +02:00
Zeke Sikelianos
75759edd26 Use consistent wording in usage (#1541) 2018-09-06 20:39:53 +02:00
Nathan Rajlich
2a5dc1dba6 Update @zeit/dockerignore to v0.0.3 (#1560)
* Update `@zeit/dockerignore` to v0.0.2

To fix https://github.com/zeit/now-cli/issues/1441.

* Update `@zeit/dockerignore` to v0.0.3
2018-09-06 20:39:09 +02:00
Javi Velasco
0cbf50b7ff 12.0.0-canary.81 2018-09-05 18:00:07 +02:00
Javi Velasco
a3f5b805bb Restructure certs command (#1567)
* Restructure add command

* Remove cert start and finish

* Rename certs add to issue

* Better errors and add cant-solve-challenge

* Show err.sh for dns configuration errors generating certs

* Add err.sh to solve challenges manually

* Add deprecation message to certs add

* Improve grammar

* Minor fixes
2018-09-05 17:58:22 +02:00
Javi Velasco
c337877501 12.0.0-canary.80 2018-09-02 23:24:48 +02:00
Javi Velasco
a6e9dd850a Allow to generate certs solving challenges manually (#1566)
* Move getDomainNameservers function

* Don't show an error as unexpected if there is a code in the error payload

* Add start and finish cert order functions

* Integrate add wildcard cert for external domains

* Use a different endpoint to get a cert by id

* Add new command to download a certificate

* If there are no pending challenges, try to generate the cert right away

* Remove cert download

* Move DNS table options to an object

* Bugfix: cancel spinner message when finish order fails

* Restore add to work only with cert add

* Refactor obtaining cns

* Add start and finish order commands
2018-09-02 23:24:04 +02:00
Leo Lamprecht
a14d4057de 12.0.0-canary.79 2018-09-01 12:18:06 +02:00
Olli Vanhoja
210240ce66 Fix scaling v1 deployments to all DCs (#1556)
```
> Error! An unexpected error occurred in scale: Error: This region (gru1) only accepts Serverless Docker Deployments (400)
```

This is happening because `now-cli` is validating DC names and
expanding "all" locally. However not all DCs have same features but
the client can't be aware of that. Instead of making the client
aware of the differing capabilities of the available DCs we should
pass "all" as a special DC selector, and let the backend handle
scaling properly.
2018-08-28 18:51:52 -07:00
timothyis
d21f7fe75c 12.0.0-canary.78 2018-08-25 15:52:37 +01:00
Timothy
54805cd2a0 Add gru1 (#1545)
* Add gru1

* Add gru to errors

* Hard code sfo,bru scale

* Add --regions argument

* Push with console.log

* Scale to `bru`

* Scale down in bru only
2018-08-25 15:52:16 +01:00
Javi Velasco
c7da2e732a 12.0.0-canary.77 2018-08-24 02:41:01 +02:00
Javi Velasco
4b4beaa892 Pass domain to verify insted of alias when verifying on alias (#1548) 2018-08-24 02:40:35 +02:00
Javi Velasco
e13596acae 12.0.0-canary.76 2018-08-20 20:10:08 +02:00
Javi Velasco
8eb065181f Return scaling validation errors on alias (#1538)
* Return scaling errors on alias

* Join pattern match error in one expression
2018-08-20 19:52:29 +02:00
Javi Velasco
1d97219fef 12.0.0-canary.75 2018-08-20 05:13:55 +02:00
Javi Velasco
d312f94825 Fix updating scale (#1536) 2018-08-20 04:46:55 +02:00
Javi Velasco
478a6f7369 12.0.0-canary.74 2018-08-19 18:22:22 +02:00
Javi Velasco
e309123296 Try to verify against domain name instead of alias (#1534) 2018-08-19 18:21:46 +02:00
Leo Lamprecht
777646cb0e 12.0.0-canary.73 2018-08-13 19:23:27 +02:00
Arunoda Susiripala
39e23f7144 Remove @zeit/schemas (#1507)
We no longer use it inside this repository.
2018-08-13 18:52:58 +02:00
Leo Lamprecht
b68b5c0ea3 12.0.0-canary.72 2018-08-09 12:23:10 +02:00
Sean
46dfeb8ca9 Less cryptic invalid alias message (#1506)
* Less cryptic invalid alias message

* URL -> hostname
2018-08-09 12:23:05 +02:00
Leo Lamprecht
6771ad43af Point Spectrum badge to correct location (#1516) 2018-08-09 12:21:35 +02:00
Luciano Pellacani Franca
1881b9c6cb fixing typo (#1509) 2018-08-07 20:26:05 +01:00
Pranay Prakash
dae6b7a980 12.0.0-canary.71 2018-08-04 03:08:37 +00:00
Pranay Prakash
aed6209f6a Bugfixes and improvements for deployment READY status detection (#1501)
* Have returnify use generators and reinstantiate them upon error

* check for status not_ready instead of 412

* poll every 5s instead of 1s for ready state notification

* poll every 2s for scale verification
2018-08-03 19:31:23 -07:00
Leo Lamprecht
b44a590aa0 12.0.0-canary.70 2018-08-03 08:16:35 +02:00
Olli Vanhoja
936b5fc983 Show selected session affinity when inspecting a deployment (#1489) 2018-08-03 08:12:43 +02:00
yairhaimo
473d6617f2 Include correct help command in now domains (#1492) 2018-08-03 08:11:29 +02:00
Leo Lamprecht
fcf50b5aeb 12.0.0-canary.69 2018-08-02 10:55:58 +02:00
Pranay Prakash
a3835d2e8a Retry 412 errors (#1494)
* retry on 412

* add comment for returnify and exit when the iterator ends
2018-08-02 10:55:02 +02:00
Pranay Prakash
d89481966e poll every 5s (#1496) 2018-08-01 22:56:19 -07:00
Leo Lamprecht
3148ce0f31 12.0.0-canary.68 2018-08-02 07:29:37 +02:00
Leo Lamprecht
f65363856a Better error message for rate limiting deployments (#1495)
* Better error message for rate limiting deployments

* Fixed typo

* Show time until reset

* Removed logging
2018-08-02 07:28:31 +02:00
Leo Lamprecht
e205b57352 12.0.0-canary.67 2018-08-01 13:49:54 +02:00
Aria Malkani
f44a1dbf21 Checks if it is an npm deployment before getting the aliased name (#1490)
* checks it is a npm deployment before reading package.json

* checks if you have a config.type first

* checks if you have a config.type first

* fixed logic for config.type
2018-08-01 13:49:27 +02:00
Leo Lamprecht
b7fe2f606c 12.0.0-canary.66 2018-07-31 10:27:36 +02:00
Leo Lamprecht
6e81c1795c Bumped configuration schemas to the latest version (#1488) 2018-07-31 10:27:22 +02:00
Leo Lamprecht
141d8e4467 12.0.0-canary.65 2018-07-30 13:35:55 +02:00
Robin Millette
edbfeab75f Updated link about authentication file (#1483)
* Fix link to config help

* Update get-default-auth-cfg.js
2018-07-30 13:35:34 +02:00
Robin Millette
e48ea10ebb Updated link about global configuration file (#1482)
* Fix link to config help

* Update get-default-cfg.js
2018-07-30 13:34:52 +02:00
Leo Lamprecht
8565af5526 12.0.0-canary.64 2018-07-30 13:23:30 +02:00
Leo Lamprecht
511d3bae8f Added schema for static header configuration (#1486)
* Added schema for static header configuration

* Bumped yet again
2018-07-30 13:23:07 +02:00
Pranay Prakash
8df233fef1 12.0.0-canary.63 2018-07-29 22:07:06 +00:00
Pranay Prakash
4b97410cc0 Don't downscale previous slot deployment on alias (#1485)
* don't downscale

* use /now/v4/deployments/:id
2018-07-29 15:05:34 -07:00
Leo Lamprecht
aadf8097d1 12.0.0-canary.62 2018-07-24 23:48:57 +02:00
Leo Lamprecht
40d8b74b1b Fixed now billing and use new API (#1477)
* Use correct source when listing

* Use correct source when buying domains

* Replaced the rest too

* Make adding work nicely

* Make listing work for all scopes

* Don't require address

* Bumped lockfile

* Removed more useless code

* Renamed file

* Fixed weird zlib error

* Fixed output

* Added some tests
2018-07-24 23:48:00 +02:00
Leo Lamprecht
8c4d42891c 12.0.0-canary.61 2018-07-22 01:54:51 +02:00
Leo Lamprecht
3672374d23 Fixed integration tests (#1476) 2018-07-22 01:54:05 +02:00
Leo Lamprecht
9ff9d3f174 12.0.0-canary.60 2018-07-19 19:54:19 +02:00
Nathan Rajlich
86540fa7fd Add --build-env to now deploy command (#1459)
* Add `--build-env` to `now deploy` command

Build env vars are only visible during build-time, compared to
regular env vars which are only exposed during runtime.

This also removes the client-side validation of the deployment
schema, because it makes it difficult to keep the client and server
in sync, especially as new features are added. Instead `now-cli`
should be responsible for knowing how to render the server's error
message in an informative and future-proof way so that we can
update the server and even older clients would show the validation
error properly.

* Remove `only` dependency

* Add `--build-env` CLI flag integration test
2018-07-19 19:53:52 +02:00
Pranay Prakash
1538c80a7d 12.0.0-canary.59 2018-07-18 23:29:18 +00:00
Pranay Prakash
0225fcfe51 Improvements to the CLI to handle slot specific flows (#1475)
* Handle scale slots error

* generate and send a requestID when verifying instantiation

* Don't compy incompatible scale settings

* deprecate BinaryDeployment

* add err.sh link

* swap cuid for uuid
2018-07-18 16:26:07 -07:00
Leo Lamprecht
57f3861de6 12.0.0-canary.58 2018-07-17 09:50:26 +02:00
Arunoda Susiripala
04c365a251 Add more integration tests (#1466)
* Add more integration tests related to static builds and env
Here we are adding a few more integration tests for static builds and for using env vars in the build step.

* Add build-env related test case.
2018-07-17 09:47:21 +02:00
Nathan Rajlich
7dfe4690ce 12.0.0-canary.57 2018-07-17 00:10:17 -07:00
Nathan Rajlich
92bcf1b7c9 Remove shallow deployment verification (#1474)
Remove shallow deployment verification
2018-07-17 00:08:55 -07:00
Igor Klopov
8a15d5c65a retry createDeploy if files are missing. addresses #1463 (#1465) 2018-07-13 18:26:12 +03:00
Leo Lamprecht
f64374225d 12.0.0-canary.56 2018-07-11 15:37:44 +02:00
Javi Velasco
7537eac6a7 Control can't solve challenge errors (#1458) 2018-07-11 15:37:22 +02:00
Leo Lamprecht
e2880d2434 12.0.0-canary.55 2018-07-10 19:28:01 +02:00
Javi Velasco
8296de16ef Check domain after adding (#1455)
* Check domain info after adding it

* Add message spiner to show while adding a domain
2018-07-10 19:27:42 +02:00
Leo Lamprecht
5cff5e9dfd 12.0.0-canary.54 2018-07-10 12:16:13 +02:00
Javi Velasco
cb07a748c2 Refactor DNS and update domains with CDN improvements (#1438)
* Do not allow adding domains with subdomains

* Do not ask for confirmation when the domain exists

* Improve message when the domain is under a different account

* Fix flow errors

* Revamp domains add command

* Remove setting dns records when setting up the domain

* Refactor DNS commands

* Hide fields in system dns records and show creator

* Better formatting for dns ls

* Remove exhaustive check of dns record type

* Remove domain ids from responses in domain commands

* Change all `domains` API references to use `v3`

* Update to domains API v3

* Remove NeedUpgrade error and use CDNNeedsUpgrade where it proceeds

* Update copies when adding domains

* Remove extra blank line

* Fix flow errors
2018-07-10 12:13:45 +02:00
Nathan Rajlich
c7b985bdc6 Remove legacy atlas logic (#1451)
* Remove legacy `atlas` logic

No longer used for anything.

* Remove another `atlas`
2018-07-10 11:43:00 +02:00
Javi Velasco
bf32ca0e4a Dont try to generate always wildcard certs when aliasing (#1445)
* Change params order in createAlias

* Make setupDomain return domainInfo

* Do not try to get a wildcard cert for alias when domain is external

* Update setup-domain.js
2018-07-10 11:42:37 +02:00
Javi Velasco
a4e52de0e3 Add retryAfter info to rate limit errors (#1442) 2018-07-10 11:41:28 +02:00
Nathan Rajlich
e43e9b11a0 Wait for static builds to be ready and show logs (#1452)
Implicitly sets the `noVerify` option since there is nothing to verify.
2018-07-10 01:11:10 -07:00
Javi Velasco
847b9e97c4 12.0.0-canary.53 2018-07-09 13:05:01 +02:00
Javi Velasco
baad689286 Fix typo when checking NS during setup domain (#1450) 2018-07-09 12:53:11 +02:00
Arunoda Susiripala
5e7afc4385 12.0.0-canary.52 2018-07-09 09:40:37 +05:30
Matheus Fernandes
d724b7a631 Revert "Upgrade to webpack 4 and latest Babel" (#1448)
* Revert "12.0.0-canary.51"

This reverts commit 5e17fe5ad6.

* Revert "Update `@zeit/schemas` to v1.6.0"

This reverts commit b216adadc0.

* Revert "Upload the Dockerfile if it's a static deployment. (#1437)"

This reverts commit 5078c95667.

* Revert "Upgrade to webpack 4 and latest Babel (#1436)"

This reverts commit 7612d77647.
2018-07-08 20:48:55 -07:00
Nathan Rajlich
5e17fe5ad6 12.0.0-canary.51 2018-07-05 17:52:14 -07:00
Nathan Rajlich
b216adadc0 Update @zeit/schemas to v1.6.0 2018-07-05 17:39:38 -07:00
Arunoda Susiripala
5078c95667 Upload the Dockerfile if it's a static deployment. (#1437)
* Upload the Dockerfile if it's a static deployment.
This allows us to build the Dockerfile and upload static assets

* Allow to upload package.json as well for static deployments.
2018-07-05 22:32:19 +05:30
Javi Velasco
7612d77647 Upgrade to webpack 4 and latest Babel (#1436)
* Upgrade to webpack 4 and last latest

* Fix building commands

* Do not call yarn build when yarn link

* Use resolved paths in webpack config
2018-07-04 17:51:16 +01:00
Leo Lamprecht
0d76041c10 12.0.0-canary.50 2018-07-03 21:20:17 +02:00
Javi Velasco
59be596d24 Allow to deploy and alias when non essential APIs are down (#1435)
* Try to purchase a domain only when there is no other choice

* Allow eventsStream to fail during deployment

* Allow to verify instantiation without events API
2018-07-03 12:18:54 -07:00
Leo Lamprecht
63e51a3c98 12.0.0-canary.49 2018-07-02 11:31:15 +02:00
Leo Lamprecht
7f3128b3e5 Fixed the tests (#1430)
* Fixed the tests

* Wrapped up
2018-07-02 11:30:44 +02:00
Leo Lamprecht
c235813ae7 12.0.0-canary.48 2018-06-22 21:50:33 +02:00
Javi Velasco
3ee18e7051 Small fixes (#1418)
* Remove FlowFixMe in deploy command

* Show success message when creating a cert for alias based on response cns
2018-06-21 23:04:40 -07:00
Leo Lamprecht
14fc5d8796 12.0.0-canary.47 2018-06-21 12:53:44 +02:00
Leo Lamprecht
9fb0077385 Improved error message for schema validation (#1416)
* Bumped schema

* Improved error message for schema validation
2018-06-21 12:53:12 +02:00
Javi Velasco
2e9c7265b6 12.0.0-canary.46 2018-06-19 17:13:48 +02:00
Javi Velasco
d9e77b784a Allow to enable and disable CDN for domains and refactor domains (#1413)
* Move domains command to its own folder

* Refactor domains commands

* Add cdn to domain ls

* Add function to patch domains

* Support toggling cdnEnabled

* Better messages

* Add new cdn options to help command in domains
2018-06-19 17:12:55 +02:00
Javi Velasco
b5b296ad7f 12.0.0-canary.45 2018-06-15 17:35:53 +02:00
Javi Velasco
acdfde5aa2 Deeply nested wildcard certs (#1407)
* Better error when we can't verify a domain during alias

* Remove preferDNS option from CLI and allow wildcard certs for deeply nested
2018-06-15 12:04:10 +02:00
Pranay Prakash
0eddfbd28c Improve the progress bar during upload (#1406)
* use push instead of read

* don't auto-clear

* single progress bar that hangs till last chink

* print symmary beofre link

* print smmary faster

* print more discreet timestamps

* fix tests

* fix flow error
2018-06-13 23:21:39 -04:00
Javi Velasco
037f0610bc 12.0.0-canary.44 2018-06-13 15:39:27 +02:00
Javi Velasco
dd45f8f2ab Request normal cert for deeply nested alias (#1404)
* Request normal cert for deeply nested alias

* When is a deeply nested domain request a normal cert
2018-06-13 15:38:47 +02:00
Leo Lamprecht
d454c84f61 12.0.0-canary.43 2018-06-11 10:40:32 +02:00
Felix Yan
a4c98e07a5 Fix a typo in unique-strings.js (#1400) 2018-06-08 12:20:57 -07:00
Igor Klopov
85715630bd test-integration: test deployment logs output (#1398) 2018-06-08 04:31:36 -07:00
Nathan Rajlich
45d8d4a84f 12.0.0-canary.42 2018-06-07 14:48:16 -07:00
Nathan Rajlich
690882c97a Fix shallow verify (#1397) 2018-06-07 14:45:27 -07:00
Leo Lamprecht
e03836e4a1 12.0.0-canary.41 2018-06-07 11:00:32 +02:00
Leo Lamprecht
d4ec54135a Updated configuration schema (#1394) 2018-06-07 10:59:51 +02:00
Javi Velasco
b29785d851 Ensure there is scale rules before trying to apply (#1389) 2018-06-07 10:52:24 +02:00
Javi Velasco
148870d706 Fixed unexpected error while aliasing (#1387) 2018-06-07 10:50:49 +02:00
Nathan Rajlich
ed1f7e335d Add shallow deployment verification (#1367)
* Add shallow deployment verification

* Add Flow type for `now.retry()`

* Add 3 fetch retries to shallow verify

* Use generics

* Collapse

* Fix

* Add `X-Now-Shallow` verification
2018-06-06 13:08:19 -07:00
Matheus Fernandes
eaf4695194 12.0.0-canary.40 2018-06-01 15:59:43 -07:00
Igor Klopov
c312d42302 reuse getSafeAlias function for all callers of findAliasByAliasOrId (#1385) 2018-06-02 01:27:47 +03:00
Leo Lamprecht
a8a2a6c066 12.0.0-canary.39 2018-05-29 20:07:19 +02:00
Leo Lamprecht
8b3512cb07 Bumped configuration schemas to the latest version (#1379) 2018-05-29 20:06:57 +02:00
Leo Lamprecht
945facdd2c 12.0.0-canary.38 2018-05-29 18:36:15 +02:00
Javi Velasco
8676ed4cff Support for Custom Deployment Suffix (#1378)
* Allow to fallback to passed body parsing a response error

* Extract domain purchase from setup-domain

* Move getCertRequestSettings

* Add create method to Now interface

* Extract print dns table and zeit world table functions

* Add support to generate certificates during deploy

* Point to v5 in deploy endpoint

* Add feedback messages when creating a cert during deployment

* Remove hardcoded references to now.sh

* Dont bump create endpoint version

* Support empty reponses in fetch
2018-05-29 07:58:24 -07:00
Leo Lamprecht
e4d4afa840 12.0.0-canary.37 2018-05-28 13:16:13 +02:00
Leo Lamprecht
81cf286ea4 Bumped configuration schemas to the latest version (#1374) 2018-05-28 13:15:58 +02:00
Leo Lamprecht
e834625728 12.0.0-canary.36 2018-05-28 09:07:30 +02:00
Pranay Prakash
a7c22eb08c add @zeit/dockerignore (#1373) 2018-05-26 09:51:56 -07:00
Leo Lamprecht
837c358371 12.0.0-canary.35 2018-05-25 20:58:30 +02:00
Leo Lamprecht
9743db27e7 Updated configuration schemas to the latest version (#1372) 2018-05-25 11:54:27 -07:00
Tim Neutkens
ce725143e6 12.0.0-canary.34 2018-05-22 17:32:26 +02:00
Tim Neutkens
677805c33a Make sure subdomain is correctly typed / checked (#1364)
Fixes an error introduced in #1344 when running `now alias` on a naked domain like `example.com`.
2018-05-22 17:31:25 +02:00
Pranay Prakash
3d3f1fe39b Make the progress bar for uploads consider bytes, not number of files (#1335)
* use got and propogate uploadProgress

* Accept comma separated cns (#1336)

* use got and propogate uploadProgress

* hook into stream.read

* revert `got` stuff

* remove stray whitespace
2018-05-21 16:40:24 -07:00
Pranay Prakash
123c68ad2b fix clipboard arg parsing (#1358) 2018-05-21 16:39:40 -07:00
Timothy
86861c58af Fix error message via #1350 (#1361) 2018-05-21 16:39:22 -07:00
Pranay Prakash
d4ddb6b3f9 Strip quotes from Dockerfile labels (#1351)
* Strip quotes from Dockerfile labels

* remove console.log

* add test

* issue normal cert for nested subdomain (#1344)

* Prefer HTTP challenge for regular certs

* 12.0.0-canary.31

* Update non-existing team test

* 12.0.0-canary.32

* Bumped `update-check` to the latest version (#1354)

* 12.0.0-canary.33

* Strip quotes from Dockerfile labels

* remove console.log

* add test
2018-05-21 16:39:06 -07:00
Leo Lamprecht
0fc7de40d4 12.0.0-canary.33 2018-05-18 14:06:57 +02:00
Leo Lamprecht
3cb4a9c8dd Bumped update-check to the latest version (#1354) 2018-05-18 14:06:18 +02:00
Javi Velasco
d681289457 12.0.0-canary.32 2018-05-18 03:24:28 +02:00
Javi Velasco
bc1c3c3f5b Update non-existing team test 2018-05-18 03:24:08 +02:00
Javi Velasco
5ae4287c0f 12.0.0-canary.31 2018-05-18 03:02:00 +02:00
Javi Velasco
cde9f56886 Prefer HTTP challenge for regular certs 2018-05-18 03:01:26 +02:00
Pranay Prakash
2b6b006bbd issue normal cert for nested subdomain (#1344) 2018-05-18 02:32:12 +02:00
Leo Lamprecht
54e1bcafc0 12.0.0-canary.30 2018-05-16 21:28:21 +02:00
Leo Lamprecht
7e98d0a22b Store config on platform & load schema from package (#1349)
* Load schema from package

* Send config to deployment endpoint

* Upgraded @zeit/schemas to the latest version

* Removed type for now

* Added config correctly
2018-05-16 21:26:41 +02:00
Tim Neutkens
c27b4a6aaf 12.0.0-canary.29 2018-05-16 18:08:53 +02:00
Javi Velasco
8f17ffd817 Fix table import (#1348) 2018-05-16 18:07:58 +02:00
Leo Lamprecht
0a7d688d32 12.0.0-canary.28 2018-05-15 21:58:07 +02:00
Leo Lamprecht
8cb2fe1284 Account for npm being down (#1346)
* Corrected license file name

* Corrected readme name

* Added editorconfig

* Account for npm being down

* Print full error

* Only show full error while debugging
2018-05-15 21:57:40 +02:00
Leo Lamprecht
e6e375232e 12.0.0-canary.27 2018-05-15 10:07:03 +02:00
Guillermo Rauch
08c4ab8a0c Updated error message about verification timeout 2018-05-15 10:06:37 +02:00
Leo Lamprecht
f86647fc26 12.0.0-canary.26 2018-05-15 10:01:29 +02:00
Javi Velasco
f310f6a86f Accept comma separated cns (#1336) 2018-05-08 21:36:53 +02:00
Javi Velasco
58c6acd265 12.0.0-canary.25 2018-04-27 22:02:10 -07:00
Igor Klopov
c7d97e3866 ignore keep-alive packets for now logs -f (#1331) 2018-04-27 21:45:47 -07:00
Javi Velasco
0890144c61 12.0.0-canary.24 2018-04-26 11:13:14 -07:00
Javi Velasco
69a7d91b57 Safe deployment polling and other minor fixes (#1330)
* Initialize polling function from args

* Remove rule of having a mandatory dest field in path alias rules

* Wait 5 seconds after getting the deployment ready

* Ignore errors coming from events stream
2018-04-26 11:07:15 -07:00
Javi Velasco
a9016c88f6 Refactor scale and remove old alias and scale (#1321)
* Migrate to arg@2.0.0

* Refactor scale command

* Move alias.js to alias/index.js

* Move alias set to its own file

* Move alias ls to its own file

* Move alias rm to its own file

* Remove old alias and scale files

* Update alias integration test

* Fix scaling to 0

* Read scale params from now.json on deploy
2018-04-24 19:16:25 -07:00
Igor Klopov
e8990742cf handle keep-alive event in inspect command (#1326) 2018-04-22 20:16:02 -07:00
Javi Velasco
f51400a3a1 12.0.0-canary.23 2018-04-18 11:27:15 -07:00
Javi Velasco
e763ee5301 Handle ENOENT error when deploying unexistent path (#1320)
* Handle ENOENT error when deploying unexistent path

* Show success on dedupped deployments
2018-04-18 11:26:47 -07:00
Javi Velasco
fd978699e8 Migrate to arg@2.0.0 (#1316) 2018-04-17 19:28:45 -07:00
Javi Velasco
bbd9585829 Fix double ambiguous deploy prompt (#1318) 2018-04-17 19:20:27 -07:00
Javi Velasco
54cf1ebb31 12.0.0-canary.22 2018-04-17 18:07:07 -07:00
Javi Velasco
6fc77c6cfa Fix 0 min scale (#1317) 2018-04-17 18:05:04 -07:00
Javi Velasco
f8372e3bb9 12.0.0-canary.21 2018-04-17 10:49:33 -07:00
Naoyuki Kanezawa
6728be7b1a improve image upload (#1293) 2018-04-17 10:37:17 -07:00
Javi Velasco
65e1a1e731 Fix number of instances verification (#1315) 2018-04-17 10:32:32 -07:00
Javi Velasco
cd9478e853 12.0.0-canary.20 2018-04-16 17:46:47 -07:00
Javi Velasco
b9364ed4fc Improve instance verification for deploy (#1313)
* Refactor verify instances for deploy

* Don't rely on return

* Use new verify instances in alias

* Use new verify instances in alias
2018-04-16 17:45:19 -07:00
Leo Lamprecht
2715e8e9d8 12.0.0-canary.19 2018-04-13 23:23:44 -07:00
Javi Velasco
f987c93cf0 Replace combine-async-generators (#1305)
* Replace combine-asyng-generators

* Ignore empty objects from events stream

* Update type for alias event
2018-04-13 21:55:06 -07:00
Javi Velasco
5c254a7151 Print response in docker test (#1304)
* Print response when failing in docker test file

* Add line

* Parse response text as JSON, print if it fails
2018-04-13 19:49:18 -07:00
Leo Lamprecht
f253e29f33 12.0.0-canary.18 2018-04-13 19:05:49 -07:00
Leo Lamprecht
f37fa13eab Added missing dependencies (#1303) 2018-04-13 19:00:17 -07:00
Javi Velasco
64765b393a Refactor deploy events printing (#1302)
* Move deploy command to its own directory

* Do not show success on downscale message

* Move getDeploymentByIdOrHost to /util/deploy

* Remove unneeded parameter in getAppName

* Better types for copyToClipboard

* Update to babel 7

* Add generator utility functions

* Add function to get deployment events

* Finish  getDeploymentEvents after getting one state-change event

* Refactor deploy events and reduce verification timeout

* Reduce verification timeout for scale and alias

* Use output.log for success message in scale

* Fix integration tests
2018-04-13 18:33:27 -07:00
Leo Lamprecht
54c84b4ce0 12.0.0-canary.17 2018-04-11 12:23:06 -07:00
Javi Velasco
146bcba794 Remove old certs command (#1295) 2018-04-11 12:22:49 -07:00
Leo Lamprecht
d3dd1b731d 12.0.0-canary.16 2018-04-11 11:43:12 -07:00
Javi Velasco
d608ee7390 Remove old alias command (#1294) 2018-04-11 11:42:38 -07:00
Leo Lamprecht
4e2e0950c7 12.0.0-canary.15 2018-04-10 17:53:51 -07:00
Leo Lamprecht
ddc7e97ab6 Fixed typo in release instructions 2018-04-10 17:53:27 -07:00
Leo Lamprecht
a21759ee42 Resolved conflicts 2018-04-10 17:53:02 -07:00
Leo Lamprecht
cc4beb94cf Merge branch 'master' into canary 2018-04-10 17:47:11 -07:00
Leo Lamprecht
0bfafa9311 12.0.0-canary.14 2018-04-10 16:38:48 -07:00
Leo Lamprecht
4eefc34629 Moved documentation for releasing out 2018-04-10 16:26:44 -07:00
Leo Lamprecht
4d3f882dc0 Make clear how a release works exactly 2018-04-10 16:08:39 -07:00
Leo Lamprecht
3a802fbb70 Improved intro and contribution section 2018-04-10 16:07:31 -07:00
Leo Lamprecht
6f00b03d24 Made sure the integration tests work as expected 2018-04-10 15:46:41 -07:00
176 changed files with 6154 additions and 7301 deletions

37
.editorconfig Normal file
View File

@@ -0,0 +1,37 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.json.example,*.gyp,*.yml,*.yaml}]
indent_style = space
indent_size = 2
[{*.py,*.asm}]
indent_style = space
[*.py]
indent_size = 4
[*.asm]
indent_size = 8
[*.md]
trim_trailing_whitespace = false
# Ideal settings - some plugins might support these.
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

View File

View File

@@ -1,6 +1,6 @@
![now](https://github.com/zeit/art/blob/a7867d60f54a41127023a8740a221921df309d24/now-cli/repo-banner.png?raw=true)
[![Build Status](https://circleci.com/gh/zeit/now-cli.svg?&style=shield)](https://circleci.com/gh/zeit/workflows/now-cli) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/now)
[![Build Status](https://circleci.com/gh/zeit/now-cli.svg?&style=shield)](https://circleci.com/gh/zeit/workflows/now-cli) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)
**Note**: The [canary](https://github.com/zeit/now-cli/tree/canary) branch is under heavy development the stable release branch is [master](https://github.com/zeit/now-cli/tree/master).
@@ -28,4 +28,4 @@ As always, you can use `yarn test` to run the tests and see if your changes have
## How to Create a Release
If you have write access to this repository, you can read more about how publish a release [here](https://github.com/zeit/zeit/blob/master/guides/now-cli-release.md).
If you have write access to this repository, you can read more about how to publish a release [here](https://github.com/zeit/zeit/blob/master/guides/now-cli-release.md).

View File

@@ -2,33 +2,33 @@
const path = require('path')
module.exports = {
target: 'node',
node: {
__dirname: false,
__filename: false,
process: false
},
entry: [
'./src/index.js'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'download.js'
},
module: {
loaders: [ {
test: /.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
plugins: [
'transform-async-to-generator',
'transform-runtime'
],
presets: [
'env'
]
}
} ]
}
target: 'node',
node: {
__dirname: false,
__filename: false,
process: false
},
entry: [
'./src/index.js'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'download.js'
},
module: {
loaders: [ {
test: /.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
plugins: [
'@babel/transform-async-to-generator',
'@babel/transform-runtime'
],
presets: [
'@babel/preset-env'
]
}
} ]
}
}

View File

@@ -0,0 +1,27 @@
# The DNS Challenge Could Not Be Solved
## Why This Error Occurred
When generating a certificate, we have to prove ownership over the domain
for the Certificate Authority (CA) that issues it. This error means that
the provider couldnt solve the requested challenges.
## How to Fix It
If your domain is pointing to ZEIT World DNS and youre getting this error,
it could be that:
- The domain was acquired recently, and it might not be ready for use yet.
- Required DNS records have not propagated yet.
When running into this, ensure that your nameservers have configuration is correct. Also, if you bought the domain recently or have made changes, please be patient,
it might take a while for these to be ready.
If your domain is _not_ pointing to ZEIT World DNS and youre getting this
error, the following methods could help:
- When solving challenges *manually*, ensure that the TXT
records required to solve the challenges exist and are propagated. You can do so by querying the nameservers with `nslookup -q=TXT _acme-challenge.domain.com` depending on the Common Names you want for your certificate.
- If you are not solving the challenges manually you must ensure that you have an
`ALIAS` and `CNAME` records in place. Ensure also that you have disabled automatic redirects to `https` and ensure all changes were propagated.

View File

@@ -16,18 +16,22 @@ and DCs have to be in *lowercase*.
- `all` (special, used to scale to all DCs, can only appear once)
- `sfo`
- `bru`
- `gru`
- `iad`
In `now-cli`, they currently are transformed to `sfo1`
and `bru1` dc identifiers before being sent to our APIs.
In `now-cli`, they currently are transformed to
DC identifiers before being sent to our APIs.
**Valid DC identifiers**:
- `sfo1`
- `bru1`
- `gru1`
- `iad1`
When passing multiple `--regions` as a CLI parameter,
make sure they're separated by a comma (`,`). For example:
```console
now --regions sfo,bru
now --regions sfo,bru,gru
```

View File

@@ -0,0 +1,23 @@
# The DNS Configuration can't be verified
## Why This Error Occurred
When generating a certificate, we have to prove ownership over the domain
for the Certificate Authority (CA) that issues it. We also run some pretests
to make sure the DNS is properly configured before submitting the request to
the CA. This error means that these pretests did not succeed.
## How to Fix It
If your domain is pointing to ZEIT World DNS and youre getting this error,
it could be that:
- The domain was acquired recently, and it might not be ready for use yet.
- Required DNS records have not propagated yet.
When running into this, ensure that your nameservers have configuration is correct. Also, if you bought the domain recently or have made changes, please be patient,
it might take a while for these to be ready.
If your domain is _not_ pointing to ZEIT World DNS and youre getting this
error, you must ensure that you have an `ALIAS` and `CNAME` records in place.
Ensure also that you have disabled automatic redirects to `https` and ensure all changes were propagated.

View File

@@ -13,7 +13,7 @@ with default scale settings.
```json
{
"regions": ["sfo", "bru"]
"regions": ["sfo", "bru", "gru", "iad"]
}
```

View File

@@ -16,17 +16,21 @@ and DCs have to be in *lowercase*.
- `all` (special, used to scale to all DCs, can only appear once)
- `sfo`
- `bru`
- `gru`
- `iad`
In `now-cli`, they currently are transformed to `sfo1`
and `bru1` dc identifiers before being sent to our APIs.
In `now-cli`, they currently are transformed to
DC identifiers before being sent to our APIs.
**Valid DC identifiers**:
- `sfo1`
- `bru1`
- `gru1`
- `iad1`
To pass multiple ones, use a comma:
```
now scale my-url-123.now.sh sfo,bru 1 5
now scale my-url-123.now.sh sfo,bru,gru 1 5
```

View File

@@ -0,0 +1,20 @@
# DNS Challenges must be solved manually
## Why This Error Occurred
When generating a certificate, we have to prove ownership over the domain
for the Certificate Authority (CA) that issues it. In the case of Wildcard Certificates,
the requested challenge consists of adding TXT DNS records so, when the domain does not
point to ZEIT World DNS, we cannot create the records to solve the challenge.
## How to Fix It
To generate a certificate solving challenges manually, you must add the given `TXT` records with
the appropriate name to your DNS. Then, after verifying that the CA can read the records,
you can rerun the issuance command.
In case you want to start issuing a certificate to get the records you have to add or to
get those records again in the console, You can run the issuance command including the
`--challenge-only` option. This way the CLI will output the challenges information and,
after adding those records, you can rerun the command without `--challenge-only` to finish
issuance.

17
errors/v2-no-min.md Normal file
View File

@@ -0,0 +1,17 @@
# No minimum scale settings on Cloud v2 deployments
#### Why This Error Occurred
An attempt was made at scaling a Cloud v2 deployment with a `min` scale
setting. This isn't supported yet.
#### Possible Ways to Fix It
Ensure your scale settings (in `now.json`, the command you're running
or from a previous deployment who's alias you're trying to overwrite) has
the `min` scale setting set to `0`. You can do this by running
```
now scale <deployment> 0 10
```

View File

@@ -0,0 +1,29 @@
# Verification Timeout
#### Why This Error Occurred
After the deployment build completed and the deployment state was set to `READY`,
instances failed to initialize properly.
The CLI attempted to verify that the scale settings of your instances matched,
but it couldn't do so within the alloted time (defaults to 2 minutes).
#### Possible Ways to Fix It
Instance verification is the process of ensuring that after
your deployment is ready, we can actually run (instantiate) your code.
If you configured [regions or scale](https://zeit.co/docs/features/scaling),
we ensure the minimums and maximums are met for the regions you enabled.
If you think your code is taking too long to instantiate, this can be due
to slow boot up times. You can supply `--no-verify` to skip verification
if you are confident your code runs properly.
If your application is not listening on a HTTP port, we might be failing to
instantiate your deployment as well. It might not be showing any errors,
but the deployment instance is effectively not routable and cannot be
verified.
If your instances are crashing before an HTTP port is exposed, verification
will fail as well. Double check your logs (e.g.: by running `now logs <url>`)

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "11.1.1",
"version": "12.0.0-canary.84",
"license": "Apache-2.0",
"description": "The command-line interface for Now",
"repository": "zeit/now-cli",
@@ -78,7 +78,7 @@
"babel": {
"presets": [
[
"env",
"@babel/preset-env",
{
"targets": {
"node": "current"
@@ -87,37 +87,34 @@
]
],
"plugins": [
"transform-flow-comments",
[
"transform-object-rest-spread",
{
"useBuiltIns": true
}
]
"@babel/plugin-transform-flow-comments",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-async-generator-functions"
]
},
"devDependencies": {
"@google/maps": "0.4.3",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-proposal-async-generator-functions": "7.0.0-beta.44",
"@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.44",
"@babel/plugin-transform-flow-comments": "7.0.0-beta.44",
"@babel/plugin-transform-runtime": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/runtime": "7.0.0-beta.44",
"@zeit/dockerignore": "0.0.3",
"@zeit/source-map-support": "0.6.2",
"ajv": "6.4.0",
"alpha-sort": "2.0.1",
"ansi-escapes": "3.0.0",
"ansi-regex": "3.0.0",
"archiver": "2.0.3",
"arg": "1.0.1",
"arg": "2.0.0",
"arr-flatten": "1.1.0",
"async-retry": "1.1.3",
"async-sema": "1.2.0",
"ava": "0.25.0",
"aws-sdk": "2.124.0",
"babel-core": "6.26.0",
"babel-eslint": "8.2.2",
"babel-loader": "7.1.4",
"babel-plugin-transform-async-to-generator": "6.24.1",
"babel-plugin-transform-flow-comments": "6.22.0",
"babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-runtime": "6.23.0",
"babel-preset-env": "1.6.1",
"babel-loader": "8.0.0-beta.2",
"bytes": "3.0.0",
"chalk": "2.3.1",
"child-process-promise": "2.2.1",
@@ -134,10 +131,10 @@
"email-prompt": "0.3.2",
"email-validator": "1.1.1",
"epipebomb": "1.0.0",
"es7-sleep": "1.0.0",
"eslint": "4.7.2",
"eslint-plugin-flowtype": "2.46.1",
"execa": "0.9.0",
"executable": "4.1.1",
"fetch-h2": "0.2.5",
"flow-babel-webpack-plugin": "1.1.1",
"fs-extra": "4.0.2",
@@ -181,7 +178,7 @@
"through2": "2.0.3",
"tmp-promise": "1.0.3",
"uid-promise": "1.0.0",
"update-check": "1.2.0",
"update-check": "1.5.0",
"webpack": "3.6.0",
"webpack-node-externals": "1.6.0",
"which-promise": "1.0.0",

View File

@@ -9,7 +9,7 @@ module.exports = async () => {
let migrated = false
const config = {
_: 'This is your Now credentials file. DON\'T SHARE! More: https://git.io/v5ECz',
_: 'This is your Now credentials file. DON\'T SHARE! More: https://goo.gl/mbf4CZ',
credentials: []
}

View File

@@ -9,7 +9,7 @@ module.exports = async () => {
let migrated = false
const config = {
_: 'This is your Now config file. See `now config help`. More: https://git.io/v5ECz'
_: 'This is your Now config file. See `now config help`. More: https://goo.gl/5aRS2s'
}
try {

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env node
//@flow
// This should be automatically included by @babel/preset-env but it's
// not being load right now. We have to remove it once it's fixed
require('core-js/modules/es7.symbol.async-iterator');
// we only enable source maps while developing, since
// they have a small performance hit. for this, we
// look for `pkg`, which is only present in the final bin
@@ -19,7 +23,6 @@ const { join } = require('path')
const debug = require('debug')('now:main')
const { existsSync } = require('fs-extra')
const mkdirp = require('mkdirp-promise')
const mri = require('mri')
const fetch = require('node-fetch')
const chalk = require('chalk')
const checkForUpdate = require('update-check')
@@ -38,6 +41,10 @@ const configFiles = require('./util/config-files')
const getUser = require('./util/get-user')
const pkg = require('./util/pkg')
import { Output } from './providers/sh/util/types'
import createOutput from './util/output'
import getArgs from './providers/sh/util/get-args'
const NOW_DIR = getNowDir()
const NOW_CONFIG_PATH = configFiles.getConfigFilePath()
const NOW_AUTH_CONFIG_PATH = configFiles.getAuthConfigFilePath()
@@ -48,34 +55,36 @@ const main = async (argv_) => {
// $FlowFixMe
const { isTTY } = process.stdout
const update = await checkForUpdate(pkg, {
interval: ms('1d'),
distTag: pkg.version.includes('canary') ? 'canary' : 'latest'
})
const argv = getArgs(argv_, {
'--version': Boolean,
'-v': '--version',
'--debug': Boolean,
'-d': '--debug'
}, { permissive: true })
const isDebugging = argv['--debug']
const output: Output = createOutput({ debug: isDebugging })
let update = null
try {
update = await checkForUpdate(pkg, {
interval: ms('1d'),
distTag: pkg.version.includes('canary') ? 'canary' : 'latest'
})
} catch (err) {
console.error(error(`Checking for updates failed${isDebugging ? ':' : ''}`))
if (isDebugging) {
console.error(err)
}
}
if (update && isTTY) {
console.log(info(`${chalk.bgRed('UPDATE AVAILABLE')} The latest version of Now CLI is ${update.latest}`))
console.log(info(`Read more about how to update here: https://zeit.co/update-cli`))
console.log(info(`Changelog: https://github.com/zeit/now-cli/releases/tag/${update.latest}`))
}
const argv = mri(argv_, {
boolean: [
'help',
'version'
],
string: [
'token',
'team',
'api'
],
alias: {
help: 'h',
version: 'v',
token: 't',
team: 'T'
}
})
// the second argument to the command can be a path
// (as in: `now path/`) or a subcommand / provider
// (as in: `now ls` or `now aws help`)
@@ -83,7 +92,7 @@ const main = async (argv_) => {
// we want to handle version or help directly only
if (!targetOrSubcommand) {
if (argv.version) {
if (argv['--version']) {
console.log(require('../package').version + `${
// $FlowFixMe
process.pkg ? '' : chalk.magenta(' (dev)')
@@ -379,8 +388,8 @@ const main = async (argv_) => {
const { sh } = ctx.config
ctx.apiUrl = 'https://api.zeit.co'
if (argv.api && typeof argv.api === 'string') {
ctx.apiUrl = argv.api
if (argv['--api'] && typeof argv['--api'] === 'string') {
ctx.apiUrl = argv['--api']
} else if (sh && sh.api) {
ctx.apiUrl = sh.api
}
@@ -401,7 +410,7 @@ const main = async (argv_) => {
if (
!authConfig.credentials.length &&
!ctx.argv.includes('-h') && !ctx.argv.includes('--help') &&
!argv.token &&
!argv['--token'] &&
subcommand !== 'login'
) {
if (isTTY) {
@@ -424,7 +433,7 @@ const main = async (argv_) => {
}
}
if (typeof argv.token === 'string' && subcommand === 'switch') {
if (typeof argv['--token'] === 'string' && subcommand === 'switch') {
console.error(error({
message: `This command doesn't work with ${param('--token')}. Please use ${param('--team')}.`,
slug: 'no-token-allowed'
@@ -433,8 +442,8 @@ const main = async (argv_) => {
return 1;
}
if (typeof argv.token === 'string') {
const {token} = argv
if (typeof argv['--token'] === 'string') {
const token = argv['--token']
if (token.length === 0) {
console.error(error({
@@ -480,9 +489,9 @@ const main = async (argv_) => {
ctx.config.sh = Object.assign(ctx.config.sh || {}, { user })
}
if (typeof argv.team === 'string' && subcommand !== 'login') {
const { team } = argv
if (typeof argv['--team'] === 'string' && subcommand !== 'login') {
const { sh } = ctx.config
const team = argv['--team']
if (team.length === 0) {
console.error(error({
@@ -554,11 +563,19 @@ const main = async (argv_) => {
try {
exitCode = await provider[subcommand](ctx);
} catch (err) {
console.error(
error(
`An unexpected error occurred in ${subcommand}: ${err.stack}`
)
)
// If there is a code we should not consider the error unexpected
// but instead show the message
if (err.code) {
output.debug(err.stack)
output.error(err.message)
return 1;
}
// Otherwise it is an unexpected error and we should show the trace
// and an unexpected error message
console.error(error(`An unexpected error occurred in ${subcommand}: ${err.stack}`))
return 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,615 +0,0 @@
// @flow
// Packages
const arg = require('arg')
const chalk = require('chalk')
const ms = require('ms')
const plural = require('pluralize')
const table = require('text-table')
// Utilities
const { handleError } = require('../../util/error')
const argCommon = require('../../util/arg-common')()
const cmd = require('../../../../util/output/cmd')
const createOutput = require('../../../../util/output')
const getContextName = require('../../util/get-context-name')
const humanizePath = require('../../../../util/humanize-path')
const logo = require('../../../../util/output/logo')
const Now = require('../../util/')
const NowAlias = require('../../util/alias')
const stamp = require('../../../../util/output/stamp')
const strlen = require('../../util/strlen')
const toHost = require('../../util/to-host')
const wait = require('../../../../util/output/wait')
import { Output } from '../../util/types'
import * as Errors from '../../util/errors'
import assignAlias from './assign-alias'
import getDeploymentForAlias from './get-deployment-for-alias'
import getRulesFromFile from './get-rules-from-file'
import getSubcommand from './get-subcommand'
import getTargetsForAlias from './get-targets-for-alias'
import promptBool from './prompt-bool'
import upsertPathAlias from './upsert-path-alias'
const help = () => {
console.log(`
${chalk.bold(`${logo} now alias`)} [options] <command>
${chalk.dim('Commands:')}
ls [app] Show all aliases (or per app name)
set <deployment> <alias> Create a new alias
rm <alias> Remove an alias using its hostname
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline(
'RULES_FILE'
)} Rules file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
-n, --no-verify Don't wait until instance count meets the previous alias constraints
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new alias to ${chalk.underline('my-api.now.sh')}
${chalk.cyan(
`$ now alias set ${chalk.underline(
'api-ownv3nc9f8.now.sh'
)} ${chalk.underline('my-api.now.sh')}`
)}
Custom domains work as alias targets
${chalk.cyan(
`$ now alias set ${chalk.underline(
'api-ownv3nc9f8.now.sh'
)} ${chalk.underline('my-api.com')}`
)}
${chalk.dim('')} The subcommand ${chalk.dim(
'`set`'
)} is the default and can be skipped.
${chalk.dim('')} ${chalk.dim(
'Protocols'
)} in the URLs are unneeded and ignored.
${chalk.gray('')} Add and modify path based aliases for ${chalk.underline(
'zeit.ninja'
)}
${chalk.cyan(
`$ now alias ${chalk.underline('zeit.ninja')} -r ${chalk.underline(
'rules.json'
)}`
)}
Export effective routing rules
${chalk.cyan(
`$ now alias ls aliasId --json > ${chalk.underline('rules.json')}`
)}
`)
}
const COMMAND_CONFIG = {
default: 'set',
ls: ['ls', 'list'],
rm: ['rm', 'remove'],
set: ['set'],
}
module.exports = async function main(ctx: any): Promise<number> {
let argv
try {
argv = arg(ctx.argv.slice(2), {
...argCommon,
'--yes': Boolean,
'-y': '--yes',
'--json': Boolean,
'--rules': String,
'--no-verify': Boolean,
'-n': '--no-verify',
'-r': '--rules'
})
} catch (err) {
handleError(err)
return 1;
}
if (argv['--help']) {
help()
return 2;
}
const output: Output = createOutput({ debug: argv['--debug'] })
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG)
switch (subcommand) {
case 'ls':
return ls(ctx, argv, args, output);
case 'rm':
return rm(ctx, argv, args, output);
default:
return set(ctx, argv, args, output);
}
}
async function confirmDeploymentRemoval(output, _alias) {
const time = chalk.gray(ms(new Date() - new Date(_alias.created)) + ' ago')
const _sourceUrl = _alias.deployment ? chalk.underline(_alias.deployment.url) : null
const tbl = table(
[
[
...(_sourceUrl ? [_sourceUrl] : []),
chalk.underline(_alias.alias),
time
]
], {
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen
})
return promptBool(output, `The following alias will be removed permanently\n ${tbl} \n ${chalk.red('Are you sure?')}`)
}
function findAlias(alias, list, output) {
let key
let val
if (/\./.test(alias)) {
val = toHost(alias)
key = 'alias'
} else {
val = alias
key = 'uid'
}
const _alias = list.find(d => {
if (d[key] === val) {
output.debug(`matched alias ${d.uid} by ${key} ${val}`)
return true
}
// Match prefix
if (`${val}.now.sh` === d.alias) {
output.debug(`matched alias ${d.uid} by url ${d.host}`)
return true
}
return false
})
return _alias
}
async function ls (ctx, opts, args, output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
const { currentTeam } = sh;
const contextName = getContextName(sh);
const {log, error, print} = output;
const { apiUrl } = ctx;
const { ['--debug']: debugEnabled } = opts;
const alias = new NowAlias({ apiUrl, token, debug: debugEnabled, currentTeam })
if (args.length === 1) {
let cancelWait;
if (!opts['--json']) {
cancelWait = wait(`Fetching alias details for "${args[0]}" under ${chalk.bold(contextName)}`);
}
const list = await alias.listAliases()
const item = list.find(listItem => {
return (listItem.uid === args[0] || listItem.alias === args[0])
})
if (!item || !item.rules) {
if (cancelWait) {
cancelWait()
}
error(`Could not match path alias for: ${args[0]}`)
return 1
}
if (opts['--json']) {
print(JSON.stringify({ rules: item.rules }, null, 2))
} else {
if (cancelWait) cancelWait();
const header = [
['', 'pathname', 'method', 'dest'].map(s => chalk.dim(s))
]
const text =
list.length === 0
? null
: table(
header.concat(
item.rules.map(rule => {
return [
'',
rule.pathname ? rule.pathname : chalk.cyan('[fallthrough]'),
rule.method ? rule.method : '*',
rule.dest
]
})
),
{
align: ['l', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
)
if (text === null) {
// don't print anything, not even \n
} else {
print(text + '\n')
}
}
return 0;
} else if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls`')}`)
return 1
}
const fetchStamp = stamp()
const aliases = await alias.ls()
aliases.sort((a, b) => new Date(b.created) - new Date(a.created))
log(
`${
plural('alias', aliases.length, true)
} found under ${chalk.bold(contextName)} ${fetchStamp()}`
)
print('\n')
console.log(
table(
[
['source', 'url', 'age'].map(h => chalk.gray(h)),
...aliases.map(
a => ([
a.rules && a.rules.length
? chalk.cyan(`[${plural('rule', a.rules.length, true)}]`)
// for legacy reasons, we might have situations
// where the deployment was deleted and the alias
// not collected appropriately, and we need to handle it
: a.deployment && a.deployment.url ?
a.deployment.url :
chalk.gray(''),
a.alias,
ms(Date.now() - new Date(a.created))
])
)
],
{
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen
}
).replace(/^/gm, ' ') + '\n\n'
)
alias.close()
return 0
}
async function rm (ctx, opts, args, output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
const { ['--debug']: debugEnabled } = opts;
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
const _target = String(args[0])
if (!_target) {
output.error(`${cmd('now alias rm <alias>')} expects one argument`)
return 1
}
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now alias rm <alias>`'
)}`
)
return 1
}
const _aliases = await now.listAliases()
const alias = findAlias(_target, _aliases, output)
if (!alias) {
output.error(
`Alias not found by "${_target}" under ${chalk.bold(contextName)}.
Run ${cmd('`now alias ls`')} to see your aliases.`
)
return 1;
}
const removeStamp = stamp()
try {
const confirmation = opts['--yes'] || await confirmDeploymentRemoval(output, alias)
if (!confirmation) {
output.log('Aborted')
return 0
}
await now.fetch(`/now/aliases/${alias.uid}`, { method: 'DELETE' })
} catch (err) {
output.error(err)
return 1
}
console.log(`${chalk.cyan('> Success!')} Alias ${chalk.bold(alias.alias)} removed ${removeStamp()}`)
return 0
}
async function set(ctx, opts, args, output): Promise<number> {
// Prepare the context
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
const { user, currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
const { ['--debug']: debugEnabled, ['--rules']: rulesPath, ['--no-verify']: noVerify } = opts;
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
const start = Date.now()
// If there are more than two args we have to error
if (args.length > 2) {
output.error(`${cmd('now alias <deployment> <target>')} accepts at most two arguments`);
return 1;
}
// Read the path alias rules in case there is is given
const rules = await getRulesFromFile(rulesPath)
if (rules instanceof Errors.FileNotFound) {
output.error(`Can't find the provided rules file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`)
return 1
} else if (rules instanceof Errors.CantParseJSONFile) {
output.error(`Error parsing provided rules.json file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`)
return 1
} else if (rules instanceof Errors.RulesFileValidationError) {
output.error(`Path Alias validation error: ${rules.meta.message}`);
output.print(` ${chalk.gray('-')} ${rules.meta.location}\n`)
return 1
}
// If the user provided rules and also a deployment target, we should fail
if (args.length === 2 && rules) {
output.error(`You can't supply a deployment target and target rules simultaneously.`);
return 1
}
// Find the targets to perform the alias
const targets = await getTargetsForAlias(output, args, opts['--local-config'])
if (targets instanceof Errors.CantFindConfig) {
output.error(`Couldn't find a project configuration file at \n ${targets.meta.paths.join(' or\n ')}`)
return 1
} else if (targets instanceof Errors.NoAliasInConfig) {
output.error(`Couldn't find a an alias in config`)
return 1
} else if (targets instanceof Errors.InvalidAliasInConfig) {
output.error(`Wrong value for alias found in config. It must be a string or array of string.`)
return 1
} else if (targets instanceof Errors.CantParseJSONFile) {
output.error(`Couldn't parse JSON file ${targets.meta.file}.`);
return 1
} else if (targets instanceof Errors.InvalidAliasTarget) {
output.error(`Invalid target to alias ${targets.meta.target}`);
return 1
}
if (rules) {
// If we have rules for path alias we assign them to the domain
for (const target of targets) {
output.log(`Assigning path alias rules from ${humanizePath(rulesPath)} to ${target}`)
const pathAlias = await upsertPathAlias(output, now, rules, target, contextName)
if (handleSetupDomainErrorImpl(output, handleCreateAliasErrorImpl(output, pathAlias)) !== 1) {
console.log(`${chalk.cyan('> Success!')} ${rules.length} rules configured for ${chalk.underline(target)} ${chalk.grey(
'[' + ms(Date.now() - start) + ']'
)}`)
}
}
} else {
// If there are no rules for path alias we should find out a deployment and perform the alias
const deployment = await getDeploymentForAlias(now, output, args, opts['--local-config'], user, contextName)
if (deployment instanceof Errors.DeploymentNotFound) {
output.error(`Failed to find deployment "${deployment.meta.id}" under ${chalk.bold(contextName)}`)
return 1
} else if (deployment instanceof Errors.DeploymentPermissionDenied) {
output.error(`No permission to access deployment "${deployment.meta.id}" under ${chalk.bold(deployment.meta.context)}`)
return 1
} else if (deployment === null) {
output.error(`Couldn't find a deployment to alias. Please provide one as an argument.`);
return 1
}
// Assign the alias for each of the targets in the array
for (const target of targets) {
output.log(`Assigning alias ${target} to deployment ${deployment.url}`)
const record = await assignAlias(output, now, deployment, target, contextName, noVerify)
if (handleSetupDomainErrorImpl(output, handleCreateAliasErrorImpl(output, record)) !== 1) {
console.log(`${chalk.cyan('> Success!')} ${target} now points to ${chalk.bold(deployment.url)}! ${chalk.grey(
'[' + ms(Date.now() - start) + ']'
)}`)
}
}
}
return 0
}
export type SetupDomainError =
Errors.DNSPermissionDenied |
Errors.DomainNameserversNotFound |
Errors.DomainNotVerified |
Errors.DomainPermissionDenied |
Errors.DomainVerificationFailed |
Errors.NeedUpgrade |
Errors.PaymentSourceNotFound |
Errors.UserAborted
function handleSetupDomainErrorImpl<Other>(output: Output, error: SetupDomainError | Other): 1 | Other {
if (error instanceof Errors.DomainVerificationFailed) {
output.error(`We couldn't verify the domain ${chalk.underline(error.meta.domain)}.\n`)
output.print(` Please make sure that your nameservers point to ${chalk.underline('zeit.world')}.\n`)
output.print(` Examples: (full list at ${chalk.underline('https://zeit.world')})\n`)
output.print(zeitWorldTable() + '\n');
output.print(`\n As an alternative, you can add following records to your DNS settings:\n`)
output.print(dnsTable([
['_now', 'TXT', error.meta.token],
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
], ' ') + '\n');
return 1
} else if (error instanceof Errors.DomainPermissionDenied) {
output.error(`You don't have permissions over domain ${chalk.underline(error.meta.domain)} under ${chalk.bold(error.meta.context)}.`)
return 1
} else if (error instanceof Errors.PaymentSourceNotFound) {
output.error(`No credit cards found to buy the domain. Please run ${cmd('now cc add')}.`)
return 1
} else if (error instanceof Errors.NeedUpgrade) {
output.error(`Custom domains are only supported for premium accounts. Please upgrade.`)
return 1
} else if (error instanceof Errors.DomainNotVerified) {
output.error(`We couldn't verify the domain ${chalk.underline(error.meta.domain)}. Please try again later`)
return 1
} else if (error instanceof Errors.DomainNameserversNotFound) {
output.error(`Couldn't find nameservers for the domain ${chalk.underline(error.meta.domain)}`)
return 1
} else if (error instanceof Errors.DNSPermissionDenied) {
output.error(`You don't have permissions to access the DNS records for ${chalk.underline(error.meta.domain)}`)
return 1
} else if (error instanceof Errors.UserAborted) {
return 1
} else {
return error
}
}
function zeitWorldTable() {
return table([
[chalk.underline('a.zeit.world'), chalk.dim('96.45.80.1')],
[chalk.underline('b.zeit.world'), chalk.dim('46.31.236.1')],
[chalk.underline('c.zeit.world'), chalk.dim('43.247.170.1')],
], {
align: ['l', 'l'],
hsep: ' '.repeat(8),
stringLength: strlen
}).replace(/^(.*)/gm, ' $1')
}
function dnsTable(rows, extraSpace = '') {
return table([
['name', 'type', 'value'].map(v => chalk.gray(v)),
...rows
], {
align: ['l', 'l', 'l'],
hsep: ' '.repeat(8),
stringLength: strlen
}).replace(/^(.*)/gm, `${extraSpace} $1`)
}
type CreateAliasError =
Errors.AliasInUse |
Errors.DeploymentNotFound |
Errors.DeploymentPermissionDenied |
Errors.DomainConfigurationError |
Errors.DomainNotFound |
Errors.DomainPermissionDenied |
Errors.DomainsShouldShareRoot |
Errors.DomainValidationRunning |
Errors.InvalidAlias |
Errors.InvalidWildcardDomain |
Errors.NeedUpgrade |
Errors.RuleValidationFailed |
Errors.TooManyCertificates |
Errors.TooManyRequests
function handleCreateAliasErrorImpl<OtherError>(output: Output, error: CreateAliasError | OtherError): 1 | OtherError {
if (error instanceof Errors.AliasInUse) {
output.error(`The alias ${chalk.dim(error.meta.alias)} is a deployment URL or it's in use by a different team.`)
return 1
} else if (error instanceof Errors.DeploymentNotFound) {
output.error(`Failed to find deployment ${chalk.dim(error.meta.id)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.InvalidAlias ) {
output.error(`Invalid alias. Nested domains are not supported.`)
return 1
} else if (error instanceof Errors.DomainPermissionDenied) {
output.error(`No permission to access domain ${chalk.underline(error.meta.domain)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.DeploymentPermissionDenied) {
output.error(`No permission to access deployment ${chalk.dim(error.meta.id)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.NeedUpgrade) {
output.error(`Custom domains are only supported for premium accounts. Please upgrade.`)
return 1
} else if (error instanceof Errors.DomainConfigurationError) {
output.error(`We couldn't verify the propagation of the DNS settings for ${chalk.underline(error.meta.domain)}`)
if (error.meta.external) {
output.print(` The propagation may take a few minutes, but please verify your settings:\n\n`)
output.print(dnsTable([
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
]) + '\n');
} else {
output.print(` We configured them for you, but the propagation may take a few minutes.\n`)
output.print(` Please try again later.\n`)
}
return 1
} else if (error instanceof Errors.TooManyCertificates) {
output.error(`Too many certificates already issued for exact set of domains: ${error.meta.domains.join(', ')}`)
return 1
} else if (error instanceof Errors.DomainValidationRunning) {
output.error(`There is a validation in course for ${chalk.underline(error.meta.domain)}. Wait until it finishes.`)
return 1
} else if (error instanceof Errors.RuleValidationFailed) {
output.error(`Rule validation error: ${error.meta.message}.`)
output.print(` Make sure your rules file is written correctly.\n`)
return 1
} else if (error instanceof Errors.TooManyRequests) {
output.error(`Too many requests detected for ${error.meta.api} API. Try again later.`)
return 1
} else if (error instanceof Errors.DomainNotFound) {
output.error(`You should buy the domain before aliasing.`)
return 1
} else if (error instanceof Errors.InvalidWildcardDomain) {
// this should never happen
output.error(`Invalid domain ${chalk.underline(error.meta.domain)}. Wildcard domains can only be followed by a root domain.`)
return 1
} else if (error instanceof Errors.DomainsShouldShareRoot) {
// this should never happen either
output.error(`All given common names should share the same root domain.`)
return 1
} else {
return error
}
}

View File

@@ -1,25 +1,25 @@
// @flow
import stamp from '../../../../util/output/stamp'
import { Now, Output } from '../../util/types'
import type { Deployment, HTTPChallengeInfo } from '../../util/types'
import setDeploymentScale from '../../util/scale/set-deployment-scale'
import waitForScale from '../../util/scale/wait-verify-deployment-scale'
import type { Deployment } from '../../util/types'
import * as Errors from '../../util/errors'
import createAlias from './create-alias'
import deploymentShouldCopyScale from './deployment-should-copy-scale'
import deploymentShouldDowscale from './deployment-should-dowscale'
import deploymentShouldDownscale from './deployment-should-downscale'
import fetchDeploymentFromAlias from './get-deployment-from-alias'
import getDeploymentDownscalePresets from './get-deployment-downscale-presets'
import getPreviousAlias from './get-previous-alias'
import setDeploymentScale from './set-deployment-scale'
import setupDomain from './setup-domain'
import waitForScale from './wait-for-scale'
// $FlowFixMe
const NOW_SH_REGEX = /\.now\.sh$/
async function assignAlias(output: Output, now: Now, deployment: Deployment, alias: string, contextName: string, noVerify: boolean) {
const prevAlias = await getPreviousAlias(now, alias)
let httpChallengeInfo: HTTPChallengeInfo
const prevAlias = await getPreviousAlias(output, now, alias)
let externalDomain = false
// If there was a previous deployment, we should fetch it to scale and downscale later
const prevDeployment = await fetchDeploymentFromAlias(output, now, contextName, prevAlias, deployment)
@@ -31,9 +31,23 @@ async function assignAlias(output: Output, now: Now, deployment: Deployment, ali
if (prevDeployment !== null && prevDeployment.type !== 'STATIC' && deployment.type !== 'STATIC') {
if (deploymentShouldCopyScale(prevDeployment, deployment)) {
const scaleStamp = stamp()
await setDeploymentScale(output, now, deployment.uid, prevDeployment.scale)
if (!noVerify) { await waitForScale(output, now, deployment.uid, prevDeployment.scale) }
output.success(`Scale rules copied from previous alias ${prevDeployment.url} ${scaleStamp()}`);
const result = await setDeploymentScale(output, now, deployment.uid, prevDeployment.scale, deployment.url)
if (
result instanceof Errors.NotSupportedMinScaleSlots ||
result instanceof Errors.ForbiddenScaleMinInstances ||
result instanceof Errors.ForbiddenScaleMaxInstances ||
result instanceof Errors.InvalidScaleMinMaxRelation
) {
return result;
}
output.log(`Scale rules copied from previous alias ${prevDeployment.url} ${scaleStamp()}`);
if (!noVerify) {
const result = await waitForScale(output, now, deployment.uid, prevDeployment.scale)
if (result instanceof Errors.VerifyScaleTimeout) {
return result
}
}
} else {
output.debug(`Both deployments have the same scaling rules.`)
}
@@ -41,35 +55,35 @@ async function assignAlias(output: Output, now: Now, deployment: Deployment, ali
// Check if the alias is a custom domain and if case we have a positive
// we have to configure the DNS records and certificate
if (!NOW_SH_REGEX.test(alias)) {
if (alias.indexOf('.') !== -1 && !NOW_SH_REGEX.test(alias)) {
// Now the domain shouldn't be available and it might or might not belong to the user
const result = await setupDomain(output, now, alias, contextName)
if (
(result instanceof Errors.DNSPermissionDenied) ||
(result instanceof Errors.DomainNameserversNotFound) ||
(result instanceof Errors.DomainNotFound) ||
(result instanceof Errors.DomainNotVerified) ||
(result instanceof Errors.DomainPermissionDenied) ||
(result instanceof Errors.DomainVerificationFailed) ||
(result instanceof Errors.NeedUpgrade) ||
(result instanceof Errors.InvalidCoupon) ||
(result instanceof Errors.MissingCreditCard) ||
(result instanceof Errors.CDNNeedsUpgrade) ||
(result instanceof Errors.PaymentSourceNotFound) ||
(result instanceof Errors.UnsupportedTLD) ||
(result instanceof Errors.UsedCoupon) ||
(result instanceof Errors.UserAborted)
) {
return result
}
// Maybe we get here an error of misconfigured shit
if (result instanceof Errors.MissingDomainDNSRecords) {
httpChallengeInfo = {
canSolveForRootDomain: !result.meta.forRootDomain,
canSolveForSubdomain: !result.meta.forSubdomain
}
}
// Assign if the domain is external to request wildcard or normal certificate
externalDomain = result.isExternal
}
// Create the alias and the certificate if it's missing
const record = await createAlias(output, now, deployment, alias, contextName, httpChallengeInfo)
const record = await createAlias(output, now, contextName, deployment, alias, externalDomain)
if (
(record instanceof Errors.AliasInUse) ||
(record instanceof Errors.CantSolveChallenge) ||
(record instanceof Errors.DeploymentNotFound) ||
(record instanceof Errors.DomainConfigurationError) ||
(record instanceof Errors.DomainPermissionDenied) ||
@@ -77,7 +91,6 @@ async function assignAlias(output: Output, now: Now, deployment: Deployment, ali
(record instanceof Errors.DomainValidationRunning) ||
(record instanceof Errors.InvalidAlias) ||
(record instanceof Errors.InvalidWildcardDomain) ||
(record instanceof Errors.NeedUpgrade) ||
(record instanceof Errors.TooManyCertificates) ||
(record instanceof Errors.TooManyRequests)
) {
@@ -86,9 +99,9 @@ async function assignAlias(output: Output, now: Now, deployment: Deployment, ali
// Downscale if the previous deployment is not static and doesn't have the minimal presets
if (prevDeployment !== null && prevDeployment.type !== 'STATIC') {
if (await deploymentShouldDowscale(output, now, prevDeployment)) {
await setDeploymentScale(output, now, prevDeployment.uid, getDeploymentDownscalePresets(prevDeployment))
output.success(`Previous deployment ${prevDeployment.url} downscaled`);
if (await deploymentShouldDownscale(output, now, prevDeployment)) {
await setDeploymentScale(output, now, prevDeployment.uid, getDeploymentDownscalePresets(prevDeployment), prevDeployment.url)
output.log(`Previous deployment ${prevDeployment.url} downscaled`);
}
}

View File

@@ -1,17 +1,17 @@
// @flow
import wait from '../../../../util/output/wait'
import type { AliasRecord, Deployment, HTTPChallengeInfo } from '../../util/types'
import { Now, Output } from '../../util/types'
import * as Errors from '../../util/errors'
import createCertForAlias from './create-cert-for-alias'
import type { AliasRecord, Deployment } from '../../util/types'
async function createAlias(
output: Output,
now: Now,
deployment: Deployment,
alias: string,
contextName: string,
httpChallengeInfo?: HTTPChallengeInfo,
output: Output,
now: Now,
contextName: string,
deployment: Deployment,
alias: string,
externalDomain: boolean
) {
const cancelMessage = wait(`Creating alias`)
try {
@@ -27,8 +27,9 @@ async function createAlias(
// If the certificate is missing we create it without expecting failures
// then we call back the createAlias function
if (error.code === 'cert_missing' || error.code === 'cert_expired') {
const cert = await createCertForAlias(output, now, alias, contextName, httpChallengeInfo)
const cert = await createCertForAlias(output, now, contextName, alias, !externalDomain)
if (
(cert instanceof Errors.CantSolveChallenge) ||
(cert instanceof Errors.DomainConfigurationError) ||
(cert instanceof Errors.DomainPermissionDenied) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
@@ -39,7 +40,7 @@ async function createAlias(
) {
return cert
} else {
return createAlias(output, now, deployment, alias, contextName, httpChallengeInfo)
return createAlias(output, now, contextName, deployment, alias, !externalDomain)
}
}
@@ -59,10 +60,6 @@ async function createAlias(
}
if (error.status === 403) {
if (error.code === 'custom_domain_needs_upgrade') {
return new Errors.NeedUpgrade()
}
if (error.code === 'alias_in_use') {
return new Errors.AliasInUse(alias)
}

View File

@@ -1,22 +1,21 @@
// @flow
import psl from 'psl'
import joinWords from '../../../../util/output/join-words'
import stamp from '../../../../util/output/stamp'
import wait from '../../../../util/output/wait'
import * as Errors from '../../util/errors'
import { Now, Output } from '../../util/types'
import type { HTTPChallengeInfo } from '../../util/types'
import createCertForCns from '../../util/certs/create-cert-for-cns'
import getWildcardCnsForAlias from './get-wildcard-cns-for-alias'
async function createCertificateForAlias(output: Output, now: Now, alias: string, context: string, httpChallengeInfo?: HTTPChallengeInfo) {
const { domain, subdomain } = psl.parse(alias)
const { cns, preferDNS } = getCertRequestSettings(alias, domain, subdomain, httpChallengeInfo)
async function createCertificateForAlias(output: Output, now: Now, context: string, alias: string, shouldBeWildcard: boolean) {
const cns = shouldBeWildcard ? getWildcardCnsForAlias(alias) : [alias]
const cancelMessage = wait(`Generating a certificate...`)
const certStamp = stamp()
// Generate the certificate with the given parameters
let cert = await createCertForCns(now, cns, context, { preferDNS })
let cert = await createCertForCns(now, cns, context)
if (
(cert instanceof Errors.CantSolveChallenge) ||
(cert instanceof Errors.DomainConfigurationError) ||
(cert instanceof Errors.DomainPermissionDenied) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
@@ -33,8 +32,9 @@ async function createCertificateForAlias(output: Output, now: Now, alias: string
// valid we can fallback to try to generate a normal certificate
if ((cert instanceof Errors.CantGenerateWildcardCert)) {
output.debug(`Falling back to a normal certificate`)
cert = await createCertForCns(now, [alias], context, { preferDNS })
cert = await createCertForCns(now, [alias], context)
if (
(cert instanceof Errors.CantSolveChallenge) ||
(cert instanceof Errors.DomainConfigurationError) ||
(cert instanceof Errors.DomainPermissionDenied) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
@@ -50,34 +50,12 @@ async function createCertificateForAlias(output: Output, now: Now, alias: string
// This is completely unexpected and should never happens
if (cert instanceof Errors.CantGenerateWildcardCert) {
throw cert
}
}
}
cancelMessage()
output.log(`Certificate for ${joinWords(cns)} (${cert.uid}) created ${certStamp()}`)
output.log(`Certificate for ${joinWords(cert.cns)} (${cert.uid}) created ${certStamp()}`)
return cert
}
function getCertRequestSettings(alias: string, domain: string, subdomain: string, httpChallengeInfo?: HTTPChallengeInfo) {
if (httpChallengeInfo) {
if (subdomain === null) {
if (httpChallengeInfo.canSolveForRootDomain) {
return { cns: [domain, `*.${domain}`], preferDNS: false }
} else {
return { cns: [alias], preferDNS: true }
}
} else {
if (httpChallengeInfo.canSolveForRootDomain) {
return { cns: [domain, `*.${domain}`], preferDNS: false }
} else if (httpChallengeInfo.canSolveForSubdomain) {
return { cns: [alias], preferDNS: false }
} else {
return { cns: [alias], preferDNS: true }
}
}
} else {
return { cns: [domain, `*.${domain}`], preferDNS: false }
}
}
export default createCertificateForAlias

View File

@@ -1,9 +1,10 @@
// @flow
import { Now } from '../../util/types'
import type { Deployment } from '../../util/types'
import getAliases from '../../util/alias/get-aliases'
async function deploymentIsAliased(now: Now, deployment: Deployment) {
const aliases = await now.listAliases()
const aliases = await getAliases(now)
return aliases.some(alias => alias.deploymentId === deployment.uid)
}

View File

@@ -1,12 +1,18 @@
// @flow
import type { NpmDeployment, BinaryDeployment } from '../../util/types'
import type { NpmDeployment, DockerDeployment } from '../../util/types'
import getScaleForDC from './get-scale-for-dc'
function shouldCopyScalingAttributes(origin: NpmDeployment | BinaryDeployment, dest: NpmDeployment | BinaryDeployment) {
return getScaleForDC('bru1', origin).min !== getScaleForDC('bru1', dest).min ||
function shouldCopyScalingAttributes(origin: NpmDeployment | DockerDeployment, dest: NpmDeployment | DockerDeployment) {
return Boolean(origin.scale) &&
getScaleForDC('bru1', origin).min !== getScaleForDC('bru1', dest).min ||
getScaleForDC('bru1', origin).max !== getScaleForDC('bru1', dest).max ||
getScaleForDC('gru1', origin).min !== getScaleForDC('gru1', dest).min ||
getScaleForDC('gru1', origin).max !== getScaleForDC('gru1', dest).max ||
getScaleForDC('sfo1', origin).min !== getScaleForDC('sfo1', dest).min ||
getScaleForDC('sfo1', origin).max !== getScaleForDC('sfo1', dest).max
getScaleForDC('sfo1', origin).max !== getScaleForDC('sfo1', dest).max ||
getScaleForDC('iad1', origin).min !== getScaleForDC('iad1', dest).min ||
getScaleForDC('iad1', origin).max !== getScaleForDC('iad1', dest).max
}
export default shouldCopyScalingAttributes

View File

@@ -1,16 +1,20 @@
// @flow
import { Output, Now } from '../../util/types'
import type { NpmDeployment, BinaryDeployment } from '../../util/types'
import type { NpmDeployment, DockerDeployment } from '../../util/types'
import deploymentIsAliased from './deployment-is-aliased'
import getScaleForDC from './get-scale-for-dc'
async function deploymentShouldDowscale(output: Output, now: Now, deployment: NpmDeployment | BinaryDeployment) {
async function deploymentShouldDownscale(output: Output, now: Now, deployment: NpmDeployment | DockerDeployment) {
const isAliased = await deploymentIsAliased(now, deployment)
output.debug(`Previous deployment is aliased: ${isAliased.toString()}`)
if (deployment.type === 'DOCKER' && !!deployment.slot) {
// Don't downscale a previous slot deployment
return false;
}
return !isAliased && Object.keys(deployment.scale).reduce((result, dc) => {
return result || getScaleForDC(dc, deployment).min !== 0 ||
getScaleForDC(dc, deployment).max !== 1
}, false)
}
export default deploymentShouldDowscale
export default deploymentShouldDownscale

View File

@@ -0,0 +1,15 @@
// @flow
import { Now, Output } from '../../util/types'
import type { Alias } from '../../util/types'
function getSafeAlias(alias: string) {
return alias
.replace(/^https:\/\//i, '')
.replace(/^\.+/, '')
.replace(/\.+$/, '')
.toLowerCase()
}
export default async function findAliasByAliasOrId(output: Output, now: Now, aliasOrId: string): Promise<Alias> {
return now.fetch(`/now/aliases/${encodeURIComponent(getSafeAlias(aliasOrId))}`);
}

View File

@@ -1,31 +1,19 @@
// @flow
import chalk from 'chalk'
import wait from '../../../../util/output/wait'
import { Output, Now } from '../../util/types'
import type { User } from '../../util/types'
import fetchDeploymentByIdOrHost from './get-deployment-by-id-or-host'
import fetchDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host'
import fetchDeploymentsByAppName from './get-deployments-by-appname'
async function getAppLastDeployment(output: Output, now: Now, appName: string, user: User, contextName: string) {
output.debug(`Looking for deployments matching app ${appName}`)
const cancelWait = wait(`Fetching user deployments in ${chalk.bold(contextName)}`)
let deployments
try {
deployments = await fetchDeploymentsByAppName(now, appName)
cancelWait()
} catch (error) {
cancelWait()
throw error
}
const deployments = await fetchDeploymentsByAppName(now, appName)
const deploymentItem = deployments
.sort((a, b) => b.created - a.created)
.filter(dep => dep.state === 'READY' && dep.creator.uid === user.uid)[0]
// Try to fetch deployment details
return deploymentItem
? await fetchDeploymentByIdOrHost(output, now, contextName, deploymentItem.uid)
? await fetchDeploymentByIdOrHost(now, contextName, deploymentItem.uid)
: null
}

View File

@@ -5,7 +5,7 @@ import { Output } from '../../util/types'
import getConfig from './get-config'
import readPackage from './read-package'
async function getAppName(output: Output, localConfig: string) {
async function getAppName(output: Output, localConfig?: string) {
const config = await getConfig(output, localConfig)
// If the name is in the configuration, return it
@@ -14,9 +14,11 @@ async function getAppName(output: Output, localConfig: string) {
}
// Otherwise try to get it from the package
const pkg = await readPackage()
if (!(pkg instanceof NowError) && pkg) {
return pkg.name
if (!(config instanceof NowError) && (!config.type || config.type === "npm")) {
const pkg = await readPackage()
if (!(pkg instanceof NowError) && pkg) {
return pkg.name
}
}
// Finally fallback to directory

View File

@@ -1,46 +0,0 @@
// @flow
import chalk from 'chalk'
import toHost from '../../util/to-host'
import wait from '../../../../util/output/wait'
import { Output, Now } from '../../util/types'
import type { Deployment } from '../../util/types'
import { DeploymentNotFound, DeploymentPermissionDenied } from '../../util/errors'
async function getDeploymentByIdOrHost(output: Output, now: Now, contextName: string, idOrHost: string) {
const cancelWait = wait(`Fetching deployment "${idOrHost}" in ${chalk.bold(contextName)}`);
try {
const { deployment } = idOrHost.indexOf('.') !== -1
? await getDeploymentByHost(output, now, toHost(idOrHost))
: await getDeploymentById(output, now, idOrHost)
cancelWait()
return deployment
} catch (error) {
cancelWait()
if (error.status === 404) {
return new DeploymentNotFound(idOrHost, contextName)
} else if (error.status === 403) {
return new DeploymentPermissionDenied(idOrHost, contextName)
} else {
throw error;
}
}
}
async function getDeploymentById(output: Output, now: Now, id: string): Promise<{ deployment: Deployment }> {
const deployment = await now.fetch(`/v3/now/deployments/${encodeURIComponent(id)}`)
return { deployment }
}
type DeploymentHostResponse = {
deployment: {
id: string
}
}
async function getDeploymentByHost(output: Output, now: Now, host: string): Promise<{ deployment: Deployment }> {
const response: DeploymentHostResponse = await now.fetch(`/v3/now/hosts/${encodeURIComponent(host)}?resolve=1`)
return getDeploymentById(output, now, response.deployment.id)
}
export default getDeploymentByIdOrHost

View File

@@ -1,7 +1,7 @@
// @flow
import type { NpmDeployment, BinaryDeployment, DeploymentScale } from '../../util/types'
import type { NpmDeployment, DockerDeployment, DeploymentScaleArgs } from '../../util/types'
function getDeploymentDownscalePresets(deployment: NpmDeployment | BinaryDeployment): DeploymentScale {
function getDeploymentDownscalePresets(deployment: NpmDeployment | DockerDeployment): DeploymentScaleArgs {
return Object.keys(deployment.scale).reduce((result, dc) => {
return Object.assign(result, {
[dc]: { min: 0, max: 1 }

View File

@@ -1,19 +1,27 @@
// @flow
import chalk from 'chalk'
import { Now, Output } from '../../util/types'
import type { User } from '../../util/types'
import getAppLastDeployment from './get-app-last-deployment'
import getAppName from './get-app-name'
import fetchDeploymentByIdOrHost from './get-deployment-by-id-or-host'
import fetchDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host'
import wait from '../../../../util/output/wait'
async function getDeploymentForAlias(now: Now, output: Output, args: Array<string>, localConfig: string, user: User, contextName: string) {
async function getDeploymentForAlias(now: Now, output: Output, args: Array<string>, localConfig: string | void, user: User, contextName: string) {
const cancelWait = wait(`Fetching deployment to alias in ${chalk.bold(contextName)}`)
let deployment
// When there are no args at all we try to get the targets from the config
if (args.length === 2) {
const [deploymentId] = args
return await fetchDeploymentByIdOrHost(output, now, contextName, deploymentId)
deployment = await fetchDeploymentByIdOrHost(now, contextName, deploymentId)
} else {
const appName = await getAppName(output, localConfig)
return await getAppLastDeployment(output, now, appName, user, contextName)
deployment = await getAppLastDeployment(output, now, appName, user, contextName)
}
cancelWait()
return deployment
}
export default getDeploymentForAlias

View File

@@ -1,7 +1,7 @@
// @flow
import { Output, Now } from '../../util/types'
import type { Alias, Deployment } from '../../util/types'
import fetchDeploymentByIdOrHost from './get-deployment-by-id-or-host'
import fetchDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host'
async function fetchDeploymentFromAlias(
output: Output,
@@ -11,7 +11,7 @@ async function fetchDeploymentFromAlias(
currentDeployment: Deployment
) {
return (prevAlias && prevAlias.deploymentId && prevAlias.deploymentId !== currentDeployment.uid)
? fetchDeploymentByIdOrHost(output, now, contextName, prevAlias.deploymentId)
? fetchDeploymentByIdOrHost(now, contextName, prevAlias.deploymentId)
: null
}

View File

@@ -1,14 +0,0 @@
// @flow
import { Now } from '../../util/types'
type InstancesInfo = {
[dc: string]: {
instances: Array<{}>
}
}
async function getDeploymentInstances(now: Now, deploymentId: string): Promise<InstancesInfo> {
return now.fetch(`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances?init=1`)
}
export default getDeploymentInstances

View File

@@ -1,12 +0,0 @@
// @flow
import { Now, Output } from '../../util/types'
import type { DNSRecord } from '../../util/types'
async function getDomainDNSRecords(output: Output, now: Now, domain: string) {
output.debug(`Fetching for DNS records of domain ${domain}`)
const payload = await now.fetch(`/domains/${encodeURIComponent(domain)}/records`)
const records: DNSRecord[] = payload.records
return records
}
export default getDomainDNSRecords

View File

@@ -1,14 +0,0 @@
// @flow
import qs from 'querystring'
import { Now } from '../../util/types'
type DomainPrice = {
period: boolean,
price: number,
}
async function getDomainPrice(now: Now, domain: string): Promise<DomainPrice> {
return now.fetch(`/domains/price?${qs.stringify({ name: domain })}`)
}
export default getDomainPrice

View File

@@ -1,23 +1,10 @@
// @flow
import { Now } from '../../util/types'
import { Output, Now } from '../../util/types'
import findAliasByAliasOrId from './find-alias-by-alias-or-id'
import type { Alias } from '../../util/types'
async function getPreviousAlias(now: Now, alias: string): Promise<Alias | void> {
const aliases = await now.listAliases()
const safeAlias = getSafeAlias(alias)
return aliases.find(a => a.alias === safeAlias)
}
function getSafeAlias(alias: string) {
const _alias = alias
.replace(/^https:\/\//i, '')
.replace(/^\.+/, '')
.replace(/\.+$/, '')
.toLowerCase()
return _alias.indexOf('.') === -1
? `${_alias}.now.sh`
: _alias
async function getPreviousAlias(output: Output, now: Now, alias: string): Promise<Alias | void> {
return findAliasByAliasOrId(output, now, alias)
}
export default getPreviousAlias

View File

@@ -1,7 +1,7 @@
// @flow
import type { Scale, NpmDeployment, BinaryDeployment } from '../../util/types'
import type { Scale, NpmDeployment, DockerDeployment } from '../../util/types'
function getScaleForDC(dc: string, deployment: NpmDeployment | BinaryDeployment) {
function getScaleForDC(dc: string, deployment: NpmDeployment | DockerDeployment) {
const dcAttrs = deployment.scale && deployment.scale[dc] || {}
const safeScale: Scale = { min: dcAttrs.min, max: dcAttrs.max }
return safeScale

View File

@@ -1,18 +0,0 @@
// @flow
type CommandConfig = {
default: string,
[command: string]: string[],
}
export default function getSubcommand(cliArgs: string[], config: CommandConfig) {
const [subcommand, ...rest] = cliArgs
for (const k of Object.keys(config)) {
if (k !== 'default' && config[k].indexOf(subcommand) !== -1) {
return { subcommand: k, args: rest }
}
}
return {
subcommand: config.default,
args: cliArgs
}
}

View File

@@ -1,12 +1,10 @@
// @flow
import toHost from '../../util/to-host'
import { Output } from '../../util/types'
import * as Errors from '../../util/errors'
import getInferredTargets from './get-inferred-targets'
import isValidDomain from '../../util/domains/is-valid-domain'
async function getTargetsForAlias(output: Output, args: string[], localConfigPath: string | void) {
async function getTargetsForAlias(output: Output, args: string[], localConfigPath?: string | void) {
const targets = await getTargets(output, args, localConfigPath)
if (
(targets instanceof Errors.CantParseJSONFile) ||
@@ -16,20 +14,12 @@ async function getTargetsForAlias(output: Output, args: string[], localConfigPat
) {
return targets
}
// Append zeit if needed or convert to host in case is a full URL
const hostTargets: string[] = targets.map(target => {
return target.indexOf('.') === -1
? `${target}.now.sh`
: toHost(target)
})
// Validate the targets
for (const target of hostTargets) {
if (!isValidDomain(target)) {
return new Errors.InvalidAliasTarget(target)
}
}
const hostTargets: string[] = targets.map(target => {
return target.indexOf('.') !== -1
? toHost(target)
: target
})
return hostTargets
}

View File

@@ -0,0 +1,10 @@
// @flow
import psl from 'psl';
export default function getWildcardCNSForAlias(alias: string) {
const { domain, subdomain } = psl.parse(alias);
const secondLevel = subdomain && subdomain.includes('.') ? subdomain.split('.').slice(1).join('.') : null;
const root = secondLevel ? `${secondLevel}.${domain}` : domain;
return [root, `*.${root}`];
}

View File

@@ -1 +1,128 @@
module.exports = require('./alias')
// @flow
import chalk from 'chalk'
import { handleError } from '../../util/error'
import { Output } from '../../util/types'
import createOutput from '../../../../util/output'
import getArgs from '../../util/get-args'
import getSubcommand from '../../util/get-subcommand'
import logo from '../../../../util/output/logo'
import type { CLIAliasOptions } from '../../util/types'
import ls from './ls'
import rm from './rm'
import set from './set'
const help = () => {
console.log(`
${chalk.bold(`${logo} now alias`)} [options] <command>
${chalk.dim('Commands:')}
ls [app] Show all aliases (or per app name)
set <deployment> <alias> Create a new alias
rm <alias> Remove an alias using its hostname
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-r ${chalk.bold.underline('RULES_FILE')}, --rules=${chalk.bold.underline(
'RULES_FILE'
)} Rules file
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
-n, --no-verify Don't wait until instance count meets the previous alias constraints
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new alias to ${chalk.underline('my-api.now.sh')}
${chalk.cyan(
`$ now alias set ${chalk.underline(
'api-ownv3nc9f8.now.sh'
)} ${chalk.underline('my-api.now.sh')}`
)}
Custom domains work as alias targets
${chalk.cyan(
`$ now alias set ${chalk.underline(
'api-ownv3nc9f8.now.sh'
)} ${chalk.underline('my-api.com')}`
)}
${chalk.dim('')} The subcommand ${chalk.dim(
'`set`'
)} is the default and can be skipped.
${chalk.dim('')} ${chalk.dim(
'Protocols'
)} in the URLs are unneeded and ignored.
${chalk.gray('')} Add and modify path based aliases for ${chalk.underline(
'zeit.ninja'
)}
${chalk.cyan(
`$ now alias ${chalk.underline('zeit.ninja')} -r ${chalk.underline(
'rules.json'
)}`
)}
Export effective routing rules
${chalk.cyan(
`$ now alias ls aliasId --json > ${chalk.underline('rules.json')}`
)}
`)
}
const COMMAND_CONFIG = {
default: 'set',
ls: ['ls', 'list'],
rm: ['rm', 'remove'],
set: ['set'],
}
module.exports = async function main(ctx: any): Promise<number> {
let argv: CLIAliasOptions
try {
argv = getArgs(ctx.argv.slice(2), {
'--json': Boolean,
'--no-verify': Boolean,
'--rules': String,
'--yes': Boolean,
'-n': '--no-verify',
'-r': '--rules',
'-y': '--yes',
})
} catch (err) {
handleError(err)
return 1;
}
if (argv['--help']) {
help()
return 2;
}
const output: Output = createOutput({ debug: argv['--debug'] })
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG)
switch (subcommand) {
case 'ls':
return ls(ctx, argv, args, output);
case 'rm':
return rm(ctx, argv, args, output);
default:
return set(ctx, argv, args, output);
}
}

View File

@@ -0,0 +1,111 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import plural from 'pluralize'
import table from 'text-table'
import Now from '../../util'
import { CLIContext, Output } from '../../util/types'
import getAliases from '../../util/alias/get-aliases'
import getContextName from '../../util/get-context-name'
import stamp from '../../../../util/output/stamp'
import strlen from '../../util/strlen'
import wait from '../../../../util/output/wait'
import type { CLIAliasOptions, Alias, PathAliasRule } from '../../util/types'
export default async function ls(ctx: CLIContext, opts: CLIAliasOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const { apiUrl } = ctx;
const contextName = getContextName(sh);
const {['--debug']: debugEnabled} = opts;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
const lsStamp = stamp()
let cancelWait;
if (args.length > 1) {
output.error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias ls [alias]`')}`)
return 1
}
if (!opts['--json']) {
cancelWait = wait(args[0]
? `Fetching alias details for "${args[0]}" under ${chalk.bold(contextName)}`
: `Fetching aliases under ${chalk.bold(contextName)}`
);
}
const aliases: Alias[] = await getAliases(now)
if (cancelWait) cancelWait();
if (args[0]) {
const alias = aliases.find(item => (item.uid === args[0] || item.alias === args[0]))
if (!alias) {
output.error(`Could not match path alias for: ${args[0]}`)
now.close()
return 1
}
if (opts['--json']) {
output.print(JSON.stringify({ rules: alias.rules }, null, 2))
} else {
const rules: PathAliasRule[] = alias.rules || []
output.log(`${rules.length} path alias ${plural('rule', rules.length)} found under ${chalk.bold(contextName)} ${lsStamp()}`)
output.print(`${printPathAliasTable(rules)}\n`)
}
} else {
aliases.sort((a, b) => new Date(b.created) - new Date(a.created))
output.log(`${plural('alias', aliases.length, true)} found under ${chalk.bold(contextName)} ${lsStamp()}`)
console.log(printAliasTable(aliases))
}
now.close()
return 0
}
function printAliasTable(aliases: Alias[]): string {
return table([
['source', 'url', 'age'].map(h => chalk.gray(h)),
...aliases.map(
a => ([
a.rules && a.rules.length
? chalk.cyan(`[${plural('rule', a.rules.length, true)}]`)
// for legacy reasons, we might have situations
// where the deployment was deleted and the alias
// not collected appropriately, and we need to handle it
: a.deployment && a.deployment.url ?
a.deployment.url :
chalk.gray(''),
a.alias,
ms(Date.now() - new Date(a.created))
])
)
], {
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen
}).replace(/^/gm, ' ') + '\n\n'
}
function printPathAliasTable(rules: PathAliasRule[]): string {
const header = [['pathname', 'method', 'dest'].map(s => chalk.gray(s))]
return table(
header.concat(
rules.map(rule => {
return [
rule.pathname ? rule.pathname : chalk.cyan('[fallthrough]'),
rule.method ? rule.method : '*',
rule.dest
]
})
),
{
align: ['l', 'l', 'l', 'l'],
hsep: ' '.repeat(6),
stringLength: strlen
}
).replace(/^(.*)/gm, ' $1') + '\n'
}

View File

@@ -1,38 +0,0 @@
// @flow
import { Output, Now } from '../../util/types'
import type { DeploymentScale } from '../../util/types'
import getDeploymentInstances from './get-deployment-instances'
const AUTO = 'auto'
async function matchDeploymentScale(output: Output, now: Now, deploymentId: string, scale: DeploymentScale) {
const currentInstances = await getDeploymentInstances(now, deploymentId)
const dcsToScale = new Set(Object.keys(scale))
const matches: Map<string, number> = new Map()
for (const dc of dcsToScale) {
const currentScale = currentInstances[dc]
if (!currentScale) {
output.debug(`Still no data for DC ${dc}`)
break;
}
const currentInstancesCount = currentScale.instances.length
const { min, max } = scale[dc]
if (isInstanceCountBetween(currentInstancesCount, min, max)) {
matches.set(dc, currentInstancesCount)
output.debug(`DC ${dc} matched scale.`)
} else {
output.debug(`DC ${dc} missing scale. Inteded (${min}, ${max}). Current ${currentInstancesCount}`)
}
}
return matches
}
function isInstanceCountBetween(value: number, min: number, max: number) {
const safeMax = max === AUTO ? Infinity : max
return value >= min && value <= safeMax
}
export default matchDeploymentScale

View File

@@ -1,56 +0,0 @@
// @flow
import { Now, Output } from '../../util/types'
import { DNSPermissionDenied, MissingDomainDNSRecords } from '../../util/errors'
import getDomainDNSRecords from './get-domain-dns-records'
import setupDNSRecord from './setup-dns-record'
const ALIAS_ZEIT = 'alias.zeit.co'
const ALIAS_ZEIT_RECORD = 'alias.zeit.co.'
async function maybeSetupDNSRecords(output: Output, now: Now, domain: string, subdomain: string | null) {
const records = await getDomainDNSRecords(output, now, domain)
let misconfiguredForRootDomain = false
let misconfiguredForSubdomain = false
// Find all the ALIAS records and if there is a collision flat that root is misconfigured
const aliasRecords = records.filter(record => record.type === 'ALIAS')
if (aliasRecords.length === 0 || !aliasRecords.find(record => record.name === '' && record.value !== ALIAS_ZEIT_RECORD)) {
const aliasResult = await setupDNSRecord(output, now, 'ALIAS', '', domain, ALIAS_ZEIT)
if (aliasResult instanceof DNSPermissionDenied) {
return aliasResult
}
} else {
misconfiguredForRootDomain = true
}
// Find all CNAME records and if there are no collisions configure, otherwise flag as misconfigured
const cnameRecords = records.filter(record => record.type === 'CNAME')
if (cnameRecords.length === 0) {
const cnameResult = await setupDNSRecord(output, now, 'CNAME', '*', domain, ALIAS_ZEIT)
if (cnameResult instanceof DNSPermissionDenied) {
return cnameResult
}
} else if (subdomain) {
if (
!cnameRecords.find(record => record.name === '*' && record.value !== ALIAS_ZEIT_RECORD) &&
!cnameRecords.find(record => record.name === subdomain && record.value !== ALIAS_ZEIT_RECORD)
) {
const cnameResult = await setupDNSRecord(output, now, 'CNAME', subdomain, domain, ALIAS_ZEIT)
if (cnameResult instanceof DNSPermissionDenied) {
return cnameResult
}
} else {
misconfiguredForSubdomain = true
}
}
// If we've found anything misconfigured, return an error
if (misconfiguredForRootDomain || misconfiguredForSubdomain) {
return new MissingDomainDNSRecords({
forRootDomain: misconfiguredForRootDomain,
forSubdomain: misconfiguredForSubdomain
})
}
}
export default maybeSetupDNSRecords

View File

@@ -11,9 +11,9 @@ import wait from '../../../../util/output/wait'
// Internal utils
import { Now, Output } from '../../util/types'
import { NowError } from '../../util/now-error'
import { DomainNotFound, UserAborted } from '../../util/errors'
import getDomainPrice from './get-domain-price'
import getDomainStatus from './get-domain-status'
import * as Errors from '../../util/errors'
import getDomainPrice from '../../util/domains/get-domain-price'
import getDomainStatus from '../../util/domains/get-domain-status'
import purchaseDomain from './purchase-domain'
// $FlowFixMe
@@ -26,20 +26,25 @@ async function purchaseDomainIfAvailable(output: Output, now: Now, domain: strin
if (available) {
// If we can't prompty and the domain is available, we should fail
if (!isTTY) { return new DomainNotFound(domain) }
if (!isTTY) { return new Errors.DomainNotFound(domain) }
output.debug(`Domain is available to purchase`)
const { period, price } = await getDomainPrice(now, domain)
cancelWait()
output.log(
`Domain not found, but you can buy it under ${
chalk.bold(contextName)
}! ${buyDomainStamp()}`
)
const domainPrice = await getDomainPrice(now, domain)
cancelWait()
if (
(domainPrice instanceof Errors.InvalidCoupon) ||
(domainPrice instanceof Errors.UsedCoupon) ||
(domainPrice instanceof Errors.UnsupportedTLD) ||
(domainPrice instanceof Errors.MissingCreditCard)
) {
return domainPrice
}
const { price, period } = domainPrice
output.log(`Domain not found, but you can buy it under ${chalk.bold(contextName)}! ${buyDomainStamp()}`)
if (!await promptBool(`Buy ${chalk.underline(domain)} for ${chalk.bold(`$${price}`)} (${plural('yr', period, true)})?`)) {
output.print(eraseLines(1))
return new UserAborted()
return new Errors.UserAborted()
}
output.print(eraseLines(1))

View File

@@ -0,0 +1,73 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import table from 'text-table'
import Now from '../../util'
import cmd from '../../../../util/output/cmd'
import getContextName from '../../util/get-context-name'
import removeAliasById from '../../util/alias/remove-alias-by-id'
import stamp from '../../../../util/output/stamp'
import strlen from '../../util/strlen'
import { CLIContext, Output } from '../../util/types'
import type { CLIAliasOptions, Alias } from '../../util/types'
import findAliasByAliasOrId from './find-alias-by-alias-or-id'
import promptBool from './prompt-bool'
export default async function rm(ctx: CLIContext, opts: CLIAliasOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const { apiUrl } = ctx;
const contextName = getContextName(sh);
const {['--debug']: debugEnabled} = opts;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
const [aliasOrId] = args
if (!aliasOrId) {
output.error(`${cmd('now alias rm <alias>')} expects one argument`)
return 1
}
if (args.length !== 1) {
output.error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias rm <alias>`')}`)
return 1
}
const alias: Alias | void = await findAliasByAliasOrId(output, now, aliasOrId)
if (!alias) {
output.error(`Alias not found by "${aliasOrId}" under ${chalk.bold(contextName)}`)
output.log(`Run ${cmd('now alias ls')} to see your aliases.`)
return 1;
}
const removeStamp = stamp()
if (!opts['--yes'] && !(await confirmAliasRemove(output, alias))) {
output.log('Aborted')
return 0
}
await removeAliasById(now, alias.uid)
console.log(`${chalk.cyan('> Success!')} Alias ${chalk.bold(alias.alias)} removed ${removeStamp()}`)
return 0
}
async function confirmAliasRemove(output: Output, alias: Alias) {
const srcUrl = alias.deployment ? chalk.underline(alias.deployment.url) : null
const tbl = table([
[ ...(srcUrl ? [srcUrl] : []),
chalk.underline(alias.alias),
chalk.gray(ms(new Date() - new Date(alias.created)) + ' ago')
]], {
align: ['l', 'l', 'r'],
hsep: ' '.repeat(4),
stringLength: strlen
})
output.log(`The following alias will be removed permanently`)
output.print(` ${tbl}\n`)
return promptBool(output, chalk.red('Are you sure?'))
}

View File

@@ -1,28 +0,0 @@
// @flow
import chalk from 'chalk'
import wait from '../../../../util/output/wait'
import { Output, Now } from '../../util/types'
import type { DeploymentScale } from '../../util/types'
async function setScale(output: Output, now: Now, deploymentId: string, scaleArgs: DeploymentScale) {
const scalesMsg = formatScaleArgs(scaleArgs)
const cancelWait = wait(`Setting scale rules for regions ${scalesMsg}`)
try {
await now.fetch(`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances`, {
method: 'PATCH',
body: scaleArgs
})
cancelWait()
} catch (error) {
cancelWait()
throw error
}
}
function formatScaleArgs(scaleArgs: DeploymentScale) {
return Object.keys(scaleArgs).map(dc => {
return `${chalk.bold(dc)}`
}).join(', ')
}
export default setScale

View File

@@ -0,0 +1,293 @@
// @flow
import ms from 'ms'
import chalk from 'chalk'
import { CLIContext, Output } from '../../util/types'
import * as Errors from '../../util/errors'
import cmd from '../../../../util/output/cmd'
import dnsTable from '../../util/dns-table'
import getContextName from '../../util/get-context-name'
import humanizePath from '../../../../util/humanize-path'
import Now from '../../util'
import stamp from '../../../../util/output/stamp'
import zeitWorldTable from '../../util/zeit-world-table'
import type { CLIAliasOptions } from '../../util/types'
import assignAlias from './assign-alias'
import getDeploymentForAlias from './get-deployment-for-alias'
import getRulesFromFile from './get-rules-from-file'
import getTargetsForAlias from './get-targets-for-alias'
import upsertPathAlias from './upsert-path-alias'
export default async function set(ctx: CLIContext, opts: CLIAliasOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam, user } = sh;
const { apiUrl } = ctx;
const contextName = getContextName(sh);
const setStamp = stamp()
const {
['--debug']: debugEnabled,
['--no-verify']: noVerify,
['--rules']: rulesPath,
} = opts;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
// If there are more than two args we have to error
if (args.length > 2) {
output.error(`${cmd('now alias <deployment> <target>')} accepts at most two arguments`);
return 1;
}
// Read the path alias rules in case there is is given
const rules = await getRulesFromFile(rulesPath)
if (rules instanceof Errors.FileNotFound) {
output.error(`Can't find the provided rules file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`)
return 1
} else if (rules instanceof Errors.CantParseJSONFile) {
output.error(`Error parsing provided rules.json file at location:`);
output.print(` ${chalk.gray('-')} ${rules.meta.file}\n`)
return 1
} else if (rules instanceof Errors.RulesFileValidationError) {
output.error(`Path Alias validation error: ${rules.meta.message}`);
output.print(` ${chalk.gray('-')} ${rules.meta.location}\n`)
return 1
}
// If the user provided rules and also a deployment target, we should fail
if (args.length === 2 && rules) {
output.error(`You can't supply a deployment target and target rules simultaneously.`);
return 1
}
// Find the targets to perform the alias
const targets = await getTargetsForAlias(output, args, opts['--local-config'])
if (targets instanceof Errors.CantFindConfig) {
output.error(`Couldn't find a project configuration file at \n ${targets.meta.paths.join(' or\n ')}`)
return 1
} else if (targets instanceof Errors.NoAliasInConfig) {
output.error(`Couldn't find a an alias in config`)
return 1
} else if (targets instanceof Errors.InvalidAliasInConfig) {
output.error(`Wrong value for alias found in config. It must be a string or array of string.`)
return 1
} else if (targets instanceof Errors.CantParseJSONFile) {
output.error(`Couldn't parse JSON file ${targets.meta.file}.`);
return 1
}
if (rules) {
// If we have rules for path alias we assign them to the domain
for (const target of targets) {
output.log(`Assigning path alias rules from ${humanizePath(rulesPath)} to ${target}`)
const pathAlias = await upsertPathAlias(output, now, rules, target, contextName)
if (handleSetupDomainErrorImpl(output, handleCreateAliasErrorImpl(output, pathAlias)) !== 1) {
console.log(`${chalk.cyan('> Success!')} ${rules.length} rules configured for ${chalk.underline(target)} ${setStamp()}`)
}
}
} else {
// If there are no rules for path alias we should find out a deployment and perform the alias
const deployment = await getDeploymentForAlias(now, output, args, opts['--local-config'], user, contextName)
if (deployment instanceof Errors.DeploymentNotFound) {
output.error(`Failed to find deployment "${deployment.meta.id}" under ${chalk.bold(contextName)}`)
return 1
} else if (deployment instanceof Errors.DeploymentPermissionDenied) {
output.error(`No permission to access deployment "${deployment.meta.id}" under ${chalk.bold(deployment.meta.context)}`)
return 1
} else if (deployment === null) {
output.error(`Couldn't find a deployment to alias. Please provide one as an argument.`);
return 1
}
// Assign the alias for each of the targets in the array
for (const target of targets) {
output.log(`Assigning alias ${target} to deployment ${deployment.url}`)
const record = await assignAlias(output, now, deployment, target, contextName, noVerify)
const handleResult = handleSetupDomainErrorImpl(output, handleCreateAliasErrorImpl(output, record));
if (handleResult !== 1) {
console.log(`${chalk.cyan('> Success!')} ${handleResult.alias} now points to ${chalk.bold(deployment.url)} ${setStamp()}`)
}
}
}
return 0
}
export type SetupDomainError =
Errors.DomainNameserversNotFound |
Errors.DomainNotFound |
Errors.DomainNotVerified |
Errors.DomainPermissionDenied |
Errors.DomainVerificationFailed |
Errors.InvalidCoupon |
Errors.MissingCreditCard |
Errors.CDNNeedsUpgrade |
Errors.PaymentSourceNotFound |
Errors.UnsupportedTLD |
Errors.UsedCoupon |
Errors.UserAborted
function handleSetupDomainErrorImpl<Other>(output: Output, error: SetupDomainError | Other): 1 | Other {
if (error instanceof Errors.DomainVerificationFailed) {
output.error(`We couldn't verify the domain ${chalk.underline(error.meta.domain)}.\n`)
output.print(` Please make sure that your nameservers point to ${chalk.underline('zeit.world')}.\n`)
output.print(` Examples: (full list at ${chalk.underline('https://zeit.world')})\n`)
output.print(zeitWorldTable() + '\n');
output.print(`\n As an alternative, you can add following records to your DNS settings:\n`)
output.print(dnsTable([
['_now', 'TXT', error.meta.token],
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
], {extraSpace: ' '}) + '\n');
return 1
} else if (error instanceof Errors.DomainPermissionDenied) {
output.error(`You don't have permissions over domain ${chalk.underline(error.meta.domain)} under ${chalk.bold(error.meta.context)}.`)
return 1
} else if (error instanceof Errors.PaymentSourceNotFound) {
output.error(`No credit cards found to buy the domain. Please run ${cmd('now cc add')}.`)
return 1
} else if (error instanceof Errors.CDNNeedsUpgrade) {
output.error(`You can't add domains with CDN enabled from an OSS plan`)
return 1
} else if (error instanceof Errors.DomainNotVerified) {
output.error(`We couldn't verify the domain ${chalk.underline(error.meta.domain)}. If it's an external domain, add it with --external.`)
return 1
} else if (error instanceof Errors.DomainNameserversNotFound) {
output.error(`Couldn't find nameservers for the domain ${chalk.underline(error.meta.domain)}`)
return 1
} else if (error instanceof Errors.UserAborted) {
output.error(`User aborted`);
return 1
} else if (error instanceof Errors.DomainNotFound) {
output.error(`You should buy the domain before aliasing.`)
return 1
} else if (error instanceof Errors.InvalidCoupon) {
output.error(`The provided coupon ${error.meta.coupon} is invalid.`)
return 1
} else if (error instanceof Errors.MissingCreditCard) {
output.print('You have no credit cards on file. Please add one to purchase the domain.')
return 1
} else if (error instanceof Errors.UnsupportedTLD) {
output.error(`The TLD for domain name ${error.meta.name} is not supported.`)
return 1
} else if (error instanceof Errors.UsedCoupon) {
output.error(`The provided coupon ${error.meta.coupon} can't be used.`)
return 1
} else {
return error
}
}
type CreateAliasError =
Errors.AliasInUse |
Errors.CantSolveChallenge |
Errors.CDNNeedsUpgrade |
Errors.DeploymentNotFound |
Errors.DeploymentPermissionDenied |
Errors.DomainConfigurationError |
Errors.DomainPermissionDenied |
Errors.DomainsShouldShareRoot |
Errors.DomainValidationRunning |
Errors.ForbiddenScaleMaxInstances |
Errors.ForbiddenScaleMinInstances |
Errors.InvalidAlias |
Errors.InvalidScaleMinMaxRelation |
Errors.InvalidWildcardDomain |
Errors.NotSupportedMinScaleSlots |
Errors.RuleValidationFailed |
Errors.TooManyCertificates |
Errors.TooManyRequests |
Errors.VerifyScaleTimeout
function handleCreateAliasErrorImpl<OtherError>(output: Output, error: CreateAliasError | OtherError): 1 | OtherError {
if (error instanceof Errors.AliasInUse) {
output.error(`The alias ${chalk.dim(error.meta.alias)} is a deployment URL or it's in use by a different team.`)
return 1
} else if (error instanceof Errors.DeploymentNotFound) {
output.error(`Failed to find deployment ${chalk.dim(error.meta.id)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.InvalidAlias ) {
output.error(`Invalid alias. Please confirm that the alias you provided is a valid hostname. Note: Nested domains are not supported.`)
return 1
} else if (error instanceof Errors.DomainPermissionDenied) {
output.error(`No permission to access domain ${chalk.underline(error.meta.domain)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.DeploymentPermissionDenied) {
output.error(`No permission to access deployment ${chalk.dim(error.meta.id)} under ${chalk.bold(error.meta.context)}`)
return 1
} else if (error instanceof Errors.CDNNeedsUpgrade) {
output.error(`You can't add domains with CDN enabled from an OSS plan.`)
return 1
} else if (error instanceof Errors.DomainConfigurationError) {
output.error(`We couldn't verify the propagation of the DNS settings for ${chalk.underline(error.meta.domain)}`)
if (error.meta.external) {
output.print(` The propagation may take a few minutes, but please verify your settings:\n\n`)
output.print(dnsTable([
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
]) + '\n');
} else {
output.print(` We configured them for you, but the propagation may take a few minutes.\n`)
output.print(` Please try again later.\n`)
}
return 1
} else if (error instanceof Errors.TooManyCertificates) {
output.error(`Too many certificates already issued for exact set of domains: ${error.meta.domains.join(', ')}`)
return 1
} else if (error instanceof Errors.CantSolveChallenge) {
if (error.meta.type === 'dns-01') {
output.error(`The certificate provider could not resolve the DNS queries for ${error.meta.domain}.`)
output.print(` This might happen to new domains or domains with recent DNS changes. Please retry later.\n`)
} else {
output.error(`The certificate provider could not resolve the HTTP queries for ${error.meta.domain}.`)
output.print(` The DNS propagation may take a few minutes, please verify your settings:\n\n`)
output.print(dnsTable([['', 'ALIAS', 'alias.zeit.co']]) + '\n');
}
return 1
} else if (error instanceof Errors.DomainValidationRunning) {
output.error(`There is a validation in course for ${chalk.underline(error.meta.domain)}. Wait until it finishes.`)
return 1
} else if (error instanceof Errors.RuleValidationFailed) {
output.error(`Rule validation error: ${error.meta.message}.`)
output.print(` Make sure your rules file is written correctly.\n`)
return 1
}else if (error instanceof Errors.TooManyRequests) {
output.error(`Too many requests detected for ${error.meta.api} API. Try again in ${ms(error.meta.retryAfter * 1000, { long: true })}.`)
return 1
} else if (error instanceof Errors.VerifyScaleTimeout) {
output.error(`Instance verification timed out (${ms(error.meta.timeout)})`)
output.log('Read more: https://err.sh/now-cli/verification-timeout')
return 1
} else if (error instanceof Errors.InvalidWildcardDomain) {
output.error(`Invalid domain ${chalk.underline(error.meta.domain)}. Wildcard domains can only be followed by a root domain.`)
return 1
} else if (error instanceof Errors.DomainsShouldShareRoot) {
output.error(`All given common names should share the same root domain.`)
return 1
} else if (error instanceof Errors.NotSupportedMinScaleSlots) {
output.error(`Scale rules from previous aliased deployment ${chalk.dim(error.meta.url)} could not be copied since Cloud v2 deployments cannot have a non-zero min`);
output.log(`Update the scale settings on ${chalk.dim(error.meta.url)} with \`now scale\` and try again`)
output.log('Read more: https://err.sh/now-cli/v2-no-min')
return 1;
} else if (error instanceof Errors.ForbiddenScaleMaxInstances) {
output.error(`Scale rules from previous aliased deployment ${chalk.dim(error.meta.url)} could not be copied since the given number of max instances (${error.meta.max}) is not allowed.`);
output.log(`Update the scale settings on ${chalk.dim(error.meta.url)} with \`now scale\` and try again`)
return 1;
} else if (error instanceof Errors.ForbiddenScaleMinInstances) {
output.error(`Scale rules from previous aliased deployment ${chalk.dim(error.meta.url)} could not be copied since the given number of min instances (${error.meta.min}) is not allowed.`);
output.log(`Update the scale settings on ${chalk.dim(error.meta.url)} with \`now scale\` and try again`)
return 1;
} else if (error instanceof Errors.InvalidScaleMinMaxRelation) {
output.error(`Scale rules from previous aliased deployment ${chalk.dim(error.meta.url)} could not be copied becuase the relation between min and max instances is wrong.`);
output.log(`Update the scale settings on ${chalk.dim(error.meta.url)} with \`now scale\` and try again`)
return 1;
} else {
return error;
}
}

View File

@@ -1,25 +0,0 @@
// @flow
import { Now, Output } from '../../util/types'
import { DNSPermissionDenied } from '../../util/errors'
import type { DNSRecordType } from '../../util/types'
async function setupDNSRecord(output: Output, now: Now, type: DNSRecordType, name: string, domain: string, value: string) {
output.debug(`Trying to setup ${type} record with name ${name} for domain ${domain}`)
try {
await now.fetch(`/domains/${domain}/records`, {
body: { type, name, value },
method: 'POST'
})
} catch (error) {
if (error.status === 403) {
return new DNSPermissionDenied(domain)
}
if (error.status !== 409) {
// ignore the record conflict to make it idempotent
throw error
}
}
}
export default setupDNSRecord

View File

@@ -4,29 +4,16 @@ import psl from 'psl'
// Internal utils
import getDomainInfo from './get-domain-info'
import getDomainNameservers from './get-domain-nameservers'
import purchaseDomainIfAvailable from './purchase-domain-if-available'
import maybeSetupDNSRecords from './maybe-setup-dns-records'
import verifyDomain from './verify-domain'
import getDomainNameservers from '../../util/domains/get-domain-nameservers'
import verifyDomain from '../../util/domains/verify-domain'
// Types and errors
import { Output, Now } from '../../util/types'
import * as Errors from '../../util/errors'
async function setupDomain(output: Output, now: Now, alias: string, contextName: string) {
const { domain, subdomain }: { domain: string, subdomain: string | null } = psl.parse(alias)
// In case the domain is avilable, we have to purchase
const purchased = await purchaseDomainIfAvailable(output, now, domain, contextName)
if (
(purchased instanceof Errors.UserAborted) ||
(purchased instanceof Errors.PaymentSourceNotFound) ||
(purchased instanceof Errors.DomainNotFound)
) {
return purchased
}
// Now the domain shouldn't be available and it might or might not belong to the user
const { domain }: { domain: string, subdomain: string | null } = psl.parse(alias)
const info = await getDomainInfo(now, domain, contextName)
if (info instanceof Errors.DomainPermissionDenied) {
return info
@@ -34,77 +21,87 @@ async function setupDomain(output: Output, now: Now, alias: string, contextName:
if (!info) {
output.debug(`Domain is unknown for ZEIT World`)
// If we have no info it means that it's an unknown domain. We have to check the
// nameservers to register and verify it as an external or non-external domain
const nameservers = await getDomainNameservers(now, domain)
if (nameservers instanceof Errors.DomainNameserversNotFound) {
return nameservers
}
output.log(
`Nameservers: ${nameservers && nameservers.length
? nameservers.map(ns => chalk.underline(ns)).join(', ')
: chalk.dim('none')}`
)
// If we find nameservers we have to try to add the domain
if (!(nameservers instanceof Errors.DomainNameserversNotFound)) {
output.log(
`Nameservers: ${nameservers && nameservers.length
? nameservers.map(ns => chalk.underline(ns)).join(', ')
: chalk.dim('none')}`
)
if (!nameservers.every(ns => ns.endsWith('.zeit.world'))) {
// If it doesn't have the nameserver pointing to now we have to create the
// domain knowing that it should be verified via a DNS TXT record.
const verified = await verifyDomain(now, alias, contextName, { isExternal: true })
const domainPointsToZeitWorld = nameservers.every(ns => ns.endsWith('.zeit.world'));
const verified = await verifyDomain(now, domain, contextName, { isExternal: !domainPointsToZeitWorld })
if (
(verified instanceof Errors.DomainNotVerified) ||
(verified instanceof Errors.DomainPermissionDenied) ||
(verified instanceof Errors.DomainVerificationFailed) ||
(verified instanceof Errors.NeedUpgrade)
(verified instanceof Errors.CDNNeedsUpgrade)
) {
return verified
} if (verified instanceof Errors.DomainVerificationFailed) {
// Verification fails when the domain is external so either its missing the TXT record
// or it's available to purchase, so we try to purchase it
const purchased = await purchaseDomainIfAvailable(output, now, alias, contextName)
if (
(purchased instanceof Errors.DomainNotFound) ||
(purchased instanceof Errors.InvalidCoupon) ||
(purchased instanceof Errors.MissingCreditCard) ||
(purchased instanceof Errors.PaymentSourceNotFound) ||
(purchased instanceof Errors.UnsupportedTLD) ||
(purchased instanceof Errors.UsedCoupon) ||
(purchased instanceof Errors.UserAborted)
) {
return purchased
} else if (!purchased) {
return verified
}
} else {
output.success(`Domain ${domain} added!`)
}
const domainInfo = await getDomainInfo(now, domain, contextName)
return domainInfo === null
? new Errors.DomainNotFound(domain)
: domainInfo
} else {
// We have to create the domain knowing that the nameservers are zeit.world
output.debug(`Detected ${chalk.bold(chalk.underline('zeit.world'))} nameservers! Setting up domain...`)
const verified = await verifyDomain(now, alias, contextName, { isExternal: false })
// If we couldn't find nameservers we try to purchase the domain
const purchased = await purchaseDomainIfAvailable(output, now, alias, contextName)
if (
(verified instanceof Errors.DomainNotVerified) ||
(verified instanceof Errors.DomainPermissionDenied) ||
(verified instanceof Errors.DomainVerificationFailed) ||
(verified instanceof Errors.NeedUpgrade)
(purchased instanceof Errors.DomainNotFound) ||
(purchased instanceof Errors.InvalidCoupon) ||
(purchased instanceof Errors.MissingCreditCard) ||
(purchased instanceof Errors.PaymentSourceNotFound) ||
(purchased instanceof Errors.UnsupportedTLD) ||
(purchased instanceof Errors.UsedCoupon) ||
(purchased instanceof Errors.UserAborted)
) {
return verified
} else {
output.success(`Domain ${domain} added!`)
return purchased
}
// Since it's pointing to our nameservers we can configure the DNS records
const result = await maybeSetupDNSRecords(output, now, domain, subdomain)
if ((result instanceof Errors.DNSPermissionDenied) || (result instanceof Errors.MissingDomainDNSRecords)) {
return result
}
const domainInfo = await getDomainInfo(now, domain, contextName)
return domainInfo === null
? new Errors.DomainNotFound(domain)
: domainInfo
}
} else {
// If we have records from the domain we have to try to verify in case it is not
// verified and from this point we can be sure about its verification
output.debug(`Domain is known for ZEIT World`)
if (!info.verified) {
const verified = await verifyDomain(now, alias, contextName, { isExternal: info.isExternal })
const verified = await verifyDomain(now, domain, contextName, { isExternal: info.isExternal })
if (
(verified instanceof Errors.DomainNotVerified) ||
(verified instanceof Errors.DomainPermissionDenied) ||
(verified instanceof Errors.DomainVerificationFailed) ||
(verified instanceof Errors.NeedUpgrade)
(verified instanceof Errors.CDNNeedsUpgrade)
) {
return verified
}
}
if (!info.isExternal) {
// Make sure that the DNS records are configured without messing with existent records
const result = await maybeSetupDNSRecords(output, now, domain, subdomain)
if ((result instanceof Errors.DNSPermissionDenied) || (result instanceof Errors.MissingDomainDNSRecords)) {
return result
}
}
return info
}
}

View File

@@ -1,8 +1,7 @@
// @flow
import chalk from 'chalk'
import wait from '../../../../util/output/wait'
import { Now, Output } from '../../util/types'
import type { HTTPChallengeInfo, AliasRecord, PathRule } from '../../util/types'
import type { AliasRecord, PathRule } from '../../util/types'
import * as Errors from '../../util/errors'
import createCertForAlias from './create-cert-for-alias'
import setupDomain from './setup-domain'
@@ -10,32 +9,29 @@ import setupDomain from './setup-domain'
const NOW_SH_REGEX = /\.now\.sh$/
async function upsertPathAlias(output: Output,now: Now, rules: PathRule[], alias: string, contextName: string) {
let httpChallengeInfo: HTTPChallengeInfo
let externalDomain = false
if (!NOW_SH_REGEX.test(alias)) {
output.log(`${chalk.bold(chalk.underline(alias))} is a custom domain.`)
const result = await setupDomain(output, now, alias, contextName)
const domainInfo = await setupDomain(output, now, alias, contextName)
if (
(result instanceof Errors.DNSPermissionDenied) ||
(result instanceof Errors.DomainNameserversNotFound) ||
(result instanceof Errors.DomainNotFound) ||
(result instanceof Errors.DomainNotVerified) ||
(result instanceof Errors.DomainPermissionDenied) ||
(result instanceof Errors.DomainVerificationFailed) ||
(result instanceof Errors.NeedUpgrade) ||
(result instanceof Errors.PaymentSourceNotFound) ||
(result instanceof Errors.UserAborted)
(domainInfo instanceof Errors.DNSPermissionDenied) ||
(domainInfo instanceof Errors.DomainNameserversNotFound) ||
(domainInfo instanceof Errors.DomainNotFound) ||
(domainInfo instanceof Errors.DomainNotVerified) ||
(domainInfo instanceof Errors.DomainPermissionDenied) ||
(domainInfo instanceof Errors.DomainVerificationFailed) ||
(domainInfo instanceof Errors.InvalidCoupon) ||
(domainInfo instanceof Errors.MissingCreditCard) ||
(domainInfo instanceof Errors.CDNNeedsUpgrade) ||
(domainInfo instanceof Errors.PaymentSourceNotFound) ||
(domainInfo instanceof Errors.UnsupportedTLD) ||
(domainInfo instanceof Errors.UsedCoupon) ||
(domainInfo instanceof Errors.UserAborted)
) {
return result
return domainInfo
}
// Maybe we get here an error of misconfigured shit
if (result instanceof Errors.MissingDomainDNSRecords) {
httpChallengeInfo = {
canSolveForRootDomain: !result.meta.forRootDomain,
canSolveForSubdomain: !result.meta.forSubdomain
}
}
externalDomain = domainInfo.isExternal
}
const cancelMessage = wait(`Updating path alias rules for ${alias}`)
@@ -52,8 +48,9 @@ async function upsertPathAlias(output: Output,now: Now, rules: PathRule[], alias
// If the certificate is missing we create it without expecting failures
// then we call back upsertPathAliasRules
if (error.code === 'cert_missing' || error.code === 'cert_expired') {
const cert = await createCertForAlias(output, now, alias, contextName, httpChallengeInfo)
const cert = await createCertForAlias(output, now, contextName, alias, !externalDomain)
if (
(cert instanceof Errors.CantSolveChallenge) ||
(cert instanceof Errors.DomainConfigurationError) ||
(cert instanceof Errors.DomainPermissionDenied) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
@@ -85,10 +82,6 @@ async function upsertPathAlias(output: Output,now: Now, rules: PathRule[], alias
}
if (error.status === 403) {
if (error.code === 'custom_domain_needs_upgrade') {
return new Errors.NeedUpgrade()
}
if (error.code === 'alias_in_use') {
console.log(error)
return new Errors.AliasInUse(alias)

View File

@@ -15,11 +15,6 @@ function validatePathAliasRules(location: string, rules: any) {
return new RulesFileValidationError(location, 'all rules must have a dest field')
}
}
const fallbackItem = rules.find((rule) => Object.keys(rule).length === 1 && rule.dest)
if (!fallbackItem) {
return new RulesFileValidationError(location, 'there must be at least one fallback destination url')
}
}
export default validatePathAliasRules

View File

@@ -1,64 +0,0 @@
// @flow
import psl from 'psl'
import retry from 'async-retry'
import wait from '../../../../util/output/wait'
import { Now } from '../../util/types'
import * as Errors from '../../util/errors'
type VerifyOptions = { isExternal: boolean }
type VerifyInfo = {
uid: string,
verified: boolean,
created: string
}
async function verifyDomain(now: Now, alias: string, contextName: string, opts: VerifyOptions) {
const cancelMessage = wait('Setting up and verifying the domain')
const { domain, subdomain } = psl.parse(alias)
try {
const { verified } = await updateVerification(now, domain, opts.isExternal)
cancelMessage()
if (verified === false) {
return new Errors.DomainNotVerified(domain)
}
} catch (error) {
cancelMessage()
if (error.status === 403) {
return error.code === 'custom_domain_needs_upgrade'
? new Errors.NeedUpgrade()
: new Errors.DomainPermissionDenied(domain, contextName)
}
if (error.status === 401 && error.code === 'verification_failed') {
return new Errors.DomainVerificationFailed(domain, subdomain, error.verifyToken)
}
if (error.status !== 409) {
// we can ignore the 409 errors since it means the domain
// is already setup
throw error
}
}
}
async function updateVerification(now: Now, domain: string, isExternal: boolean): Promise<VerifyInfo> {
return retry(async (bail) => {
try {
return await now.fetch('/domains', {
body: { name: domain, isExternal },
method: 'POST',
})
} catch (err) {
// retry in case the user has to setup a TXT record
if (err.code !== 'verification_failed') {
bail(err)
} else {
throw err
}
}
}, { retries: 5, maxTimeout: 8000 })
}
export default verifyDomain

View File

@@ -1,62 +0,0 @@
// @flow
import ms from 'ms'
import chalk from 'chalk'
import sleep from 'then-sleep'
import { tick } from '../../../../util/output/chars'
import elapsed from '../../../../util/output/elapsed'
import wait from '../../../../util/output/wait'
import { Output, Now } from '../../util/types'
import type { DeploymentScale } from '../../util/types'
import matchDeploymentScale from './match-deployment-scale'
async function waitForScale(output: Output, now: Now, deploymentId: string, scale: DeploymentScale) {
const checkInterval = 500
const timeout = ms('5m')
const start = Date.now()
let remainingMatches = new Set(Object.keys(scale))
let cancelWait = renderRemainingDCsWait(remainingMatches)
while (true) { // eslint-disable-line
if (start + timeout <= Date.now()) {
throw new Error('Timeout while verifying instance count (10m)');
}
// Get the matches for deployment scale args
const matches = await matchDeploymentScale(output, now, deploymentId, scale)
const newMatches = new Set([...remainingMatches].filter(dc => matches.has(dc)))
remainingMatches = new Set([...remainingMatches].filter(dc => !matches.has(dc)))
// When there are new matches we print and check if we are done
if (newMatches.size !== 0) {
if (cancelWait) {
cancelWait()
}
// Print the new matches that we got
for (const dc of newMatches) {
// $FlowFixMe
output.log(`${chalk.cyan(tick)} Scaled ${chalk.bold(dc)} (${matches.get(dc)} instance) ${elapsed(Date.now() - start)}`);
}
// If we are done return, otherwise put the spinner back
if (remainingMatches.size === 0) {
return
} else {
cancelWait = renderRemainingDCsWait(remainingMatches)
}
}
// Sleep for the given interval until the next poll
await sleep(checkInterval);
}
}
function renderRemainingDCsWait(remainingDcs) {
return wait(`Waiting for instances in ${
Array.from(remainingDcs).map(id => chalk.bold(id)).join(', ')
} to match constraints`)
}
export default waitForScale

View File

@@ -7,9 +7,7 @@ const ccValidator = require('credit-card')
// Utilities
const textInput = require('../../../../util/input/text')
const countries = require('../../util/billing/country-list')
const cardBrands = require('../../util/billing/card-brands')
const geocode = require('../../util/billing/geocode')
const success = require('../../../../util/output/success')
const wait = require('../../../../util/output/wait')
const { tick } = require('../../../../util/output/chars')
@@ -69,66 +67,6 @@ module.exports = async function({
placeholder: 'mm / yyyy',
middleware: expDateMiddleware,
validateValue: data => !ccValidator.isExpired(...data.split(' / '))
},
addressGroupLabel: `\n> ${chalk.bold('Enter your billing address')}`,
country: {
label: rightPad('Country', 12),
async autoComplete(value) {
for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) {
continue
}
if (country.startsWith(value)) {
return country.substr(value.length)
}
const lowercaseCountry = country.toLowerCase()
const lowercaseValue = value.toLowerCase()
if (lowercaseCountry.startsWith(lowercaseValue)) {
return lowercaseCountry.substr(value.length)
}
}
return false
},
validateValue: value => {
for (const country in countries) {
if (!Object.hasOwnProperty.call(countries, country)) {
continue
}
if (country.toLowerCase() === value.toLowerCase()) {
return true
}
}
return false
}
},
zipCode: {
label: rightPad('ZIP', 12),
validadeKeypress: data => data.trim().length > 0,
validateValue: data => data.trim().length > 0
},
state: {
label: rightPad('State', 12),
validateValue: data => data.trim().length > 0
},
city: {
label: rightPad('City', 12),
validateValue: data => data.trim().length > 0
},
address1: {
label: rightPad('Address', 12),
validateValue: data => data.trim().length > 0
}
}
@@ -137,11 +75,14 @@ module.exports = async function({
if (!Object.hasOwnProperty.call(state, key)) {
continue
}
const piece = state[key]
if (typeof piece === 'string') {
console.log(piece)
} else if (typeof piece === 'object') {
let result
try {
/* eslint-disable no-await-in-loop */
result = await textInput({
@@ -179,22 +120,6 @@ module.exports = async function({
let text = result.split(' / ')
text = text[0] + chalk.gray(' / ') + text[1]
process.stdout.write(`${chalk.cyan(tick)} ${piece.label}${text}\n`)
} else if (key === 'zipCode') {
const stopSpinner = wait(piece.label + result)
const addressInfo = await geocode({
country: state.country.value,
zipCode: result
})
if (addressInfo.state) {
state.state.initialValue = addressInfo.state
}
if (addressInfo.city) {
state.city.initialValue = addressInfo.city
}
stopSpinner()
process.stdout.write(
`${chalk.cyan(tick)} ${piece.label}${result}\n`
)
} else {
process.stdout.write(
`${chalk.cyan(tick)} ${piece.label}${result}\n`
@@ -209,6 +134,7 @@ module.exports = async function({
}
}
}
console.log('') // New line
const stopSpinner = wait('Saving card')
@@ -217,21 +143,19 @@ module.exports = async function({
name: state.name.value,
cardNumber: state.cardNumber.value,
ccv: state.ccv.value,
expDate: state.expDate.value,
country: state.country.value,
zipCode: state.zipCode.value,
state: state.state.value,
city: state.city.value,
address1: state.address1.value
expDate: state.expDate.value
})
stopSpinner()
if (clear) {
const linesToClear = state.error ? 15 : 14
process.stdout.write(ansiEscapes.eraseLines(linesToClear))
}
console.log(success(
`${state.cardNumber
.brand} ending in ${res.last4} was added to ${chalk.bold(
.brand || state.cardNumber.card.brand} ending in ${res.last4 || res.card.last4} was added to ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
))

View File

@@ -7,16 +7,16 @@ const ms = require('ms')
const plural = require('pluralize')
// Utilities
const { handleError, error } = require('../util/error')
const NowCreditCards = require('../util/credit-cards')
const indent = require('../util/indent')
const listInput = require('../../../util/input/list')
const success = require('../../../util/output/success')
const promptBool = require('../../../util/input/prompt-bool')
const info = require('../../../util/output/info')
const logo = require('../../../util/output/logo')
const addBilling = require('./billing/add')
const exit = require('../../../util/exit')
const { error } = require('../../util/error')
const NowCreditCards = require('../../util/credit-cards')
const indent = require('../../util/indent')
const listInput = require('../../../../util/input/list')
const success = require('../../../../util/output/success')
const promptBool = require('../../../../util/input/prompt-bool')
const info = require('../../../../util/output/info')
const logo = require('../../../../util/output/logo')
const addBilling = require('./add')
const exit = require('../../../../util/exit')
const help = () => {
console.log(`
@@ -57,7 +57,7 @@ let debug
let apiUrl
let subcommand
const main = async ctx => {
module.exports = async ctx => {
argv = mri(ctx.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
@@ -74,14 +74,14 @@ const main = async ctx => {
if (argv.help || !subcommand) {
help()
await exit(0)
return 2;
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
try {
await run({ token, sh })
return run({ token, sh })
} catch (err) {
if (err.userError) {
console.error(error(err.message))
@@ -89,36 +89,27 @@ const main = async ctx => {
console.error(error(`Unknown error: ${err.stack}`))
}
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
process.exit(1)
return 1;
}
}
// Builds a `choices` object that can be passesd to inquirer.prompt()
function buildInquirerChoices(cards) {
return cards.cards.map(card => {
return cards.sources.map(source => {
const _default =
card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : ''
const id = `${chalk.cyan(`ID: ${card.id}`)}${_default}`
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}`
source.id === cards.defaultSource ? ' ' + chalk.bold('(default)') : ''
const id = `${chalk.cyan(`ID: ${source.id}`)}${_default}`
const number = `${chalk.gray('#### ').repeat(3)}${source.last4 || source.card.last4}`
const str = [
id,
indent(card.name, 2),
indent(`${card.brand} ${number}`, 2)
indent(source.name || source.owner.name, 2),
indent(`${source.brand || source.card.brand} ${number}`, 2)
].join('\n')
return {
name: str, // Will be displayed by Inquirer
value: card.id, // Will be used to identify the answer
short: card.id // Will be displayed after the users answers
value: source.id, // Will be used to identify the answer
short: source.id // Will be displayed after the users answers
}
})
}
@@ -132,43 +123,27 @@ async function run({ token, sh: { currentTeam, user } }) {
case 'ls':
case 'list': {
let cards
try {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
return 1;
}
const text = cards.cards
.map(card => {
const text = cards.sources
.map(source => {
const _default =
card.id === cards.defaultCardId ? ' ' + chalk.bold('(default)') : ''
source.id === cards.defaultSource ? ' ' + chalk.bold('(default)') : ''
const id = `${chalk.gray('-')} ${chalk.cyan(
`ID: ${card.id}`
`ID: ${source.id}`
)}${_default}`
const number = `${chalk.gray('#### ').repeat(3)}${card.last4}`
let address = card.address_line1
if (card.address_line2) {
address += `, ${card.address_line2}.`
} else {
address += '.'
}
address += `\n${card.address_city}, `
if (card.address_state) {
address += `${card.address_state}, `
}
// Stripe is returning a two digit code for the country,
// but we want the full country name
address += `${card.address_zip}. ${card.address_country}`
const number = `${chalk.gray('#### ').repeat(3)}${source.last4 || source.card.last4}`
return [
id,
indent(card.name, 2),
indent(`${card.brand} ${number}`, 2),
indent(address, 2)
indent(source.name || source.owner.name, 2),
indent(`${source.brand || source.card.brand} ${number}`, 2)
].join('\n')
})
.join('\n\n')
@@ -176,7 +151,7 @@ async function run({ token, sh: { currentTeam, user } }) {
const elapsed = ms(new Date() - start)
console.log(
`> ${
plural('card', cards.cards.length, true)
plural('card', cards.sources.length, true)
} found under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed}]`)}`
@@ -191,7 +166,7 @@ async function run({ token, sh: { currentTeam, user } }) {
case 'set-default': {
if (args.length > 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
return 1;
}
const start = new Date()
@@ -201,12 +176,12 @@ async function run({ token, sh: { currentTeam, user } }) {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
return 1;
}
if (cards.cards.length === 0) {
if (cards.sources.length === 0) {
console.error(error('You have no credit cards to choose from'))
return exit(0)
return 0;
}
let cardId = args[0]
@@ -233,17 +208,19 @@ async function run({ token, sh: { currentTeam, user } }) {
const confirmation = await promptBool(label, {
trailing: '\n'
})
if (!confirmation) {
console.log(info('Aborted'))
break
}
const start = new Date()
await creditCards.setDefault(cardId)
const card = cards.cards.find(card => card.id === cardId)
const card = cards.sources.find(card => card.id === cardId)
const elapsed = ms(new Date() - start)
console.log(success(
`${card.brand} ending in ${card.last4} is now the default ${chalk.gray(
`${card.brand || card.card.brand} ending in ${card.last4 || card.card.last4} is now the default ${chalk.gray(
`[${elapsed}]`
)}`
))
@@ -258,7 +235,7 @@ async function run({ token, sh: { currentTeam, user } }) {
case 'remove': {
if (args.length > 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
return 1;
}
const start = new Date()
@@ -267,16 +244,16 @@ async function run({ token, sh: { currentTeam, user } }) {
cards = await creditCards.ls()
} catch (err) {
console.error(error(err.message))
return
return 1;
}
if (cards.cards.length === 0) {
if (cards.sources.length === 0) {
console.error(error(
`You have no credit cards to choose from to delete under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
))
return exit(0)
return 0;
}
let cardId = args[0]
@@ -310,24 +287,24 @@ async function run({ token, sh: { currentTeam, user } }) {
const start = new Date()
await creditCards.rm(cardId)
const deletedCard = cards.cards.find(card => card.id === cardId)
const remainingCards = cards.cards.filter(card => card.id !== cardId)
const deletedCard = cards.sources.find(card => card.id === cardId)
const remainingCards = cards.sources.filter(card => card.id !== cardId)
let text = `${deletedCard.brand} ending in ${deletedCard.last4} was deleted`
let text = `${deletedCard.brand || deletedCard.card.brand} ending in ${deletedCard.last4 || deletedCard.card.last4} was deleted`
// ${chalk.gray(`[${elapsed}]`)}
if (cardId === cards.defaultCardId) {
if (cardId === cards.defaultSource) {
if (remainingCards.length === 0) {
// The user deleted the last card in their account
text += `\n${chalk.yellow('Warning!')} You have no default card`
} else {
// We can't guess the current default card let's ask the API
const cards = await creditCards.ls()
const newDefaultCard = cards.cards.find(
const newDefaultCard = cards.sources.find(
card => card.id === cards.defaultCardId
)
text += `\n${newDefaultCard.brand} ending in ${newDefaultCard.last4} in now default for ${chalk.bold(
text += `\n${newDefaultCard.brand || newDefaultCard.card.brand} ending in ${newDefaultCard.last4 || newDefaultCard.card.last4} in now default for ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
}
@@ -356,8 +333,9 @@ async function run({ token, sh: { currentTeam, user } }) {
default:
console.error(error('Please specify a valid subcommand: ls | add | rm | set-default'))
help()
exit(1)
return 1;
}
creditCards.close()
// This is required, otherwise we get those weird zlib errors
return exit(0);
}

View File

@@ -1,377 +0,0 @@
#!/usr/bin/env node
// @flow
// Native
const path = require('path')
// Packages
const arg = require('arg')
const chalk = require('chalk')
const fs = require('fs-extra')
const ms = require('ms')
const plural = require('pluralize')
const psl = require('psl')
const table = require('text-table')
// Utilities
const { handleError } = require('../util/error')
const argCommon = require('../util/arg-common')()
const cmd = require('../../../util/output/cmd')
const createOutput = require('../../../util/output')
const elapsed = require('../../../util/output/elapsed')
const getContextName = require('../util/get-context-name')
const logo = require('../../../util/output/logo')
const Now = require('../util')
const strlen = require('../util/strlen')
const wait = require('../../../util/output/wait')
const help = () => {
console.log(`
${chalk.bold(`${logo} now certs`)} [options] <command>
${chalk.yellow('NOTE:')} This command is intended for advanced use only.
By default, Now manages your certificates automatically.
${chalk.dim('Commands:')}
ls Show all available certificates
add <cn>[, <cn>] Create a certificate for a domain
rm <id> Renew the certificate of a existing domain
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--crt ${chalk.bold.underline('FILE')} Certificate file
--key ${chalk.bold.underline('FILE')} Certificate key file
--ca ${chalk.bold.underline('FILE')} CA certificate chain file
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray(
''
)} Generate a certificate with the cnames "acme.com" and "www.acme.com"
${chalk.cyan(
'$ now certs add acme.com www.acme.com'
)}
${chalk.gray(
''
)} Remove a certificate
${chalk.cyan(
'$ now certs rm acme.com www.acme.com'
)}
`)
}
module.exports = async function main(ctx: any): Promise<number> {
let argv
try {
argv = arg(ctx.argv.slice(3), {
'--overwrite': Boolean,
'--crt': String,
'--key': String,
'--ca': String,
...argCommon
})
} catch (err) {
handleError(err)
return 1;
}
const apiUrl = ctx.apiUrl
const debugEnabled = argv['--debug']
const subcommand = argv._[0];
const output = createOutput({ debug: debugEnabled });
const { success, log, print, error } = output;
if (!subcommand) {
error(`${cmd('now cert <command>')} expects one command`)
help()
return 1;
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
const { currentTeam } = sh;
const contextName = getContextName(sh);
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
const args = argv._.slice(1)
const startTime = Date.now()
if (subcommand === 'ls' || subcommand === 'list') {
if (args.length !== 0) {
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs ls`')}`)
return 1;
}
// Get the list of certificates
const certs = sortByCn(await caught(getCerts(now)))
log(`${plural('certificate', certs.length, true)} found ${elapsed(Date.now() - startTime)} under ${chalk.bold(contextName)}`)
if (certs.length > 0) {
console.log(formatCertsTable(certs))
}
} else if (subcommand === 'add' || subcommand === 'create') {
if (argv['--overwrite']) {
error('Overwrite option is deprecated')
now.close();
return 1;
}
let cert
if (argv['--crt'] || argv['--key'] || argv['--ca']) {
if ((args.length !== 0) || (!argv['--crt'] || !argv['--key'] || !argv['--ca'])) {
error(
`Invalid number of arguments for a custom certificate entry. Usage: ${chalk.cyan(
'`now certs add --crt DOMAIN.CRT --key DOMAIN.KEY --ca CA.CRT`'
)}`
)
now.close();
return 1
}
// Read the files provided
const crt = readX509File(argv['--crt'])
const key = readX509File(argv['--key'])
const ca = readX509File(argv['--ca'])
// Create the certificate
const cancelWait = wait('Adding your custom certificate');
cert = await caught(createCustomCert(now, key, crt, ca))
cancelWait()
} else {
if (args.length < 1) {
error(
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now certs add <cn>[, <cn>]`'
)}`
)
now.close();
return 1
}
// Create the certificate
const cancelWait = wait(`Issuing a certificate for ${chalk.bold(args)}`);
cert = await caught(createCert(now, args))
cancelWait();
}
// Check for errors
if (cert instanceof Error) {
error(cert.message);
now.close();
return 1;
}
// Print success
const cns = chalk.bold(cert.cns.join(', '))
success(`Certificate entry for ${cns} ${chalk.gray(`(${cert.uid})`)} created ${elapsed(new Date() - startTime)}`)
} else if (subcommand === 'renew') {
error('Renewing certificates is deprecated, issue a new one.')
return 1
} else if (subcommand === 'rm' || subcommand === 'remove') {
if (args.length !== 1) {
error(
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now certs rm <id>`'
)}`
);
now.close();
return 1;
}
const id = args[0]
const cert = await getCertById(now, id)
if (!cert) {
error(`No certificate found by id or cn "${id}" under ${chalk.bold(contextName)}`)
now.close();
return 1;
}
const yes = await readConfirmation('The following certificate will be removed permanently', cert)
if (!yes) {
error('User abort');
now.close();
return 0;
}
await deleteCertById(now, id)
success(
`Certificate ${chalk.bold(
cert.cns.join(', ')
)} ${chalk.gray(`(${id})`)} removed ${elapsed(new Date() - startTime)}`
)
} else {
error('Please specify a valid subcommand: ls | add | rm')
now.close();
help();
return 2;
}
return now.close()
function readConfirmation(msg, cert) {
return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(cert.created)) + ' ago')
log(msg)
print(table([[cert.uid, chalk.bold(cert.cns.join(', ')), time]], {
align: ['l', 'r', 'l'],
hsep: ' '.repeat(6)
}).replace(/^(.*)/gm, ' $1') + '\n')
print(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`)
process.stdin
.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim().toLowerCase() === 'y')
})
.resume()
})
}
}
function caught (p) {
return new Promise(r => {
p.then(r).catch(r)
})
}
function readX509File(file) {
return fs.readFileSync(path.resolve(file), 'utf8')
}
async function getCertById(now, id) {
return (await getCerts(now)).filter(c => c.uid === id)[0]
}
async function getCerts(now) {
const { certs } = await now.fetch('/v3/now/certs')
return certs
}
async function createCert(now, cns) {
return now.fetch('/v3/now/certs', {
method: 'POST',
body: {
domains: cns
},
retry: {
maxTimeout: 90000,
minTimeout: 30000,
retries: 3
}
})
}
async function createCustomCert(now, key, cert, ca) {
return now.fetch('/v3/now/certs', {
method: 'PUT',
body: {
ca, cert, key
}
})
}
async function deleteCertById(now, id) {
return now.fetch(`/v3/now/certs/${id}`, {
method: 'DELETE',
})
}
/**
* This function sorts the list of certs by root domain changing *
* to 'wildcard' since that will allow psl get the root domain
* properly to make the comparison.
*/
function sortByCn(certsList) {
return certsList.concat().sort((a, b) => {
const domainA = psl.get(a.cns[0].replace('*', 'wildcard'))
const domainB = psl.get(b.cns[0].replace('*', 'wildcard'))
if (!domainA || !domainB) return 0;
return domainA.localeCompare(domainB)
})
}
function formatCertsTable(certsList) {
return table([
formatCertsTableHead(),
...formatCertsTableBody(certsList),
], {
align: ['l', 'l', 'r', 'c', 'r'],
hsep: ' '.repeat(2),
stringLength: strlen
}
).replace(/^(.*)/gm, ' $1') + '\n'
}
function formatCertsTableHead() {
return [
chalk.dim('id'),
chalk.dim('cns'),
chalk.dim('expiration'),
chalk.dim('renew'),
chalk.dim('age')
];
}
function formatCertsTableBody(certsList) {
const now = new Date();
return certsList.reduce((result, cert) => ([
...result,
...formatCert(now, cert)
]), [])
}
function formatCert(time, cert) {
return cert.cns.map((cn, idx) => (
(idx === 0)
? formatCertFirstCn(time, cert, cn, cert.cns.length > 1)
: formatCertNonFirstCn(cn, cert.cns.length > 1)
))
}
function formatCertFirstCn(time, cert, cn, multiple) {
return [
cert.uid,
formatCertCn(cn, multiple),
formatExpirationDate(new Date(cert.expiration)),
cert.autoRenew ? 'yes' : 'no',
chalk.gray(ms(time - new Date(cert.created))),
]
}
function formatExpirationDate(date) {
const diff = date - Date.now()
return diff < 0
? chalk.gray(ms(-diff) + ' ago')
: chalk.gray('in ' + ms(diff))
}
function formatCertNonFirstCn(cn, multiple) {
return ['', formatCertCn(cn, multiple), '', '', '']
}
function formatCertCn(cn, multiple) {
return multiple
? `${chalk.gray('-')} ${chalk.bold(cn)}`
: chalk.bold(cn)
}

View File

@@ -1,104 +0,0 @@
// @flow
import chalk from 'chalk'
import Now from '../../util'
import getContextName from '../../util/get-context-name'
import stamp from '../../../../util/output/stamp'
import wait from '../../../../util/output/wait'
import { CLIContext, Output } from '../../util/types'
import * as Errors from '../../util/errors'
import { handleDomainConfigurationError } from '../../util/error-handlers'
import type { CLICertsOptions } from '../../util/types'
import createCertFromFile from '../../util/certs/create-cert-from-file'
import createCertForCns from '../../util/certs/create-cert-for-cns'
async function add(ctx: CLIContext, opts: CLICertsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const { apiUrl } = ctx;
const contextName = getContextName(sh);
const addStamp = stamp()
let cert
const {
['--overwrite']: overwite,
['--debug']: debugEnabled,
['--crt']: crtPath,
['--key']: keyPath,
['--ca']: caPath,
} = opts;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
if (overwite) {
output.error('Overwrite option is deprecated')
now.close();
return 1;
}
if (crtPath || keyPath || caPath) {
if ((args.length !== 0) || (!crtPath || !keyPath || !caPath)) {
output.error(`Invalid number of arguments to create a custom certificate entry. Usage:`)
output.print(` ${chalk.cyan(`now certs add --crt <domain.crt> --key <domain.key> --ca <ca.crt>`)}\n`)
now.close();
return 1
}
// Create a custom certificate from the given file paths
cert = await createCertFromFile(now, keyPath, crtPath, caPath, contextName)
if (cert instanceof Errors.InvalidCert) {
output.error(`The provided certificate is not valid and can't be added.`)
return 1
} else if (cert instanceof Errors.DomainPermissionDenied) {
output.error(`You don't have permissions over domain ${chalk.underline(cert.meta.domain)} under ${chalk.bold(cert.meta.context)}.`)
return 1
}
} else {
if (args.length < 1) {
output.error(`Invalid number of arguments to create a custom certificate entry. Usage:`)
output.print(` ${chalk.cyan(`now certs add <cn>[, <cn>]`)}\n`)
now.close();
return 1
}
// Create the certificate from the given array of CNs
const cancelWait = wait(`Generating a certificate for ${chalk.bold(args.join(', '))}`);
cert = await createCertForCns(now, args, contextName)
cancelWait();
if (cert instanceof Errors.TooManyRequests) {
output.error(`Too many requests detected for ${cert.meta.api} API. Try again later.`)
return 1
} else if (cert instanceof Errors.TooManyCertificates) {
output.error(`Too many certificates already issued for exact set of domains: ${cert.meta.domains.join(', ')}`)
return 1
} else if (cert instanceof Errors.DomainValidationRunning) {
output.error(`There is a validation in course for ${chalk.underline(cert.meta.domain)}. Wait until it finishes.`)
return 1
} else if (cert instanceof Errors.DomainConfigurationError) {
handleDomainConfigurationError(output, cert)
return 1
} else if (cert instanceof Errors.CantGenerateWildcardCert) {
output.error(`Wildcard certificates are allowed only for domains in ${chalk.underline('zeit.world')}`)
return 1
} else if (cert instanceof Errors.DomainsShouldShareRoot) {
output.error(`All given common names should share the same root domain.`)
return 1
} else if (cert instanceof Errors.InvalidWildcardDomain) {
output.error(`Invalid domain ${chalk.underline(cert.meta.domain)}. Wildcard domains can only be followed by a root domain.`)
return 1
} else if (cert instanceof Errors.DomainPermissionDenied) {
output.error(`You don't have permissions over domain ${chalk.underline(cert.meta.domain)} under ${chalk.bold(cert.meta.context)}.`)
return 1
}
}
// Print success message
const cns = chalk.bold(cert.cns.join(', '))
output.success(`Certificate entry for ${cns} created ${addStamp()}`)
return 0
}
export default add

View File

@@ -9,7 +9,7 @@ import getSubcommand from '../../util/get-subcommand'
import logo from '../../../../util/output/logo'
import type { CLICertsOptions } from '../../util/types'
import add from './add'
import issue from './issue'
import ls from './ls'
import rm from './rm'
@@ -22,9 +22,9 @@ const help = () => {
${chalk.dim('Commands:')}
ls Show all available certificates
add <cn>[, <cn>] Create a certificate for a domain
rm <id or cn> Remove an available certificate
ls Show all available certificates
issue <cn> [<cn>] Issue a new certificate for a domain
rm <id> Remove a certificate by id
${chalk.dim('Options:')}
@@ -40,6 +40,7 @@ const help = () => {
'TOKEN'
)} Login token
-T, --team Set a custom team scope
--challenge-only Only show challenges needed to issue a cert
--crt ${chalk.bold.underline('FILE')} Certificate file
--key ${chalk.bold.underline('FILE')} Certificate key file
--ca ${chalk.bold.underline('FILE')} CA certificate chain file
@@ -51,7 +52,7 @@ const help = () => {
)} Generate a certificate with the cnames "acme.com" and "www.acme.com"
${chalk.cyan(
'$ now certs add acme.com www.acme.com'
'$ now certs issue acme.com www.acme.com'
)}
${chalk.gray(
@@ -66,9 +67,10 @@ const help = () => {
const COMMAND_CONFIG = {
add: ['add'],
issue: ['issue'],
ls: ['ls', 'list'],
renew: ['renew'],
rm: ['rm', 'remove'],
rm: ['rm', 'remove']
}
module.exports = async function main(ctx: any): Promise<number> {
@@ -76,10 +78,12 @@ module.exports = async function main(ctx: any): Promise<number> {
try {
argv = getArgs(ctx.argv.slice(2), {
'--challenge-only': Boolean,
'--overwrite': Boolean,
'--output': String,
'--crt': String,
'--key': String,
'--ca': String,
'--ca': String
})
} catch (err) {
handleError(err)
@@ -90,21 +94,24 @@ module.exports = async function main(ctx: any): Promise<number> {
help()
return 0
}
const output: Output = createOutput({ debug: argv['--debug'] })
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG)
switch (subcommand) {
case 'add':
return add(ctx, argv, args, output)
case 'issue':
return issue(ctx, argv, args, output)
case 'ls':
return ls(ctx, argv, args, output)
case 'rm':
return rm(ctx, argv, args, output)
case 'add':
output.error(`${chalk.cyan('now certs add')} is deprecated. Please use ${chalk.cyan('now certs issue <cn> <cns>')} instead`)
return 1
case 'renew':
output.error('Renewing certificates is deprecated, issue a new one.')
return 1
default:
output.error('Please specify a valid subcommand: ls | add | rm')
output.error('Please specify a valid subcommand: ls | issue | rm')
help()
return 2
}

View File

@@ -0,0 +1,165 @@
// @flow
import {parse} from 'psl'
import chalk from 'chalk'
import ms from 'ms'
import { CLIContext, Output } from '../../util/types'
import { handleDomainConfigurationError } from '../../util/error-handlers'
import * as Errors from '../../util/errors'
import dnsTable from '../../util/dns-table'
import getCnsFromArgs from '../../util/certs/get-cns-from-args'
import getContextName from '../../util/get-context-name'
import Now from '../../util'
import stamp from '../../../../util/output/stamp'
import type { CLICertsOptions } from '../../util/types'
import createCertForCns from '../../util/certs/create-cert-for-cns'
import createCertFromFile from '../../util/certs/create-cert-from-file'
import finishCertOrder from '../../util/certs/finish-cert-order'
import startCertOrder from '../../util/certs/start-cert-order'
export default async function issue(ctx: CLIContext, opts: CLICertsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const { apiUrl } = ctx;
const contextName = getContextName(sh);
const addStamp = stamp()
let cert
const {
['--challenge-only']: challengeOnly,
['--overwrite']: overwite,
['--debug']: debugEnabled,
['--crt']: crtPath,
['--key']: keyPath,
['--ca']: caPath,
} = opts;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
if (overwite) {
output.error('Overwrite option is deprecated')
now.close();
return 1;
}
if (crtPath || keyPath || caPath) {
if ((args.length !== 0) || (!crtPath || !keyPath || !caPath)) {
output.error(`Invalid number of arguments to create a custom certificate entry. Usage:`)
output.print(` ${chalk.cyan(`now certs issue --crt <domain.crt> --key <domain.key> --ca <ca.crt>`)}\n`)
now.close();
return 1
}
// Create a custom certificate from the given file paths
cert = await createCertFromFile(now, keyPath, crtPath, caPath, contextName)
if (cert instanceof Errors.InvalidCert) {
output.error(`The provided certificate is not valid and cannot be added.`)
return 1
} else if (cert instanceof Errors.DomainPermissionDenied) {
output.error(`You do not have permissions over domain ${chalk.underline(cert.meta.domain)} under ${chalk.bold(cert.meta.context)}.`)
return 1
}
// Print success message
output.success(`Certificate entry for ${chalk.bold(cert.cns.join(', '))} created ${addStamp()}`)
return 0;
}
if (args.length < 1) {
output.error(`Invalid number of arguments to create a custom certificate entry. Usage:`)
output.print(` ${chalk.cyan(`now certs add <cn>[, <cn>]`)}\n`)
now.close();
return 1
}
const cns = getCnsFromArgs(args)
// If the user specifies that he wants the challenge to be solved manually, we request the
// order, show the result challenges and finish immediately.
if (challengeOnly) {
return await runStartOrder(output, now, cns, contextName, addStamp);
}
// If the user does not specify anything, we try to fullfill a pending order that may exist
// and if it doesn't exist we try to issue the cert solving from the server
cert = await finishCertOrder(now, cns, contextName)
if (cert instanceof Errors.CertOrderNotFound) {
cert = await createCertForCns(now, cns, contextName)
}
if (cert instanceof Errors.CantSolveChallenge) {
output.error(`We could not solve the ${cert.meta.type} challenge for domain ${cert.meta.domain}.`)
if (cert.meta.type === 'dns-01') {
output.log(`The certificate provider could not resolve the required DNS record queries.`)
output.print(' Read more: https://err.sh/now-cli/cant-solve-challenge\n')
} else {
output.log(`The certificate provider could not resolve the HTTP queries for ${cert.meta.domain}.`)
output.print(` The DNS propagation may take a few minutes, please verify your settings:\n\n`)
output.print(dnsTable([['', 'ALIAS', 'alias.zeit.co']]) + '\n\n');
output.log(`Alternatively, you can solve DNS challenges manually after running:\n`);
output.print(` ${chalk.cyan(`now certs issue --challenge-only ${cns.join(' ')}`)}\n`);
output.print(' Read more: https://err.sh/now-cli/cant-solve-challenge\n')
}
return 1
} else if (cert instanceof Errors.TooManyRequests) {
output.error(`Too many requests detected for ${cert.meta.api} API. Try again in ${ms(cert.meta.retryAfter * 1000, { long: true })}.`)
return 1
} else if (cert instanceof Errors.TooManyCertificates) {
output.error(`Too many certificates already issued for exact set of domains: ${cert.meta.domains.join(', ')}`)
return 1
} else if (cert instanceof Errors.DomainValidationRunning) {
output.error(`There is a validation in course for ${chalk.underline(cert.meta.domain)}. Please wait for it to complete.`)
return 1
} else if (cert instanceof Errors.DomainConfigurationError) {
handleDomainConfigurationError(output, cert)
return 1
} else if (cert instanceof Errors.CantGenerateWildcardCert) {
return await runStartOrder(output, now, cns, contextName, addStamp, {fallingBack: true});
} else if (cert instanceof Errors.DomainsShouldShareRoot) {
output.error(`All given common names should share the same root domain.`)
return 1
} else if (cert instanceof Errors.InvalidWildcardDomain) {
output.error(`Invalid domain ${chalk.underline(cert.meta.domain)}. Wildcard domains can only be followed by a root domain.`)
return 1
} else if (cert instanceof Errors.DomainPermissionDenied) {
output.error(`You do not have permissions over domain ${chalk.underline(cert.meta.domain)} under ${chalk.bold(cert.meta.context)}.`)
return 1
}
output.success(`Certificate entry for ${chalk.bold(cert.cns.join(', '))} created ${addStamp()}`)
return 0;
}
async function runStartOrder(output: Output, now: Now, cns: string[], contextName: string, stamp: () => string, {fallingBack = false}: {fallingBack: boolean} = {}) {
const {challengesToResolve} = await startCertOrder(now, cns, contextName)
const pendingChallenges = challengesToResolve.filter(challenge => challenge.status === 'pending')
if (fallingBack) {
output.warn(`To generate a wildcard certificate for domain for an external domain you must solve challenges manually.`);
}
if (pendingChallenges.length === 0) {
output.log(`A certificate issuance for ${chalk.bold(cns.join(', '))} has been started ${stamp()}`)
output.print(` There are no pending challenges. Finish the issuance by running: \n`)
output.print(` ${chalk.cyan(`now certs issue ${cns.join(' ')}`)}\n`)
return 0;
}
output.log(`A certificate issuance for ${chalk.bold(cns.join(', '))} has been started ${stamp()}`)
output.print(` Add the following TXT records with your registrar to be able to the solve the DNS challenge:\n\n`)
const [header, ...rows] = dnsTable(pendingChallenges.map((challenge) => ([
parse(challenge.domain).subdomain ? `_acme-challenge.${parse(challenge.domain).subdomain}` : `_acme-challenge`,
'TXT',
challenge.value
]))).split('\n');
output.print(header + '\n');
process.stdout.write(rows.join('\n') + '\n\n')
output.log(`To issue the certificate once the records are added, run:`);
output.print(` ${chalk.cyan(`now certs issue ${cns.join(' ')}`)}\n`);
output.print(' Read more: https://err.sh/now-cli/solve-challenges-manually\n')
return 0
}

View File

@@ -18,7 +18,7 @@ async function ls(ctx: CLIContext, opts: CLICertsOptions, args: string[], output
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })

View File

@@ -1,42 +1,58 @@
#!/usr/bin/env node
//@flow
// Native
const { resolve, basename } = require('path')
// Packages
const Progress = require('progress')
const fs = require('fs-extra')
const ms = require('ms')
const bytes = require('bytes')
const chalk = require('chalk')
const mri = require('mri')
const dotenv = require('dotenv')
const { eraseLines } = require('ansi-escapes')
const { write: copy } = require('clipboardy')
const bytes = require('bytes')
const chalk = require('chalk')
const dotenv = require('dotenv')
const fs = require('fs-extra')
const inquirer = require('inquirer')
const executable = require('executable')
const { tick } = require('../../../util/output/chars')
const elapsed = require('../../../util/output/elapsed')
const sleep = require('then-sleep');
const mri = require('mri')
const ms = require('ms')
const plural = require('pluralize')
const Progress = require('progress')
// Utilities
const Now = require('../util')
const isELF = require('../util/is-elf')
const createOutput = require('../../../util/output')
const toHumanPath = require('../../../util/humanize-path')
const { handleError } = require('../util/error')
const readMetaData = require('../util/read-metadata')
const checkPath = require('../util/check-path')
const logo = require('../../../util/output/logo')
const cmd = require('../../../util/output/cmd')
const wait = require('../../../util/output/wait')
const stamp = require('../../../util/output/stamp')
const promptBool = require('../../../util/input/prompt-bool')
const promptOptions = require('../util/prompt-options')
const exit = require('../../../util/exit')
const { normalizeRegionsList, isValidRegionOrDcId } = require('../util/dcs')
const printEvents = require('../util/events')
const { handleError } = require('../../util/error')
const { tick } = require('../../../../util/output/chars')
const checkPath = require('../../util/check-path')
const cmd = require('../../../../util/output/cmd')
const createOutput = require('../../../../util/output')
const exit = require('../../../../util/exit')
const logo = require('../../../../util/output/logo')
const Now = require('../../util')
const uniq = require('../../util/unique-strings')
const promptBool = require('../../../../util/input/prompt-bool')
const promptOptions = require('../../util/prompt-options')
const readMetaData = require('../../util/read-metadata')
const toHumanPath = require('../../../../util/humanize-path')
import { Output } from '../../util/types'
import * as Errors from '../../util/errors'
import combineAsyncGenerators from '../../../../util/combine-async-generators'
import createDeploy from '../../util/deploy/create-deploy'
import dnsTable from '../../util/dns-table'
import eventListenerToGenerator from '../../../../util/event-listener-to-generator'
import formatLogCmd from '../../../../util/output/format-log-cmd'
import formatLogOutput from '../../../../util/output/format-log-output'
import getContextName from '../../util/get-context-name'
import getEventsStream from '../../util/deploy/get-events-stream'
import getInstanceIndex from '../../util/deploy/get-instance-index'
import getStateChangeFromPolling from '../../util/deploy/get-state-change-from-polling'
import joinWords from '../../../../util/output/join-words';
import normalizeRegionsList from '../../util/scale/normalize-regions-list'
import raceAsyncGenerators from '../../../../util/race-async-generators'
import regionOrDCToDc from '../../util/scale/region-or-dc-to-dc'
import stamp from '../../../../util/output/stamp'
import verifyDeploymentScale from '../../util/scale/verify-deployment-scale'
import zeitWorldTable from '../../util/zeit-world-table'
import type { Readable } from 'stream'
import type { NewDeployment, DeploymentEvent } from '../../util/types'
import type { CreateDeployError } from '../../util/deploy/create-deploy'
const mriOpts = {
string: ['name', 'alias', 'session-affinity', 'regions'],
@@ -46,15 +62,21 @@ const mriOpts = {
'debug',
'force',
'links',
'no-clipboard',
'C',
'clipboard',
'forward-npm',
'docker',
'npm',
'static',
'public'
],
default: {
C: false,
clipboard: true,
},
alias: {
env: 'e',
'build-env': 'b',
dotenv: 'E',
help: 'h',
debug: 'd',
@@ -62,7 +84,6 @@ const mriOpts = {
force: 'f',
links: 'l',
public: 'p',
'no-clipboard': 'C',
'forward-npm': 'N',
'session-affinity': 'S',
name: 'n',
@@ -79,8 +100,8 @@ const help = () => {
${chalk.dim('Cloud')}
deploy [path] Performs a deployment ${chalk.bold('(default)')}
ls | list [app] List deployments
rm | remove [id] Remove a deployment
ls | list [app] Lists deployments
rm | remove [id] Removes a deployment
ln | alias [id] [url] Configures aliases for deployments
domains [name] Manages your domain names
certs [cmd] Manages your SSL certificates
@@ -96,8 +117,9 @@ const help = () => {
upgrade | downgrade [plan] Upgrades or downgrades your plan
teams [team] Manages your teams
switch Switches between teams and your account
login Login into your account or creates a new one
logout Logout from your account
login Logs into your account or creates a new one
logout Logs out of your account
whoami Displays the currently logged in username
${chalk.dim('Options:')}
@@ -119,9 +141,10 @@ const help = () => {
-p, --public Deployment is public (${chalk.dim(
'`/_src`'
)} is exposed) [on for oss, off for premium]
-e, --env Include an env var (e.g.: ${chalk.dim(
-e, --env Include an env var during run time (e.g.: ${chalk.dim(
'`-e KEY=value`'
)}). Can appear many times.
-b, --build-env Similar to ${chalk.dim('`--env`')} but for build time only.
-E ${chalk.underline('FILE')}, --dotenv=${chalk.underline(
'FILE'
)} Include env vars from .env file. Defaults to '.env'
@@ -173,6 +196,7 @@ let deploymentName
let sessionAffinity
let log
let error
let warn
let debug
let note
let debugEnabled
@@ -190,6 +214,34 @@ let alwaysForwardNpm
// If the current deployment is a repo
const gitRepo = {}
// For `env` and `buildEnv`
const getNullFields = o => Object.keys(o).filter(k => o[k] === null)
const addProcessEnv = async (env) => {
let val
for (const key of Object.keys(env)) {
if (typeof env[key] !== 'undefined') continue
val = process.env[key]
if (typeof val === 'string') {
log(
`Reading ${chalk.bold(
`"${chalk.bold(key)}"`
)} from your env (as no value was specified)`
)
// Escape value if it begins with @
env[key] = val.replace(/^@/, '\\@')
} else {
error(
`No value specified for env ${chalk.bold(
`"${chalk.bold(key)}"`
)} and it was not found in your env.`
)
await exit(1)
return
}
}
}
const stopDeployment = async msg => {
handleError(msg)
await exit(1)
@@ -241,7 +293,7 @@ const promptForEnvFields = async list => {
}
// eslint-disable-next-line import/no-unassigned-import
require('../../../util/input/patch-inquirer')
require('../../../../util/input/patch-inquirer')
log('Please enter values for the following environment variables:')
const answers = await inquirer.prompt(questions)
@@ -283,19 +335,19 @@ async function main(ctx: any) {
deploymentName = argv.name
sessionAffinity = argv['session-affinity']
debugEnabled = argv.debug
clipboard = !argv['no-clipboard']
clipboard = argv.clipboard && !argv.C
forwardNpm = argv['forward-npm']
followSymlinks = !argv.links
wantsPublic = argv.public
regions = (argv.regions || '').split(',').map(s => s.trim()).filter(Boolean)
noVerify = argv['verify'] === false
apiUrl = ctx.apiUrl
const output = createOutput({ debug: debugEnabled })
// https://github.com/facebook/flow/issues/1825
// $FlowFixMe
isTTY = process.stdout.isTTY
quiet = !isTTY
const output = createOutput({ debug: debugEnabled })
;({ log, error, note, debug } = output)
;({ log, error, note, debug, warn } = output)
if (argv.h || argv.help) {
help()
@@ -304,27 +356,26 @@ async function main(ctx: any) {
const { authConfig: { credentials }, config: { sh } } = ctx
const { token } = credentials.find(item => item.provider === 'sh')
const contextName = getContextName(sh);
const config = sh
alwaysForwardNpm = config.forwardNpm
try {
return sync({ output, token, config, showMessage: true })
return sync({ contextName, output, token, config, firstRun: true, deploymentType: undefined })
} catch (err) {
await stopDeployment(err)
}
}
async function sync({ output, token, config: { currentTeam, user }, showMessage }) {
async function sync({ contextName, output, token, config: { currentTeam, user }, firstRun, deploymentType }) {
return new Promise(async (_resolve, reject) => {
const deployStamp = stamp()
let deployStamp = stamp()
const rawPath = argv._[0]
let meta
let deployment
let deploymentType
let deployment: NewDeployment | null = null
let isFile
let atlas = false
if (paths.length === 1) {
try {
@@ -333,13 +384,12 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
if (fsData.isFile()) {
isFile = true
deploymentType = 'static'
atlas = await isELF(paths[0]) && executable.checkMode(fsData.mode, fsData.gid, fsData.uid)
}
} catch (err) {
let repo
let isValidRepo = false
const { fromGit, isRepoPath, gitPathParts } = require('../util/git')
const { fromGit, isRepoPath, gitPathParts } = require('../../util/git')
try {
isValidRepo = isRepoPath(rawPath)
@@ -412,7 +462,7 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
await exit(1)
}
if (!quiet && showMessage) {
if (!quiet && firstRun) {
if (gitRepo.main) {
const gitRef = gitRepo.ref ? ` at "${chalk.bold(gitRepo.ref)}" ` : ''
@@ -478,64 +528,58 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
} = await readMeta(paths[0], deploymentName, deploymentType, sessionAffinity))
}
const nowConfig = meta.nowConfig
const nowConfig = meta.nowConfig || {}
const scaleFromConfig = getScaleFromConfig(nowConfig)
let scale = {}
let dcIds
let scale
if (regions.length) {
// ignore now.json if regions cli option exists
scale = {}
} else {
const _nowConfig = nowConfig || {}
regions = _nowConfig.regions || []
scale = _nowConfig.scale || {}
// If there are regions coming from the args and now.json warn about it
if (regions.length > 0 && getRegionsFromConfig(nowConfig).length > 0) {
warn(`You have regions defined from both args and now.json, using ${chalk.bold(regions.join(','))}`)
}
// get all the region or dc identifiers from the scale settings
const scaleKeys = Object.keys(scale)
for (const scaleKey of scaleKeys) {
if (!isValidRegionOrDcId(scaleKey)) {
error(
`The value "${scaleKey}" in \`scale\` settings is not a valid region or DC identifier`,
'deploy-invalid-dc'
)
await exit(1)
}
// If there are no regions from args, use config
if (regions.length === 0) {
regions = getRegionsFromConfig(nowConfig)
}
let dcIds = []
// Read scale and fail if we have both regions and scale
if (regions.length > 0 && Object.keys(scaleFromConfig).length > 0) {
error("Can't set both `regions` and `scale` options simultaneously", 'regions-and-scale-at-once')
await exit(1)
}
if (regions.length) {
if (Object.keys(scale).length) {
error(
"Can't set both `regions` and `scale` options simultaneously",
'regions-and-scale-at-once'
)
// If we have a regions list we use it to build scale presets
if (regions.length > 0) {
dcIds = normalizeRegionsList(regions)
if (dcIds instanceof Errors.InvalidRegionOrDCForScale) {
error(`The value "${dcIds.meta.regionOrDC}" is not a valid region or DC identifier`)
await exit(1)
return 1
} else if (dcIds instanceof Errors.InvalidAllForScale) {
error(`You can't use all in the regions list mixed with other regions`)
await exit(1)
return 1
}
try {
dcIds = normalizeRegionsList(regions)
} catch (err) {
if (err.code === 'INVALID_ID') {
error(
`The value "${err.id}" in \`--regions\` is not a valid region or DC identifier`,
'deploy-invalid-dc'
)
await exit(1)
} else if (err.code === 'INVALID_ALL') {
error('The region value "all" was used, but it cannot be used alongside other region or dc identifiers')
// Build the scale presets based on the given regions
scale = dcIds.reduce((result, dcId) => ({ ...result, [dcId]: {min: 0, max: 1}}), {})
} else if (Object.keys(scaleFromConfig).length > 0) {
// If we have no regions list we get it from the scale keys but we have to validate
// them becase we don't admin `all` in this scenario. Also normalize presets in scale.
for (const regionOrDc of Object.keys(scaleFromConfig)) {
const dc = regionOrDCToDc(regionOrDc)
if (dc === undefined) {
error(`The value "${regionOrDc}" in \`scale\` settings is not a valid region or DC identifier`, 'deploy-invalid-dc')
await exit(1)
return 1
} else {
throw err
scale[dc] = scaleFromConfig[regionOrDc]
}
}
for (const dcId of dcIds) {
scale[dcId] = { min: 0, max: 1 }
}
}
debug(`Scale presets for deploy: ${JSON.stringify(scale)}`)
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
let dotenvConfig
@@ -543,7 +587,7 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
if (argv.dotenv) {
dotenvOption = argv.dotenv
} else if (nowConfig && nowConfig.dotenv) {
} else if (nowConfig.dotenv) {
dotenvOption = nowConfig.dotenv
}
@@ -572,15 +616,40 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
const deploymentEnv = Object.assign(
{},
dotenvConfig,
parseEnv(nowConfig && nowConfig.env, null),
parseEnv(nowConfig.env, null),
parseEnv(argv.env, undefined)
)
// If there's any envs with `null` then prompt the user for the values
const askFor = Object.keys(deploymentEnv).filter(
key => deploymentEnv[key] === null
// Merge build env out of `build.env` from now.json, and `--build-env` args
const deploymentBuildEnv = Object.assign(
{},
parseEnv(nowConfig.build && nowConfig.build.env, null),
parseEnv(argv['build-env'], undefined)
)
Object.assign(deploymentEnv, await promptForEnvFields(askFor))
// If there's any envs with `null` then prompt the user for the values
const envNullFields = getNullFields(deploymentEnv)
const buildEnvNullFields = getNullFields(deploymentBuildEnv)
const userEnv = await promptForEnvFields(uniq([
...envNullFields,
...buildEnvNullFields
]).sort())
for (const key of envNullFields) {
deploymentEnv[key] = userEnv[key]
}
for (const key of buildEnvNullFields) {
deploymentBuildEnv[key] = userEnv[key]
}
// If there's any undefined values, then inherit them from this process
await addProcessEnv(deploymentEnv)
await addProcessEnv(deploymentBuildEnv)
// Put the `build.env` back onto the `nowConfig`
if (Object.keys(deploymentBuildEnv).length > 0) {
if (!nowConfig.build) nowConfig.build = {};
nowConfig.build.env = deploymentBuildEnv;
}
let secrets
const findSecret = async uidOrName => {
@@ -616,28 +685,6 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
let val = deploymentEnv[key]
if (val === undefined) {
if (key in process.env) {
log(
`Reading ${chalk.bold(
`"${chalk.bold(key)}"`
)} from your env (as no value was specified)`
)
// Escape value if it begins with @
if (process.env[key] != null) {
val = process.env[key].replace(/^@/, '\\@')
}
} else {
error(
`No value specified for env ${chalk.bold(
`"${chalk.bold(key)}"`
)} and it was not found in your env.`
)
await exit(1)
}
}
if (val[0] === '@') {
const uidOrName = val.substr(1)
const _secrets = await findSecret(uidOrName)
@@ -698,15 +745,38 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
scale,
wantsPublic,
sessionAffinity,
isFile,
atlas: atlas || (meta.hasNowJson && nowConfig && Boolean(nowConfig.atlas))
isFile
},
meta
)
deployment = await now.create(paths, createArgs)
deployStamp = stamp()
const firstDeployCall = await createDeploy(output, now, contextName, paths, createArgs)
if (
(firstDeployCall instanceof Errors.CantSolveChallenge) ||
(firstDeployCall instanceof Errors.CantGenerateWildcardCert) ||
(firstDeployCall instanceof Errors.DomainConfigurationError) ||
(firstDeployCall instanceof Errors.DomainNameserversNotFound) ||
(firstDeployCall instanceof Errors.DomainNotFound) ||
(firstDeployCall instanceof Errors.DomainNotVerified) ||
(firstDeployCall instanceof Errors.DomainPermissionDenied) ||
(firstDeployCall instanceof Errors.DomainsShouldShareRoot) ||
(firstDeployCall instanceof Errors.DomainValidationRunning) ||
(firstDeployCall instanceof Errors.DomainVerificationFailed) ||
(firstDeployCall instanceof Errors.InvalidWildcardDomain) ||
(firstDeployCall instanceof Errors.CDNNeedsUpgrade) ||
(firstDeployCall instanceof Errors.TooManyCertificates) ||
(firstDeployCall instanceof Errors.TooManyRequests)
) {
handleCreateDeployError(output, firstDeployCall)
await exit(1)
return
}
deployment = firstDeployCall
if (now.syncFileCount > 0) {
const uploadStamp = stamp();
await new Promise((resolve) => {
if (now.syncFileCount !== now.fileCount) {
debug(`Total files ${now.fileCount}, ${now.syncFileCount} changed`)
@@ -717,7 +787,7 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
? 's'
: ''}`
const bar = new Progress(
`> Upload [:bar] :percent :etas (${size}) [${syncCount}]`,
`${chalk.gray('>')} Upload [:bar] :percent :etas (${size}) [${syncCount}]`,
{
width: 20,
complete: '=',
@@ -727,16 +797,17 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
}
)
now.upload()
now.upload({ scale })
now.on('upload', ({ names, data }) => {
const amount = data.length
debug(`Uploaded: ${names.join(' ')} (${bytes(data.length)})`)
bar.tick(amount)
})
now.on('complete', () => resolve())
now.on('uploadProgress', progress => {
bar.tick(progress);
})
now.on('complete', resolve)
now.on('error', err => {
error('Upload failed')
@@ -744,7 +815,45 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
})
})
deployment = await now.create(paths, createArgs)
if (!quiet && syncCount) {
log(`Synced ${syncCount} (${bytes(now.syncAmount)}) ${uploadStamp()}`)
}
for (let i = 0; i < 4; i += 1) {
deployStamp = stamp()
const secondDeployCall = await createDeploy(output, now, contextName, paths, createArgs)
if (
(secondDeployCall instanceof Errors.CantSolveChallenge) ||
(secondDeployCall instanceof Errors.CantGenerateWildcardCert) ||
(secondDeployCall instanceof Errors.DomainConfigurationError) ||
(secondDeployCall instanceof Errors.DomainNameserversNotFound) ||
(secondDeployCall instanceof Errors.DomainNotFound) ||
(secondDeployCall instanceof Errors.DomainNotVerified) ||
(secondDeployCall instanceof Errors.DomainPermissionDenied) ||
(secondDeployCall instanceof Errors.DomainsShouldShareRoot) ||
(secondDeployCall instanceof Errors.DomainValidationRunning) ||
(secondDeployCall instanceof Errors.DomainVerificationFailed) ||
(secondDeployCall instanceof Errors.InvalidWildcardDomain) ||
(secondDeployCall instanceof Errors.CDNNeedsUpgrade) ||
(secondDeployCall instanceof Errors.TooManyCertificates) ||
(secondDeployCall instanceof Errors.TooManyRequests)
) {
handleCreateDeployError(output, secondDeployCall)
await exit(1)
return
}
if (now.syncFileCount === 0) {
deployment = secondDeployCall
break;
}
}
if (deployment === null) {
error('Uploading failed. Please try again.')
await exit(1)
return
}
}
} catch (err) {
if (err.code === 'plan_requires_public') {
@@ -786,13 +895,15 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
wantsPublic = true
sync({
contextName,
output,
token,
config: {
currentTeam,
user
},
showMessage: false
firstRun: false,
deploymentType
})
return
@@ -810,15 +921,14 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
}
await stopDeployment(err)
return 1;
}
const { url } = now
// $FlowFixMe
const dcs = (deploymentType !== 'static' && deployment.scale)
? ` (${chalk.bold(Object.keys(deployment.scale).join(', '))})`
: ''
if (isTTY) {
if (clipboard) {
try {
@@ -835,116 +945,80 @@ async function sync({ output, token, config: { currentTeam, user }, showMessage
process.stdout.write(url)
}
if (!quiet) {
if (syncCount) {
log(`Synced ${syncCount} (${bytes(now.syncAmount)}) ${deployStamp()}`)
if (deploymentType === 'static') {
if (deployment.readyState === 'INITIALIZING') {
// This static deployment requires a build, so show the logs
noVerify = true
} else {
if (!quiet) {
output.log(chalk`{cyan Deployment complete!}`)
}
await exit(0)
return
}
}
// Show build logs
if (deploymentType === 'static') {
if (!quiet) {
log(chalk`{cyan Deployment complete!}`)
}
await exit(0)
} else {
let cancelWait = () => {}
if (!quiet) {
cancelWait = wait('Initializing…')
}
// (We have to add this check for flow but it will never happen)
if (deployment !== null) {
try {
// If the created deployment is ready it was a deduping and we should exit
if (deployment.readyState !== 'READY') {
require('assert')(deployment) // mute linter
await printEvents(now, now.id, currentTeam, {
mode: 'deploy', onEvent: printEvent, onOpen: cancelWait, quiet, debugEnabled,
findOpts: { direction: 'forward', follow: true }
})
} catch (err) {
cancelWait()
throw err
}
const instanceIndex = getInstanceIndex()
const eventsStream = await maybeGetEventsStream(now, deployment)
const eventsGenerator = getEventsGenerator(now, contextName, deployment, eventsStream)
if (deployment) {
if (!noVerify) {
output.log(`Build completed`)
await waitForScale(output, now, deployment.deploymentId, deployment.scale)
for await (const event of eventsGenerator) {
// Stop when the deployment is ready
if (event.type === 'state-change' && event.payload.value === 'READY') {
output.log(`Build completed`);
break
}
// Stop then there is an error state
if (event.type === 'state-change' && event.payload.value === 'ERROR') {
output.error(`Build failed`);
await exit(1)
}
// For any relevant event we receive, print the result
if (event.type === 'build-start') {
output.log('Building…')
} else if (event.type === 'command') {
output.log(formatLogCmd(event.payload.text))
} else if (event.type === 'stdout' || event.type === 'stderr') {
formatLogOutput(event.payload.text).forEach(msg => output.log(msg))
}
}
if (!noVerify) {
output.log(`Verifying instantiation in ${joinWords(Object.keys(deployment.scale).map(dc => chalk.bold(dc)))}`)
const verifyStamp = stamp()
const verifyDCsGenerator = getVerifyDCsGenerator(output, now, deployment, eventsStream)
for await (const dcOrEvent of verifyDCsGenerator) {
if (dcOrEvent instanceof Errors.VerifyScaleTimeout) {
output.error(`Instance verification timed out (${ms(dcOrEvent.meta.timeout)})`)
output.log('Read more: https://err.sh/now-cli/verification-timeout')
await exit(1)
} else if (Array.isArray(dcOrEvent)) {
const [dc, instances] = dcOrEvent
output.log(`${chalk.cyan(tick)} Scaled ${plural('instance', instances, true)} in ${chalk.bold(dc)} ${verifyStamp()}`)
} else if (dcOrEvent && (dcOrEvent.type === 'stdout' || dcOrEvent.type === 'stderr')) {
const prefix = chalk.gray(`[${instanceIndex(dcOrEvent.payload.instanceId)}] `)
formatLogOutput(dcOrEvent.payload.text, prefix).forEach(msg => output.log(msg))
}
}
}
output.success(`Deployment ready!`)
}
output.success(`Deployment ready`)
await exit(0)
}
})
}
// TODO: refactor this funtion to use something similar in alias and scale
async function waitForScale(output, now, deploymentId, scale) {
const checkInterval = 500
const timeout = ms('5m')
const start = Date.now()
let remainingMatches = new Set(Object.keys(scale))
let cancelWait = renderRemainingDCsWait(Object.keys(scale))
while (true) { // eslint-disable-line
if (start + timeout <= Date.now()) {
throw new Error('Timeout while verifying instance count (10m)');
}
// Get the matches for deployment scale args
const instances = await getDeploymentInstances(now, deploymentId)
const matches = new Set(await getMatchingScalePresets(scale, instances, matchMinPresets))
const newMatches = new Set([...remainingMatches].filter(dc => matches.has(dc)))
remainingMatches = new Set([...remainingMatches].filter(dc => !matches.has(dc)))
// When there are new matches we print and check if we are done
if (newMatches.size !== 0) {
if (cancelWait) {
cancelWait()
}
// Print the new matches that we got
for (const dc of newMatches) {
// $FlowFixMe
output.log(`${chalk.cyan(tick)} Verified ${chalk.bold(dc)} (${instances[dc].instances.length}) ${elapsed(Date.now() - start)}`);
}
// If we are done return, otherwise put the spinner back
if (remainingMatches.size === 0) {
return null
} else {
cancelWait = renderRemainingDCsWait(Array.from(remainingMatches))
}
}
// Sleep for the given interval until the next poll
await sleep(checkInterval);
}
}
// TODO: reuse this function in alias and scale commands
function getMatchingScalePresets(scale, instances, predicate) {
return Object.keys(scale).reduce((result, dc) => {
return predicate(scale[dc], instances[dc])
? [...result, dc]
: result
}, [])
}
function renderRemainingDCsWait(remainingDcs) {
return wait(`Verifying instances in ${
remainingDcs.map(id => chalk.bold(id)).join(', ')
}`)
}
function matchMinPresets(scalePreset, instancesObj) {
const value = Math.max(1, scalePreset.min)
return instancesObj.instances.length >= value
}
async function getDeploymentInstances(now, deploymentId) {
return now.fetch(`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances?init=1`)
}
async function readMeta(
_path,
_deploymentName,
@@ -996,31 +1070,117 @@ async function readMeta(
}
}
function printEvent({ type, event, text }, callOnOpenOnce) {
if (event === 'build-start') {
callOnOpenOnce()
log('Building…')
return 1
} else
if ([ 'command', 'stdout', 'stderr' ].includes(type)) {
text = text.replace(/\n$/, '').replace(/^\n/, '')
callOnOpenOnce()
const lines = text.split('\n')
function getRegionsFromConfig(config = {}): Array<string> {
return config.regions || []
}
if (type === 'command') {
log(`${text}`)
} else if (type === 'stdout' || type === 'stderr') {
lines.forEach(v => {
// strip out the beginning `>` if there is one because
// `log()` prepends its own and we don't want `> >`
log(v.replace(/^> /, ''))
})
}
function getScaleFromConfig(config = {}): Object {
return config.scale || {}
}
return lines.length
async function maybeGetEventsStream(now: Now, deployment: NewDeployment) {
try {
return await getEventsStream(now, deployment.deploymentId, { direction: 'forward', follow: true })
} catch (error) {
return null
}
}
function getEventsGenerator(now: Now, contextName: string, deployment: NewDeployment, eventsStream: null | Readable): AsyncGenerator<DeploymentEvent, void, void> {
const stateChangeFromPollingGenerator = getStateChangeFromPolling(now, contextName, deployment.deploymentId, deployment.readyState)
if (eventsStream !== null) {
return combineAsyncGenerators(
eventListenerToGenerator('data', eventsStream),
stateChangeFromPollingGenerator
);
}
return 0
return stateChangeFromPollingGenerator
}
function getVerifyDCsGenerator(output: Output, now: Now, deployment: NewDeployment, eventsStream: Readable | null) {
const verifyDeployment = verifyDeploymentScale(output, now, deployment.deploymentId, deployment.scale)
return eventsStream
? raceAsyncGenerators(eventListenerToGenerator('data', eventsStream), verifyDeployment)
: verifyDeployment
}
function handleCreateDeployError<OtherError>(output: Output, error: CreateDeployError | OtherError): 1 | OtherError {
if (error instanceof Errors.CantGenerateWildcardCert) {
output.error(`Custom suffixes are only allowed for domains in ${chalk.underline('zeit.world')}`)
return 1
} else if (error instanceof Errors.CantSolveChallenge) {
if (error.meta.type === 'dns-01') {
output.error(`The certificate provider could not resolve the DNS queries for ${error.meta.domain}.`)
output.print(` This might happen to new domains or domains with recent DNS changes. Please retry later.\n`)
} else {
output.error(`The certificate provider could not resolve the HTTP queries for ${error.meta.domain}.`)
output.print(` The DNS propagation may take a few minutes, please verify your settings:\n\n`)
output.print(dnsTable([['', 'ALIAS', 'alias.zeit.co']]) + '\n');
}
return 1
} else if (error instanceof Errors.DomainConfigurationError) {
output.error(`We couldn't verify the propagation of the DNS settings for ${chalk.underline(error.meta.domain)}`)
if (error.meta.external) {
output.print(` The propagation may take a few minutes, but please verify your settings:\n\n`)
output.print(dnsTable([
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
]) + '\n');
} else {
output.print(` We configured them for you, but the propagation may take a few minutes.\n`)
output.print(` Please try again later.\n`)
}
return 1
} else if (error instanceof Errors.DomainNameserversNotFound) {
output.error(`Couldn't find nameservers for the domain ${chalk.underline(error.meta.domain)}`)
return 1
} else if (error instanceof Errors.DomainNotVerified) {
output.error(`The domain used as a suffix ${chalk.underline(error.meta.domain)} is not verified and can't be used as custom suffix.`)
return 1
} else if (error instanceof Errors.DomainPermissionDenied) {
output.error(`You don't have permissions to access the domain used as a suffix ${chalk.underline(error.meta.domain)}.`)
return 1
} else if (error instanceof Errors.DomainsShouldShareRoot) {
// this is not going to happen
return 1
} else if (error instanceof Errors.DomainValidationRunning) {
output.error(`There is a validation in course for ${chalk.underline(error.meta.domain)}. Wait until it finishes.`)
return 1
} else if (error instanceof Errors.DomainVerificationFailed) {
output.error(`We couldn't verify the domain ${chalk.underline(error.meta.domain)}.\n`)
output.print(` Please make sure that your nameservers point to ${chalk.underline('zeit.world')}.\n`)
output.print(` Examples: (full list at ${chalk.underline('https://zeit.world')})\n`)
output.print(zeitWorldTable() + '\n');
output.print(`\n As an alternative, you can add following records to your DNS settings:\n`)
output.print(dnsTable([
['_now', 'TXT', error.meta.token],
error.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [error.meta.subdomain, 'CNAME', 'alias.zeit.co']
], {extraSpace: ' '}) + '\n');
return 1
} else if (error instanceof Errors.InvalidWildcardDomain) {
// this should never happen
output.error(`Invalid domain ${chalk.underline(error.meta.domain)}. Wildcard domains can only be followed by a root domain.`)
return 1
} else if (error instanceof Errors.CDNNeedsUpgrade) {
output.error(`You can't add domains with CDN enabled from an OSS plan`)
return 1
} else if (error instanceof Errors.TooManyCertificates) {
output.error(`Too many certificates already issued for exact set of domains: ${error.meta.domains.join(', ')}`)
return 1
} else if (error instanceof Errors.TooManyRequests) {
output.error(`Too many requests detected for ${error.meta.api} API. Try again in ${ms(error.meta.retryAfter * 1000, { long: true })}.`)
return 1
} else if (error instanceof Errors.DomainNotFound) {
output.error(`The domain used as a suffix ${chalk.underline(error.meta.domain)} no longer exists. Please update or remove your custom suffix.`)
return 1
}
return error
}
module.exports = main

View File

@@ -0,0 +1 @@
module.exports = require('./deploy')

View File

@@ -1,351 +0,0 @@
#!/usr/bin/env node
// Packages
const chalk = require('chalk')
const mri = require('mri')
const ms = require('ms')
const table = require('text-table')
// Utilities
const DomainRecords = require('../util/domain-records')
const indent = require('../util/indent')
const strlen = require('../util/strlen')
const { handleError, error } = require('../util/error')
const exit = require('../../../util/exit')
const logo = require('../../../util/output/logo')
const help = () => {
console.log(`
${chalk.bold(
`${logo} now dns`
)} [options] <command>
${chalk.dim('Commands:')}
add [details] Add a new DNS entry (see below for examples)
rm [id] Remove a DNS entry using its ID
ls [domain] List all DNS entries for a domain
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('')} Add an A record for a subdomain
${chalk.cyan(
'$ now dns add <DOMAIN> <SUBDOMAIN> <A | AAAA | ALIAS | CNAME | TXT> <VALUE>'
)}
${chalk.cyan('$ now dns add zeit.rocks api A 198.51.100.100')}
${chalk.gray('')} Add an MX record (@ as a name refers to the domain)
${chalk.cyan(
`$ now dns add <DOMAIN> '@' MX <RECORD VALUE> <PRIORITY>`
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' MX mail.zeit.rocks 10`)}
${chalk.gray('')} Add an SRV record
${chalk.cyan(
'$ now dns add <DOMAIN> <NAME> SRV <PRIORITY> <WEIGHT> <PORT> <TARGET>'
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' SRV 10 0 389 zeit.party`)}
${chalk.gray('')} Add a CAA record
${chalk.cyan(
`$ now dns add <DOMAIN> <NAME> CAA '<FLAGS> <TAG> "<VALUE>"'`
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' CAA '0 issue "zeit.co"'`)}
`)
}
// Options
let argv
let debug
let apiUrl
let subcommand
const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
boolean: ['help', 'debug'],
alias: {
help: 'h',
debug: 'd'
}
})
argv._ = argv._.slice(1)
debug = argv.debug
apiUrl = ctx.apiUrl
subcommand = argv._[0]
if (argv.help || !subcommand) {
help()
await exit(0)
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
try {
await run({ token, sh })
} catch (err) {
handleError(err)
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
process.exit(1)
}
}
async function run({ token, sh: { currentTeam, user } }) {
const domainRecords = new DomainRecords({ apiUrl, token, debug, currentTeam })
const args = argv._.slice(1)
const start = Date.now()
if (subcommand === 'ls' || subcommand === 'list') {
if (args.length > 1) {
console.error(error(
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now dns ls [domain]`'
)}`
))
return exit(1)
}
const elapsed = ms(new Date() - start)
const res = await domainRecords.ls(args[0])
const text = []
let count = 0
res.forEach((records, domain) => {
count += records.length
if (records.length > 0) {
const cur = Date.now()
const header = [
['', 'id', 'name', 'type', 'value', 'aux', 'created'].map(s =>
chalk.dim(s)
)
]
const out = table(
header.concat(
records.map(record => {
const time = chalk.gray(
ms(cur - new Date(Number(record.created))) + ' ago'
)
const aux = (() => {
if (record.mxPriority !== undefined) return record.mxPriority
if (record.priority !== undefined) return record.priority
return ''
})()
return [
'',
record.id,
record.name,
record.type,
record.value,
aux,
time
]
})
),
{
align: ['l', 'r', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
)
text.push(`\n\n${chalk.bold(domain)}\n${indent(out, 2)}`)
}
})
console.log(
`> ${count} record${count === 1 ? '' : 's'} found ${chalk.gray(
`[${elapsed}]`
)} under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}`
)
console.log(text.join(''))
} else if (subcommand === 'add') {
const param = parseAddArgs(args)
if (!param) {
console.error(error(
`Invalid number of arguments. See: ${chalk.cyan(
'`now dns --help`'
)} for usage.`
))
return exit(1)
}
const record = await domainRecords.create(param.domain, param.data)
const elapsed = ms(new Date() - start)
console.log(
`${chalk.cyan('> Success!')} A new DNS record for domain ${chalk.bold(
param.domain
)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray(
`[${elapsed}]`
)} (${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)})`
)
} else if (subcommand === 'rm' || subcommand === 'remove') {
if (args.length !== 1) {
console.error(error(
`Invalid number of arguments. Usage: ${chalk.cyan('`now dns rm <id>`')}`
))
return exit(1)
}
const record = await domainRecords.getRecord(args[0])
if (!record) {
console.error(error('DNS record not found'))
return exit(1)
}
const yes = await readConfirmation(
record,
'The following record will be removed permanently \n'
)
if (!yes) {
console.error(error('User abort'))
return exit(0)
}
await domainRecords.delete(record.domain, record.id)
const elapsed = ms(new Date() - start)
console.log(
`${chalk.cyan('> Success!')} Record ${chalk.gray(
`${record.id}`
)} removed ${chalk.gray(`[${elapsed}]`)}`
)
} else {
console.error(error('Please specify a valid subcommand: ls | add | rm'))
help()
exit(1)
}
return domainRecords.close()
}
process.on('uncaughtException', err => {
handleError(err)
exit(1)
})
function parseAddArgs(args) {
if (!args || args.length < 4) {
return null
}
const domain = args[0]
const name = args[1] === '@' ? '' : args[1].toString()
const type = args[2]
const value = args[3]
if (!(domain && typeof name === 'string' && type)) {
return null
}
if (type === 'MX') {
if (args.length !== 5) {
return null
}
return {
domain,
data: {
name,
type,
value,
mxPriority: Number(args[4])
}
}
} else if (type === 'SRV') {
if (args.length !== 7) {
return null
}
return {
domain,
data: {
name,
type,
srv: {
priority: Number(value),
weight: Number(args[4]),
port: Number(args[5]),
target: args[6]
}
}
}
}
if (args.length !== 4) {
return null
}
return {
domain,
data: {
name,
type,
value
}
}
}
function readConfirmation(record, msg) {
return new Promise(resolve => {
const time = chalk.gray(
ms(new Date() - new Date(Number(record.created))) + ' ago'
)
const tbl = table(
[
[
record.id,
chalk.bold(
`${record.name.length > 0
? record.name + '.'
: ''}${record.domain} ${record.type} ${record.value} ${record.mxPriority
? record.mxPriority
: ''}`
),
time
]
],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
)
process.stdout.write(`> ${msg}`)
process.stdout.write(' ' + tbl + '\n')
process.stdout.write(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
)
process.stdin
.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim().toLowerCase() === 'y')
})
.resume()
})
}

View File

@@ -0,0 +1,102 @@
// @flow
import chalk from 'chalk'
import Now from '../../util'
import getContextName from '../../util/get-context-name'
import stamp from '../../../../util/output/stamp'
import addDNSRecord from '../../util/dns/add-dns-record'
import { DomainNotFound, DNSPermissionDenied } from '../../util/errors'
import { CLIContext, Output } from '../../util/types'
import type { CLIDNSOptions } from '../../util/types'
async function add(ctx: CLIContext, opts: CLIDNSOptions, args: string[], output: Output): Promise<number> { // eslint-disable-line
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const parsedParams = parseAddArgs(args)
if (!parsedParams) {
output.error(
`Invalid number of arguments. See: ${chalk.cyan(
'`now dns --help`'
)} for usage.`
)
return 1
}
const addStamp = stamp()
const {domain, data} = parsedParams
const record = await addDNSRecord(output, now, domain, data)
if (record instanceof DomainNotFound) {
output.error(`The domain ${domain} can't be found under ${
chalk.bold(contextName)
} ${chalk.gray(addStamp())}`)
return 1
} else if (record instanceof DNSPermissionDenied) {
output.error(`You don't have permissions to add records to domain ${domain} under ${
chalk.bold(contextName)
} ${chalk.gray(addStamp())}`)
return 1
} else {
console.log(
`${chalk.cyan('> Success!')} DNS record for domain ${chalk.bold(
domain
)} ${chalk.gray(`(${record.uid})`)} created under ${
chalk.bold(contextName)
} ${chalk.gray(addStamp())}`
)
}
return 0;
}
function parseAddArgs(args: string[]) {
if (!args || args.length < 4) {
return null
}
const domain = args[0]
const name = args[1] === '@' ? '' : args[1].toString()
const type = args[2]
const value = args[3]
if (!(domain && typeof name === 'string' && type)) {
return null
} else if (type === 'MX' && args.length === 5) {
return {
domain,
data: { name, type, value, mxPriority: Number(args[4]) }
}
} else if (type === 'SRV' && args.length === 7) {
return {
domain,
data: {
name,
type,
srv: {
priority: Number(value),
weight: Number(args[4]),
port: Number(args[5]),
target: args[6]
}
}
}
} else if (args.length === 4) {
return {
domain,
data: {
name,
type,
value
}
}
}
return null;
}
export default add

View File

@@ -0,0 +1,105 @@
// @flow
import chalk from 'chalk'
import createOutput from '../../../../util/output'
import getArgs from '../../util/get-args'
import getSubcommand from '../../util/get-subcommand'
import logo from '../../../../util/output/logo'
import { Output } from '../../util/types'
import { handleError } from '../../util/error'
import type { CLIDNSOptions } from '../../util/types'
import add from './add'
import ls from './ls'
import rm from './rm'
const help = () => {
console.log(`
${chalk.bold(
`${logo} now dns`
)} [options] <command>
${chalk.dim('Commands:')}
add [details] Add a new DNS entry (see below for examples)
rm [id] Remove a DNS entry using its ID
ls [domain] List all DNS entries for a domain
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('')} Add an A record for a subdomain
${chalk.cyan(
'$ now dns add <DOMAIN> <SUBDOMAIN> <A | AAAA | ALIAS | CNAME | TXT> <VALUE>'
)}
${chalk.cyan('$ now dns add zeit.rocks api A 198.51.100.100')}
${chalk.gray('')} Add an MX record (@ as a name refers to the domain)
${chalk.cyan(
`$ now dns add <DOMAIN> '@' MX <RECORD VALUE> <PRIORITY>`
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' MX mail.zeit.rocks 10`)}
${chalk.gray('')} Add an SRV record
${chalk.cyan(
'$ now dns add <DOMAIN> <NAME> SRV <PRIORITY> <WEIGHT> <PORT> <TARGET>'
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' SRV 10 0 389 zeit.party`)}
${chalk.gray('')} Add a CAA record
${chalk.cyan(
`$ now dns add <DOMAIN> <NAME> CAA '<FLAGS> <TAG> "<VALUE>"'`
)}
${chalk.cyan(`$ now dns add zeit.rocks '@' CAA '0 issue "zeit.co"'`)}
`)
}
const COMMAND_CONFIG = {
add: ['add'],
ls: ['ls', 'list'],
rm: ['rm', 'remove'],
}
module.exports = async function main(ctx: any): Promise<number> {
let argv: CLIDNSOptions;
try {
argv = getArgs(ctx.argv.slice(2), {});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help()
return 2;
}
const output: Output = createOutput({ debug: argv['--debug'] })
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG)
switch (subcommand) {
case 'add':
return add(ctx, argv, args, output);
case 'rm':
return rm(ctx, argv, args, output);
default:
return ls(ctx, argv, args, output);
}
}

View File

@@ -0,0 +1,91 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import plural from 'pluralize'
import Now from '../../util'
import getContextName from '../../util/get-context-name'
import getDNSRecords from '../../util/dns/get-dns-records'
import getDomainDNSRecords from '../../util/dns/get-domain-dns-records'
import stamp from '../../../../util/output/stamp'
import formatTable from '../../util/format-table'
import { CLIContext, Output } from '../../util/types'
import { DomainNotFound } from '../../util/errors'
import type { CLIDNSOptions, DNSRecord } from '../../util/types'
async function ls(ctx: CLIContext, opts: CLIDNSOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const [domainName] = args;
const lsStamp = stamp()
if (args.length > 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now dns ls [domain]`'
)}`
)
return 1
}
if (domainName) {
const records = await getDomainDNSRecords(output, now, domainName)
if (records instanceof DomainNotFound) {
output.error(`The domain ${domainName} can't be found under ${
chalk.bold(contextName)
} ${chalk.gray(lsStamp())}`)
return 1
}
output.log(
`${plural('Record', records.length, true)} found under ${
chalk.bold(contextName)
} ${chalk.gray(lsStamp())}`
)
console.log(getDNSRecordsTable([{domainName, records}]))
return 0
}
const dnsRecords = await getDNSRecords(output, now, contextName);
const nRecords = dnsRecords.reduce((p, r) => r.records.length + p, 0);
output.log(
`${plural('Record', nRecords, true)} found under ${
chalk.bold(contextName)
} ${chalk.gray(lsStamp())}`
)
console.log(getDNSRecordsTable(dnsRecords))
return 0
}
function getDNSRecordsTable(dnsRecords: Array<{domainName: string, records: DNSRecord[]}>): string {
return formatTable(
['', 'id', 'name', 'type', 'value', 'created'],
['l', 'r', 'l', 'l', 'l', 'l'],
dnsRecords.map(({domainName, records}) => ({
name: chalk.bold(domainName),
rows: records.map(record => getDNSRecordRow(domainName, record))
}))
)
}
function getDNSRecordRow(domainName: string, record: DNSRecord) {
const isSystemRecord = record.creator === 'system'
const createdAt = ms(Date.now() - new Date(Number(record.created))) + ' ago'
const priority = record.mxPriority || record.priority || null
return [
'',
!isSystemRecord ? record.id : '',
record.name,
record.type,
priority ? `${priority} ${record.value}` : record.value,
chalk.gray(isSystemRecord ? 'default' : createdAt),
]
}
export default ls

View File

@@ -0,0 +1,77 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import table from 'text-table'
import Now from '../../util'
import getContextName from '../../util/get-context-name'
import deleteDNSRecordById from '../../util/dns/delete-dns-record-by-id'
import getDNSRecordById from '../../util/dns/get-dns-record-by-id'
import stamp from '../../../../util/output/stamp'
import { CLIContext, Output } from '../../util/types'
import type { CLIDNSOptions, DNSRecord } from '../../util/types'
async function rm(ctx: CLIContext, opts: CLIDNSOptions, args: string[], output: Output): Promise<number> { // eslint-disable-line
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const [recordId] = args
if (args.length !== 1) {
output.error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns rm <id>`')}`)
return 1
}
const domainRecord = await getDNSRecordById(output, now, contextName, recordId)
if (!domainRecord) {
output.error('DNS record not found')
return 1
}
const {domainName, record} = domainRecord
const yes = await readConfirmation(output, 'The following record will be removed permanently', domainName, record)
if (!yes) {
output.error(`User aborted.`)
return 0;
}
const rmStamp = stamp()
await deleteDNSRecordById(output, now, contextName, domainName, record.id);
console.log(
`${chalk.cyan('> Success!')} Record ${chalk.gray(
`${record.id}`
)} removed ${chalk.gray(rmStamp())}`
)
return 0;
}
function readConfirmation(output: Output, msg: string, domainName: string, record: DNSRecord) {
return new Promise(resolve => {
output.log(msg)
output.print(table(
[getDeleteTableRow(domainName, record)],
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) }
).replace(/^(.*)/gm, ' $1') + '\n')
output.print(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`)
process.stdin.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim().toLowerCase() === 'y')
}).resume()
})
}
function getDeleteTableRow(domainName: string, record: DNSRecord) {
const recordName = `${record.name.length > 0 ? record.name + '.' : ''}${domainName}`;
return [
record.id,
chalk.bold(`${recordName} ${record.type} ${record.value} ${record.mxPriority || ''}`),
chalk.gray(ms(new Date() - new Date(Number(record.created))) + ' ago')
]
}
export default rm

View File

@@ -1,370 +0,0 @@
#!/usr/bin/env node
// Packages
const chalk = require('chalk')
const mri = require('mri')
const ms = require('ms')
const psl = require('psl')
const table = require('text-table')
const plural = require('pluralize')
// Utilities
const NowDomains = require('../util/domains')
const exit = require('../../../util/exit')
const logo = require('../../../util/output/logo')
const promptBool = require('../../../util/input/prompt-bool')
const strlen = require('../util/strlen')
const toHost = require('../util/to-host')
const { handleError, error } = require('../util/error')
const buy = require('./domains/buy')
const help = () => {
console.log(`
${chalk.bold(`${logo} now domains`)} [options] <command>
${chalk.dim('Commands:')}
ls Show all domains in a list
add [name] Add a new domain that you already own
rm [name] Remove a domain
buy [name] Buy a domain that you don't yet own
${chalk.dim('Options:')}
-h, --help Output usage information
-d, --debug Debug mode [off]
-e, --external Use external DNS server
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('')} Add a domain that you already own
${chalk.cyan(`$ now domains add ${chalk.underline('domain-name.com')}`)}
Make sure the domain's DNS nameservers are at least 2 of the
ones listed on ${chalk.underline('https://zeit.world')}.
${chalk.yellow('NOTE:')} Running ${chalk.dim(
'`now alias`'
)} will automatically register your domain
if it's configured with these nameservers (no need to ${chalk.dim(
'`domain add`'
)}).
${chalk.gray(
''
)} Add a domain using an external nameserver
${chalk.cyan('$ now domain add -e my-app.com')}
`)
}
// Options
let argv
let debug
let apiUrl
let subcommand
const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
string: ['coupon'],
boolean: ['help', 'debug', 'external'],
alias: {
help: 'h',
coupon: 'c',
debug: 'd',
external: 'e',
}
})
argv._ = argv._.slice(1)
debug = argv.debug
apiUrl = ctx.apiUrl
subcommand = argv._[0]
if (argv.help || !subcommand) {
help()
await exit(0)
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
try {
await run({ token, sh })
} catch (err) {
if (err.userError) {
console.error(error(err.message))
} else {
console.error(error(`Unknown error: ${err}\n${err.stack}`))
}
exit(1)
}
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
process.exit(1)
}
}
async function run({ token, sh: { currentTeam, user } }) {
const domain = new NowDomains({ apiUrl, token, debug, currentTeam })
const args = argv._.slice(1)
switch (subcommand) {
case 'ls':
case 'list': {
if (args.length !== 0) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const start_ = new Date()
const domains = await domain.ls()
domains.sort((a, b) => new Date(b.created) - new Date(a.created))
const current = new Date()
const header = [
['', 'domain', 'dns', 'verified', 'age'].map(s => chalk.dim(s))
]
const out =
domains.length === 0
? null
: table(
header.concat(
domains.map(domain => {
const ns = domain.isExternal ? 'external' : 'zeit.world'
const url = chalk.bold(domain.name)
const time = chalk.gray(
ms(current - new Date(domain.created))
)
return ['', url, ns, domain.verified, time]
})
),
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
)
const elapsed_ = ms(new Date() - start_)
console.log(
`> ${plural('domain', domains.length, true)} found under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)} ${chalk.gray(`[${elapsed_}]`)}`
)
if (out) {
console.log('\n' + out + '\n')
}
break
}
case 'rm':
case 'remove': {
if (args.length !== 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const _target = String(args[0])
if (!_target) {
const err = new Error('No domain specified')
err.userError = true
throw err
}
const _domains = await domain.ls()
const _domain = findDomain(_target, _domains)
if (!_domain) {
const err = new Error(
`Domain not found by "${_target}". Run ${chalk.dim(
'`now domains ls`'
)} to see your domains.`
)
err.userError = true
throw err
}
try {
const confirmation = (await readConfirmation(
domain,
_domain
)).toLowerCase()
if (confirmation !== 'y' && confirmation !== 'yes') {
console.log('\n> Aborted')
process.exit(0)
}
const start = new Date()
await domain.rm(_domain)
const elapsed = ms(new Date() - start)
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(_domain.name)} removed [${elapsed}]`
)
} catch (err) {
console.error(error(err))
exit(1)
}
break
}
case 'add':
case 'set': {
if (args.length !== 1) {
console.error(error('Invalid number of arguments'))
return exit(1)
}
const name = String(args[0])
if (!await promptBool(`Are you sure you want to add "${name}"?`)) {
return exit(1)
}
const parsedDomain = psl.parse(name)
if (parsedDomain.subdomain) {
const msg =
`You are adding "${name}" as a domain name which seems to contain a subdomain part "${parsedDomain.subdomain}".\n` +
' This is probably wrong unless you really know what you are doing.\n' +
` To add the root domain instead please run: ${chalk.cyan(
'now domain add ' +
(argv.external ? '-e ' : '') +
parsedDomain.domain
)}\n` +
` Continue adding "${name}" as a domain name?`
if (!await promptBool(msg)) {
return exit(1)
}
}
const start = new Date()
const { uid, code, created, verified } = await domain.add(
name,
argv.external
)
const elapsed = ms(new Date() - start)
if (created) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} added [${elapsed}]`
)
} else if (verified) {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} verified [${elapsed}]`
)
} else if (code === 'not_modified') {
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(name)
)} ${chalk.dim(`(${uid})`)} already exists [${elapsed}]`
)
} else {
console.log(
'> Verification required: Please rerun this command after some time'
)
}
break
}
case 'buy': {
await buy({
domains: domain,
args,
currentTeam,
user,
coupon: argv.coupon
})
break
}
default:
console.error(error('Please specify a valid subcommand: ls | add | rm'))
help()
exit(1)
}
domain.close()
}
async function readConfirmation(domain, _domain) {
return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(_domain.created)) + ' ago')
const tbl = table([[chalk.bold(_domain.name), time]], {
align: ['r', 'l'],
hsep: ' '.repeat(6)
})
process.stdout.write('> The following domain will be removed permanently\n')
process.stdout.write(' ' + tbl + '\n')
if (_domain.aliases.length > 0) {
process.stdout.write(
`> ${chalk.yellow('Warning!')} This domain's ` +
`${chalk.bold(
plural('alias', _domain.aliases.length, true)
)} ` +
`will be removed. Run ${chalk.dim('`now alias ls`')} to list them.\n`
)
}
if (_domain.certs.length > 0) {
process.stdout.write(
`> ${chalk.yellow('Warning!')} This domain's ` +
`${chalk.bold(
plural('certificate', _domain.certs.length, true)
)} ` +
`will be removed. Run ${chalk.dim('`now cert ls`')} to list them.\n`
)
}
process.stdout.write(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
)
process.stdin
.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim())
})
.resume()
})
}
function findDomain(val, list) {
return list.find(d => {
if (d.uid === val) {
if (debug) {
console.log(`> [debug] matched domain ${d.uid} by uid`)
}
return true
}
// Match prefix
if (d.name === toHost(val)) {
if (debug) {
console.log(`> [debug] matched domain ${d.uid} by name ${d.name}`)
}
return true
}
return false
})
}

View File

@@ -0,0 +1,150 @@
// @flow
import chalk from 'chalk'
import psl from 'psl'
import { CLIContext, Output } from '../../util/types'
import * as Errors from '../../util/errors'
import addDomain from '../../util/domains/add-domain'
import getDomainByName from '../../util/domains/get-domain-by-name'
import isDomainExternal from '../../util/domains/is-domain-external'
import updateDomain from '../../util/domains/update-domain.js'
import cmd from '../../../../util/output/cmd'
import dnsTable from '../../util/dns-table'
import getContextName from '../../util/get-context-name'
import getBooleanOptionValue from '../../util/get-boolean-option-value'
import Now from '../../util'
import promptBool from '../../../../util/input/prompt-bool'
import stamp from '../../../../util/output/stamp'
import type { CLIDomainsOptions } from '../../util/types'
import zeitWorldTable from '../../util/zeit-world-table'
export default async function add(ctx: CLIContext, opts: CLIDomainsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const cdnEnabled = getBooleanOptionValue(opts, 'cdn');
if (cdnEnabled instanceof Errors.ConflictingOption) {
output.error(`You can't use ${cmd('--cdn')} and ${cmd('--no-cdn')} in the same command`)
return 1
}
if (opts['--external'] && opts['--cdn']) {
output.error(`You cant enable the Now CDN for domains that are not pointing to zeit.world`);
return 1;
}
if (args.length !== 1) {
output.error(`${cmd('now domains rm <domain>')} expects one argument`)
return 1
}
// If the user is adding with subdomain, warn about what he's doing
const domainName = String(args[0])
const { domain, subdomain } = psl.parse(domainName)
if (!domain) {
output.error(`The domain '${domainName}' is not valid.`)
return 1;
}
// Do not allow to add domains with a subdomain
if (subdomain) {
output.error(
`You are adding '${domainName}' as a domain name containing a subdomain part '${subdomain}'\n` +
` This feature is deprecated, please add just the root domain: ${chalk.cyan(
'now domain add ' + (opts['--external'] ? '-e ' : '') + domain
)}`
)
return 1;
}
// Check if the domain exists and ask for confirmation if it doesn't
const domainInfo = await getDomainByName(output, now, contextName, domain);
if (!domainInfo && opts['--external'] && !await promptBool(`Are you sure you want to add "${domainName}"?`)) {
return 0
}
// Do not allow to switch from internal to external or viceversa if the domain is added
if (domainInfo && isDomainExternal(domainInfo) !== Boolean(opts['--external'])) {
const youWant = isDomainExternal(domainInfo) ? 'non-external' : 'external'
const youHave = isDomainExternal(domainInfo) ? 'external' : 'non-external'
output.error(
`You already have the domain ${domainInfo.name} as as ${youHave} domain.\n` +
` If you want to change the domain to be ${youWant}, please remove it and then add it back as an ${youWant} domain.`
)
return 1;
}
const addStamp = stamp()
if (!domainInfo || !domainInfo.verified) {
const addedDomain = await addDomain(now, domainName, contextName, opts['--external'], cdnEnabled)
if (addedDomain instanceof Errors.CDNNeedsUpgrade) {
output.error(`You can't add domains with CDN enabled from an OSS plan.`)
return 1
} else if (addedDomain instanceof Errors.DomainPermissionDenied) {
if (domainInfo) {
output.error(`You don't have permissions over domain ${chalk.underline(addedDomain.meta.domain)} under ${chalk.bold(addedDomain.meta.context)}.`)
return 1
} else {
output.error(`The domain ${chalk.underline(addedDomain.meta.domain)} is already registered by a different account.\n` +
` If this seems like a mistake, please contact us at support@zeit.co`)
return 1
}
} else if (addedDomain instanceof Errors.DomainVerificationFailed) {
output.error(`We couldn't verify the domain ${chalk.underline(addedDomain.meta.domain)}.\n`)
output.print(` Please make sure that your nameservers point to ${chalk.underline('zeit.world')}.\n`)
output.print(` Examples: (full list at ${chalk.underline('https://zeit.world')})\n`)
output.print(zeitWorldTable() + '\n');
output.print(`\n As an alternative, you can add following records to your DNS settings:\n`)
output.print(dnsTable([
['_now', 'TXT', addedDomain.meta.token],
addedDomain.meta.subdomain === null
? ['', 'ALIAS', 'alias.zeit.co']
: [addedDomain.meta.subdomain, 'CNAME', 'alias.zeit.co']
], {extraSpace: ' '}) + '\n');
return 1
} else if (addedDomain instanceof Errors.DomainAlreadyExists) {
output.error(`The domain exists already`);
return 1
} else {
const addedDomainInfo = await getDomainByName(output, now, contextName, domain);
if (addedDomainInfo) {
maybeWarnAboutUnverified(output, domainName, addedDomain.verified)
if (addedDomainInfo.cdnEnabled) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domainName))} was added and configured with CDN enabled. ${addStamp()}`)
return 0
}
}
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domainName))} was added. ${addStamp()}`)
return 0;
}
} else if (cdnEnabled !== undefined && domainInfo.cdnEnabled !== cdnEnabled) {
maybeWarnAboutUnverified(output, domainName, domainInfo.verified)
await updateDomain(now, domainName, cdnEnabled)
if (cdnEnabled) {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domainName))} was updated and configured with CDN enabled. ${addStamp()}`)
return 0
} else {
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(chalk.underline(domainName))} was updated and configured with CDN disabled. ${addStamp()}`)
return 0
}
} else {
maybeWarnAboutUnverified(output, domainName, domainInfo.verified)
console.log(`You requested to modify information for ${chalk.bold(chalk.underline(domainName))} that is already as requested; nothing was changed.`)
return 0
}
}
function maybeWarnAboutUnverified(output: Output, domainName: string, isVerified: boolean) {
if (!isVerified) {
output.warn(
`The domain was added but it could not be verified. If the domain doesn't point to ${chalk.bold('zeit.world')} nameservers\n` +
` please, remove the domain and add it back using ${cmd(`now domains add ${domainName} --external`)}.`
)
}
}

View File

@@ -1,123 +1,97 @@
// Packages
const { italic, bold } = require('chalk')
// @flow
import chalk from 'chalk'
import psl from 'psl'
// Utilities
const error = require('../../../../util/output/error')
const wait = require('../../../../util/output/wait')
const cmd = require('../../../../util/output/cmd')
const param = require('../../../../util/output/param')
const info = require('../../../../util/output/info')
const success = require('../../../../util/output/success')
const stamp = require('../../../../util/output/stamp')
const promptBool = require('../../../../util/input/prompt-bool')
const eraseLines = require('../../../../util/output/erase-lines')
const treatBuyError = require('../../util/domains/treat-buy-error')
const NowCreditCards = require('../../util/credit-cards')
const addBilling = require('../billing/add')
import { CLIContext, Output } from '../../util/types'
import * as Errors from '../../util/errors'
import cmd from '../../../../util/output/cmd'
import getContextName from '../../util/get-context-name'
import getDomainPrice from '../../util/domains/get-domain-price'
import getDomainStatus from '../../util/domains/get-domain-status'
import Now from '../../util'
import param from '../../../../util/output/param'
import promptBool from '../../../../util/input/prompt-bool'
import purchaseDomain from '../../util/domains/purchase-domain'
import stamp from '../../../../util/output/stamp'
import type { CLIDomainsOptions } from '../../util/types'
import wait from '../../../../util/output/wait'
module.exports = async function({ domains, args, currentTeam, user, coupon }) {
const name = args[0]
let elapsed
export default async function buy(ctx: CLIContext, opts: CLIDomainsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const contextName = getContextName(sh)
const { currentTeam } = sh
const { apiUrl } = ctx
if (!name) {
return console.error(error(`Missing domain name. Run ${cmd('now domains help')}`))
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const coupon = opts['--coupon']
const domainName = args[0]
if (!domainName) {
output.error(`Missing domain name. Run ${cmd('now domains --help')}`)
return 1
}
const nameParam = param(name)
let stopSpinner
let price
let period
let validCoupon
try {
if (coupon) {
stopSpinner = wait(`Validating coupon ${param(coupon)}`)
const creditCards = new NowCreditCards({
apiUrl: domains._agent._url,
token: domains._token,
debug: domains._debug,
currentTeam
})
const [couponInfo, { cards }] = await Promise.all([
domains.coupon(coupon),
creditCards.ls()
])
stopSpinner()
if (!couponInfo.isValid) {
return console.error(error(`The coupon ${param(coupon)} is invalid`))
}
if (!couponInfo.canBeUsed) {
return console.error(error(`The coupon ${param(coupon)} has already been used`))
}
validCoupon = true
if (cards.length === 0) {
console.log(info(
'You have no credit cards on file. Please add one in order to claim your free domain'
))
console.log(info(`Your card will ${bold('not')} be charged`))
await addBilling({
creditCards,
currentTeam,
user,
clear: true
})
}
}
elapsed = stamp()
stopSpinner = wait(`Checking availability for ${nameParam}`)
const json = await domains.price(name)
price = validCoupon ? 0 : json.price
period = json.period
} catch (err) {
stopSpinner()
return console.error(error(err.message))
const {domain: rootDomain, subdomain} = psl.parse(domainName)
if (subdomain || !rootDomain) {
output.error(`Invalid domain name "${domainName}". Run ${cmd('now domains --help')}`)
return 1
}
const available = await domains.status(name)
stopSpinner()
if (!available) {
console.error(error(
`The domain ${nameParam} is ${italic('unavailable')}! ${elapsed()}`
))
return
const availableStamp = stamp()
const domainPrice = await getDomainPrice(now, domainName, coupon)
if (domainPrice instanceof Errors.InvalidCoupon) {
output.error(`The coupon ${param(coupon)} is not valid.`)
return 1
} else if (domainPrice instanceof Errors.UsedCoupon) {
output.error(`The coupon ${param(coupon)} has already been used.`)
return 1
} if (domainPrice instanceof Errors.UnsupportedTLD) {
output.error(`The TLD for ${param(domainName)} is not supported.`)
return 1
}
const periodMsg = `${period}yr${period > 1 ? 's' : ''}`
console.log(info(
`The domain ${nameParam} is ${italic('available')} to buy under ${bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}! ${elapsed()}`
))
const confirmation = await promptBool(
`Buy now for ${bold(`$${price}`)} (${periodMsg})?`
)
eraseLines(1)
if (!confirmation) {
return console.log(info('Aborted'))
if (domainPrice instanceof Errors.MissingCreditCard) {
output.print('You have no credit cards on file. Please add one in order to claim your free domain')
output.print(`Your card will ${chalk.bold('not')} be charged`)
return 1
}
stopSpinner = wait('Purchasing')
elapsed = stamp()
try {
await domains.buy({ name, coupon, expectedPrice: price })
} catch (err) {
stopSpinner()
return treatBuyError(err)
if (!(await getDomainStatus(now, domainName)).available) {
output.error(`The domain ${param(domainName)} is ${chalk.underline('unavailable')}! ${availableStamp()}`)
return 1
}
stopSpinner()
const {period, price} = domainPrice
output.log(`The domain ${param(domainName)} is ${chalk.underline('available')} to buy under ${chalk.bold(contextName)}! ${availableStamp()}`)
if (!await promptBool(`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${period > 1 ? 's' : ''}`})?`)) {
return 0
}
console.log(success(`Domain ${nameParam} purchased ${elapsed()}`))
console.log(info(
`You may now use your domain as an alias to your deployments. Run ${cmd(
'now alias --help'
)}`
))
const purchaseStamp = stamp()
const stopPurchaseSpinner = wait('Purchasing')
const buyResult = await purchaseDomain(output, now, domainName, coupon, price)
stopPurchaseSpinner()
if (buyResult instanceof Errors.InvalidDomain) {
output.error(`The domain ${buyResult.meta.domain} is not valid.`)
return 1
} else if (buyResult instanceof Errors.DomainNotAvailable) {
output.error(`The domain ${buyResult.meta.domain} is not available.`)
return 1
} else if (buyResult instanceof Errors.DomainServiceNotAvailable) {
output.error(`The domain purchase service is not available. Please try again later.`)
return 1
} else if (buyResult instanceof Errors.UnexpectedDomainPurchaseError) {
output.error(`An unexpected error happened while performing the purchase.`)
return 1
} else if (buyResult instanceof Errors.PremiumDomainForbidden) {
output.error(`A coupon cannot be used to register a premium domain.`)
return 1
}
console.log(`${chalk.cyan('> Success!')} Domain ${param(domainName)} purchased ${purchaseStamp()}`)
output.note(`You may now use your domain as an alias to your deployments. Run ${cmd('now alias --help')}`)
return 0
}

View File

@@ -0,0 +1,110 @@
// @flow
import chalk from 'chalk'
import { handleError } from '../../util/error'
import { Output } from '../../util/types'
import createOutput from '../../../../util/output'
import getArgs from '../../util/get-args'
import getSubcommand from '../../util/get-subcommand'
import logo from '../../../../util/output/logo'
import type { CLIDomainsOptions } from '../../util/types'
import add from './add'
import buy from './buy'
import ls from './ls'
import rm from './rm'
const help = () => {
console.log(`
${chalk.bold(`${logo} now domains`)} [options] <command>
${chalk.dim('Commands:')}
ls Show all domains in a list
add [name] Add a new domain that you already own
rm [name] Remove a domain
buy [name] Buy a domain that you don't yet own
${chalk.dim('Options:')}
-h, --help Output usage information
-d, --debug Debug mode [off]
-e, --external Use external DNS server
--cdn Enable CDN support for the specified domain
--no-cdn Disable CDN support for the specified domain, if it was previously enabled
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('')} Add a domain that you already own
${chalk.cyan(`$ now domains add ${chalk.underline('domain-name.com')}`)}
Make sure the domain's DNS nameservers are at least 2 of the
ones listed on ${chalk.underline('https://zeit.world')}.
${chalk.yellow('NOTE:')} Running ${chalk.dim(
'`now alias`'
)} will automatically register your domain
if it's configured with these nameservers (no need to ${chalk.dim(
'`domain add`'
)}).
${chalk.gray(
''
)} Add a domain using an external nameserver
${chalk.cyan('$ now domain add -e my-app.com')}
`)
}
const COMMAND_CONFIG = {
add: ['add'],
buy: ['buy'],
ls: ['ls', 'list'],
rm: ['rm', 'remove'],
}
module.exports = async function main(ctx: any): Promise<number> {
let argv: CLIDomainsOptions;
try {
argv = getArgs(ctx.argv.slice(2), {
'--cdn': Boolean,
'--no-cdn': Boolean,
'--coupon': String,
'--external': Boolean,
'-c': '--coupon',
'-e': '--external'
})
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help()
return 2;
}
const output: Output = createOutput({ debug: argv['--debug'] })
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG)
switch (subcommand) {
case 'add':
return add(ctx, argv, args, output);
case 'buy':
return buy(ctx, argv, args, output);
case 'rm':
return rm(ctx, argv, args, output);
default:
return ls(ctx, argv, args, output);
}
}

View File

@@ -0,0 +1,64 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import plural from 'pluralize'
import table from 'text-table'
import { CLIContext, Output } from '../../util/types'
import getContextName from '../../util/get-context-name'
import getDomains from '../../util/domains/get-domains'
import isDomainExternal from '../../util/domains/is-domain-external'
import Now from '../../util'
import stamp from '../../../../util/output/stamp'
import strlen from '../../util/strlen'
import type { CLIDomainsOptions, Domain } from '../../util/types'
async function ls(ctx: CLIContext, opts: CLIDomainsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const lsStamp = stamp()
if (args.length !== 0) {
output.error(`Invalid number of arguments. Usage: ${chalk.cyan('`now domains ls`')}`)
return 1;
}
const domains = await getDomains(output, now, contextName);
output.log(`${plural('domain', domains.length, true)} found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}\n`)
if (domains.length > 0) {
console.log(formatDomainsTable(domains))
}
return 0;
}
function formatDomainsTable(domains: Domain[]) {
const current = new Date();
return table(
[
['', 'domain', 'dns', 'verified', 'cdn', 'age'].map(s => chalk.dim(s)),
...domains.map(domain => {
const cdnEnabled = domain.cdnEnabled || false
const ns = isDomainExternal(domain) ? 'external' : 'zeit.world'
const url = chalk.bold(domain.name)
const time = chalk.gray(
ms(current - new Date(domain.created))
)
return ['', url, ns, domain.verified, cdnEnabled, time]
})
],
{
align: ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
}
)
}
export default ls;

View File

@@ -0,0 +1,104 @@
// @flow
import chalk from 'chalk'
import ms from 'ms'
import plural from 'pluralize'
import table from 'text-table'
import { CLIContext, Output } from '../../util/types'
import cmd from '../../../../util/output/cmd'
import getContextName from '../../util/get-context-name'
import Now from '../../util'
import stamp from '../../../../util/output/stamp'
import type { CLIDomainsOptions, Certificate, Domain } from '../../util/types'
import deleteCertById from '../../util/certs/delete-cert-by-id';
import getCertsForDomain from '../../util/certs/get-certs-for-domain';
import getDomainByName from '../../util/domains/get-domain-by-name'
import removeAliasById from '../../util/alias/remove-alias-by-id'
import removeDomainByName from '../../util/domains/remove-domain-by-name'
async function rm(ctx: CLIContext, opts: CLIDomainsOptions, args: string[], output: Output): Promise<number> {
const {authConfig: { credentials }, config: { sh }} = ctx
const { currentTeam } = sh;
const contextName = getContextName(sh);
const { apiUrl } = ctx;
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: opts['--debug'], currentTeam })
const [domainIdOrName] = args
if (!domainIdOrName) {
output.error(`${cmd('now domains rm <domain>')} expects one argument`)
return 1
}
if (args.length !== 1) {
output.error(`Invalid number of arguments. Usage: ${chalk.cyan('`now alias rm <alias>`')}`)
return 1
}
const domain = await getDomainByName(output, now, contextName, domainIdOrName)
if (!domain) {
output.error(`Domain not found by "${domainIdOrName}" under ${chalk.bold(contextName)}`)
output.log(`Run ${cmd('now domains ls')} to see your domains.`)
return 1;
}
const certs = await getCertsForDomain(output, now, domain.name)
if (!opts['--yes'] && !(await confirmDomainRemove(output, domain, certs))) {
output.log('Aborted')
return 0
}
const removeStamp = stamp();
output.debug(`Removing aliases`)
for (const aliasId of domain.aliases) {
await removeAliasById(now, aliasId)
}
output.debug(`Removing certs`)
for (const cert of certs) {
await deleteCertById(output, now, cert.uid);
}
output.debug(`Removing domain`)
await removeDomainByName(output, now, domain.name);
console.log(`${chalk.cyan('> Success!')} Domain ${chalk.bold(domain.name)} removed ${removeStamp()}`)
return 0;
}
async function confirmDomainRemove(output: Output, domain: Domain, certs: Certificate[]) {
return new Promise(resolve => {
const time = chalk.gray(ms(new Date() - new Date(domain.created)) + ' ago')
const tbl = table([[chalk.bold(domain.name), time]], {
align: ['r', 'l'],
hsep: ' '.repeat(6)
})
output.log(`The following domain will be removed permanently`)
output.print(` ${tbl}\n`)
if (domain.aliases.length > 0) {
output.warn(
`This domain's ${chalk.bold(plural('alias', domain.aliases.length, true))
} will be removed. Run ${chalk.dim('`now alias ls`')} to list them.`
)
}
if (certs.length > 0) {
output.warn(
`This domain's ${chalk.bold(plural('certificate', certs.length, true))
} will be removed. Run ${chalk.dim('`now cert ls`')} to list them.`
)
}
output.print(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`)
process.stdin.on('data', d => {
process.stdin.pause()
resolve(d.toString().trim().toLowerCase() === 'y')
}).resume()
})
}
export default rm;

View File

@@ -2,7 +2,6 @@
// Packages
const chalk = require('chalk')
const arg = require('arg')
const table = require('text-table')
// Utilities
@@ -11,12 +10,13 @@ const createOutput = require('../../../util/output')
const Now = require('../util/')
const logo = require('../../../util/output/logo')
const elapsed = require('../../../util/output/elapsed')
const argCommon = require('../util/arg-common')()
const wait = require('../../../util/output/wait')
const { handleError } = require('../util/error')
const strlen = require('../util/strlen')
const getContextName = require('../util/get-context-name')
import getArgs from '../util/get-args'
const STATIC = 'STATIC'
const help = () => {
@@ -50,9 +50,7 @@ module.exports = async function main (ctx: any): Promise<number> {
let argv;
try {
argv = arg(ctx.argv.slice(3), {
...argCommon
})
argv = getArgs(ctx.argv.slice(2))
} catch (err) {
handleError(err)
return 1;
@@ -69,9 +67,9 @@ module.exports = async function main (ctx: any): Promise<number> {
const { print, log, error } = output;
// extract the first parameter
id = argv._[0]
id = argv._[1]
if (argv._.length !== 1) {
if (argv._.length !== 2) {
error(`${cmd('now inspect <url>')} expects exactly one argument`)
help();
return 1;
@@ -118,6 +116,7 @@ module.exports = async function main (ctx: any): Promise<number> {
print(` ${chalk.dim('name')}\t${deployment.name}\n`)
print(` ${chalk.dim('state')}\t${stateString(deployment.state)}\n`)
print(` ${chalk.dim('type')}\t${deployment.type}\n`)
print(` ${chalk.dim('affinity')}\t${deployment.sessionAffinity}\n`)
print(` ${chalk.dim('url')}\t\t${deployment.url}\n`)
print(` ${chalk.dim('created')}\t${new Date(deployment.created)} ${elapsed(Date.now() - deployment.created)}\n`)
@@ -161,6 +160,7 @@ module.exports = async function main (ctx: any): Promise<number> {
exitCode = 1;
} else if (events) {
events.forEach((data) => {
if (!data.event) return; // keepalive
print(` ${chalk.gray(new Date(data.created).toISOString())} ${data.event} ${
getEventMetadata(data)
}\n`);

View File

@@ -2,7 +2,6 @@
//@flow
// Packages
const arg = require('arg')
const chalk = require('chalk')
const ms = require('ms')
const plural = require('pluralize')
@@ -19,9 +18,10 @@ const wait = require('../../../util/output/wait')
const strlen = require('../util/strlen')
const getContextName = require('../util/get-context-name')
const toHost = require('../util/to-host')
const argCommon = require('../util/arg-common')()
import getDeploymentInstances from './alias/get-deployment-instances'
import getAliases from '../util/alias/get-aliases'
import getArgs from '../util/get-args'
import getDeploymentInstances from '../util/deploy/get-deployment-instances'
const help = () => {
console.log(`
@@ -65,8 +65,7 @@ module.exports = async function main(ctx) {
let argv
try {
argv = arg(ctx.argv.slice(3), {
...argCommon,
argv = getArgs(ctx.argv.slice(2), {
'--all': Boolean,
'-a': '--all',
})
@@ -78,12 +77,12 @@ module.exports = async function main(ctx) {
const debugEnabled = argv['--debug']
const { print, log, error, note, debug } = createOutput({ debug: debugEnabled })
if (argv._.length > 1) {
if (argv._.length > 2) {
error(`${cmd('now ls [app]')} accepts at most one argument`);
return 1;
}
let app = argv._[0]
let app = argv._[1]
let host = null
const apiUrl = ctx.apiUrl
@@ -159,13 +158,13 @@ module.exports = async function main(ctx) {
if (app && !deployments.length) {
debug('No deployments: attempting to find aliases that matches supplied app name')
const aliases = await now.listAliases()
const aliases = await getAliases(now)
const item = aliases.find(e => e.uid === app || e.alias === app)
if (item) {
debug('Found alias that matches app name');
const match = await now.findDeployment(item.deploymentId)
const instances = await getDeploymentInstances(now, item.deploymentId)
const instances = await getDeploymentInstances(now, item.deploymentId, 'now_cli_alias_instances')
match.instanceCount = Object.keys(instances).reduce((count, dc) => count + instances[dc].instances.length, 0)
if (match !== null && typeof match !== 'undefined') {
deployments = Array.of(match)
@@ -221,7 +220,7 @@ module.exports = async function main(ctx) {
[
dep.name,
chalk.bold((includeScheme ? 'https://' : '') + dep.url),
dep.type === 'BINARY' || dep.instanceCount == null ? chalk.gray('-') : dep.instanceCount,
dep.instanceCount == null ? chalk.gray('-') : dep.instanceCount,
dep.type,
stateString(dep.state),
chalk.gray(ms(Date.now() - new Date(dep.created)))

View File

@@ -17,7 +17,7 @@ const getContextName = require('../util/get-context-name')
const help = () => {
console.log(`
${chalk.bold(`${logo} now logs`)} <deploymentId|url>
${chalk.bold(`${logo} now logs`)} <url|deploymentId>
${chalk.dim('Options:')}
@@ -205,6 +205,8 @@ function compareEvents(d1, d2) {
}
function printLogShort(log) {
if (!log.created) return; // keepalive
let data
const obj = log.object
if (log.type === 'request') {
@@ -241,6 +243,8 @@ function printLogShort(log) {
}
function printLogRaw(log) {
if (!log.created) return; // keepalive
if (log.object) {
console.log(log.object)
} else {

View File

@@ -17,6 +17,7 @@ const cmd = require('../../../util/output/cmd')
const elapsed = require('../../../util/output/elapsed')
const { normalizeURL } = require('../../../util/url')
const getContextName = require('../util/get-context-name')
import getAliases from '../util/alias/get-aliases'
const help = () => {
console.log(`
@@ -125,7 +126,7 @@ module.exports = async function main (ctx: any): Promise<number>{
let aliases;
try {
aliases = await Promise.all(matches.map(depl => now.listAliases(depl.uid)))
aliases = await Promise.all(matches.map(depl => getAliases(now, depl.uid)))
cancelWait();
} catch (err) {
cancelWait();
@@ -142,7 +143,7 @@ module.exports = async function main (ctx: any): Promise<number>{
})
if (matches.length === 0) {
error(
log(
`Could not find ${argv.safe
? 'unaliased'
: 'any'} deployments matching ${ids

View File

@@ -1,34 +1,27 @@
#!/usr/bin/env node
// @flow
import ms from 'ms'
import chalk from 'chalk'
// Packages
const ms = require('ms');
const chalk = require('chalk')
const arg = require('arg')
const sleep = require('then-sleep');
import cmd from '../../../util/output/cmd'
import createOutput from '../../../util/output'
import logo from '../../../util/output/logo'
import stamp from '../../../util/output/stamp'
// Utilities
const cmd = require('../../../util/output/cmd')
const createOutput = require('../../../util/output')
const Now = require('../util/')
const logo = require('../../../util/output/logo')
const elapsed = require('../../../util/output/elapsed')
const argCommon = require('../util/arg-common')()
const wait = require('../../../util/output/wait')
const { tick } = require('../../../util/output/chars')
const { normalizeRegionsList } = require('../util/dcs')
const { handleError } = require('../util/error')
const getContextName = require('../util/get-context-name')
const exit = require('../../../util/exit')
// the "auto" value for scaling
const AUTO = 'auto'
// deployment type
const TYPE_STATIC = 'STATIC'
// states
const STATE_ERROR = 'ERROR'
import * as Errors from '../util/errors'
import Now from '../util/'
import getArgs from '../util/get-args'
import getContextName from '../util/get-context-name'
import getDCsFromArgs from '../util/scale/get-dcs-from-args'
import getDeploymentByIdOrHost from '../util/deploy/get-deployment-by-id-or-host'
import getDeploymentByIdOrThrow from '../util/deploy/get-deployment-by-id-or-throw'
import getMaxFromArgs from '../util/scale/get-max-from-args'
import getMinFromArgs from '../util/scale/get-min-from-args'
import patchDeploymentScale from '../util/scale/patch-deployment-scale'
import waitVerifyDeploymentScale from '../util/scale/wait-verify-deployment-scale'
import type { CLIScaleOptions, DeploymentScaleArgs } from '../util/types'
import { CLIContext, Output } from '../util/types'
import { handleError } from '../util/error'
import { VerifyScaleTimeout } from '../util/errors'
const help = () => {
console.log(`
@@ -65,24 +58,14 @@ const help = () => {
`)
}
// $FlowFixMe
module.exports = async function main (ctx) {
let id // Deployment Id or URL
let dcIds = null // Target DCs
let min = null // Minimum number of instances
let max = null // Maximum number of instances
let deployment
let argv;
module.exports = async function main(ctx: CLIContext): Promise<number> {
let argv: CLIScaleOptions
try {
argv = arg(ctx.argv.slice(3), {
...argCommon,
argv = getArgs(ctx.argv.slice(2), {
'--verify-timeout': Number,
'--no-verify': Boolean,
'-n': '--no-verify',
'--verify-timeout': String,
'-t': '--verify-timeout'
})
} catch (err) {
handleError(err)
@@ -94,379 +77,138 @@ module.exports = async function main (ctx) {
return 2;
}
const apiUrl = ctx.apiUrl
const debugEnabled = argv['--debug']
const output = createOutput({ debug: debugEnabled })
const { log, success, error, debug } = output;
// Prepare the context
const { authConfig: { credentials }, config: { sh } } = ctx
const { currentTeam } = sh;
const { apiUrl } = ctx;
// extract the first parameter
id = argv._[0]
// $FlowFixMe
const {token} = credentials.find(item => item.provider === 'sh')
const now = new Now({ apiUrl, token, debug: argv['--debug'], currentTeam })
const output: Output = createOutput({ debug: argv['--debug'] })
const contextName = getContextName(sh);
// `now scale ls` has been deprecated
if (id === 'ls') {
error(`${cmd('now scale ls')} has been deprecated. Use ${cmd('now ls')} and ${cmd('now inspect <url>')}`, 'scale-ls')
// Fail if the user is providing an old command
if (argv._[1] === 'ls') {
output.error(`${cmd('now scale ls')} has been deprecated. Use ${cmd('now ls')} and ${cmd('now inspect <url>')}`)
now.close();
return 1
}
if (argv._.length < 2) {
error(`${cmd('now scale <url> <dc> [min] [max]')} expects at least two arguments`)
// Ensure the number of arguments is between the allower range
if (argv._.length < 3 || argv._.length > 5) {
output.error(`${cmd('now scale <url> <dc> [min] [max]')} expects at least two arguments`)
help();
return 1;
}
if (argv._.length > 4) {
error(`${cmd('now scale <url> <dc> [min] [max]')} expects at most four arguments`)
help();
return 1;
}
if (argv['--verify-timeout'] != null && argv['--no-verify']) {
error('The options `--verify-timeout` and `--no-verify` cannot be used at once');
return 1;
}
if (null != argv['--verify-timeout'] && !Number.isInteger(ms(argv['--verify-timeout']))) {
error('Invalid time string for `--verify-timeout`. Should be a number of miliseconds or a string like "3m"');
return 1;
}
// for legacy reasons, we still allow `now scale <url> <min> [max]`.
// if this is the case, we apply the desired rules to all dcs
let dcIdOrMin = argv._[1];
if (isMinOrMaxArgument(dcIdOrMin)) {
min = toMin(dcIdOrMin)
const maybeMax = argv._[2];
if (maybeMax !== undefined) {
if (isMinOrMaxArgument(maybeMax)) {
max = toMax(maybeMax);
} else {
error(`Expected "${maybeMax}" to be a <max> argument, but it's not numeric or "auto" (<min> was supplied as "${min}")`)
return 1
}
// if we got min and max, but something else, err
if (argv._.length > 3) {
error(`Invalid number of arguments: expected <min> ("${min}") and [max]`)
return 1
}
} else {
if (min === AUTO) {
min = 0;
max = AUTO;
} else {
max = min;
}
}
// NOTE: in the future, we might warn that this will be deprecated
// for now, we just translate it into all DCs
dcIdOrMin = "all";
}
// extract the dcs
try {
dcIds = normalizeRegionsList(dcIdOrMin.split(','))
debug(`${dcIdOrMin} normalized to ${dcIds.join(',')}`)
} catch (err) {
if (err.code === 'INVALID_ID') {
error(
`The value "${err.id}" in \`--regions\` is not a valid region or DC identifier`,
'scale-invalid-dc'
)
return 1;
} else if (err.code === 'INVALID_ALL') {
error('The region value "all" was used, but it cannot be used alongside other region or dc identifiers')
return 1;
} else {
throw err;
}
}
// convert numeric parameters into actual numbers
// only if min and max haven't been set above
if (min === null) {
if (argv._[2] != null) {
if (isMinOrMaxArgument(argv._[2])) {
min = toMin(argv._[2]);
} else {
error(`Invalid <min> parameter "${argv._[2]}". A number or "auto" were expected`);
return 1;
}
if (argv._[3] != null) {
if (isMinOrMaxArgument(argv._[3])) {
max = toMax(argv._[3]);
} else {
error(`Invalid <max> parameter "${argv._[3]}". A number or "auto" were expected`);
return 1;
}
} else {
if (min === AUTO) {
max = AUTO;
} else {
max = min;
}
}
} else {
min = 0;
max = AUTO;
}
}
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
const { currentTeam } = sh;
const contextName = getContextName(sh);
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam })
// resolve the deployment, since we might have been given an alias
const depFetchStart = Date.now();
const cancelWait = wait(`Fetching deployment "${id}" in ${chalk.bold(contextName)}`);
try {
deployment = await now.findDeployment(id)
cancelWait();
} catch (err) {
cancelWait();
if (err.status === 404) {
error(`Failed to find deployment "${id}" in ${chalk.bold(contextName)}`)
now.close();
return 1;
} else {
// unexpected
throw err;
}
}
log(`Fetched deployment "${deployment.url}" ${elapsed(Date.now() - depFetchStart)}`);
if (deployment.type === TYPE_STATIC) {
error('Scaling rules cannot be set on static deployments');
now.close();
return 1;
}
if (deployment.state === STATE_ERROR) {
error('Cannot scale a deployment in the ERROR state');
const dcs = getDCsFromArgs(argv._)
if (dcs instanceof Errors.InvalidAllForScale) {
output.error('The region value "all" was used, but it cannot be used alongside other region or dc identifiers')
now.close();
return 1
} else if (dcs instanceof Errors.InvalidRegionOrDCForScale) {
output.error(`The value "${dcs.meta.regionOrDC}" is not a valid region or DC identifier`)
now.close();
return 1
}
const min = getMinFromArgs(argv._)
if (min instanceof Errors.InvalidMinForScale) {
output.error(`Invalid <min> parameter "${min.meta.value}". A number or "auto" were expected`)
now.close();
return 1
}
const max = getMaxFromArgs(argv._)
if (max instanceof Errors.InvalidMinForScale) {
output.error(`Invalid <min> parameter "${max.meta.value}". A number or "auto" were expected`)
now.close();
return 1
} else if (max instanceof Errors.InvalidArgsForMinMaxScale) {
output.error(`Invalid number of arguments: expected <min> ("${max.meta.min}") and [max]`)
now.close();
return 1
} else if (max instanceof Errors.InvalidMaxForScale) {
output.error(`Invalid <max> parameter "${max.meta.value}". A number or "auto" were expected`)
now.close();
return 1
}
// Fetch the deployment
const deploymentStamp = stamp()
const deployment = await getDeploymentByIdOrHost(now, contextName, argv._[1])
if (deployment instanceof Errors.DeploymentPermissionDenied) {
output.error(`No permission to access deployment ${chalk.dim(deployment.meta.id)} under ${chalk.bold(deployment.meta.context)}`)
now.close();
return 1
} else if (deployment instanceof Errors.DeploymentNotFound) {
output.error(`Failed to find deployment "${argv._[1]}" in ${chalk.bold(contextName)}`)
now.close();
return 1
}
output.log(`Fetched deployment "${deployment.url}" ${deploymentStamp()}`);
// Make sure the deployment can be scaled
if (deployment.type === 'STATIC') {
output.error('Scaling rules cannot be set on static deployments');
now.close();
return 1;
} else if (deployment.state === 'ERROR') {
output.error('Cannot scale a deployment in the ERROR state');
now.close();
return 1;
}
const scaleArgs = {}
for (const dc of dcIds) {
scaleArgs[dc] = {
min,
max
}
}
debug('scale args: ' + JSON.stringify(scaleArgs));
const scaleArgs: DeploymentScaleArgs = dcs.reduce((result, dc) => ({...result, [dc]: { min, max }}), {})
output.debug(`Setting scale deployment presets to ${JSON.stringify(scaleArgs)}`)
const cancelScaleWait = wait(`Setting scale rules for ${
dcIds.map(d => chalk.bold(d)).join(', ')
} (min: ${chalk.bold(min)}, max: ${chalk.bold(max)})`);
const startScale = Date.now();
try {
await setScale(now, deployment.uid, scaleArgs);
cancelScaleWait();
} catch (err) {
cancelScaleWait();
if (err.status === 400) {
switch (err.code) {
case 'forbidden_min_instances':
error(`You can't scale to more than ${err.max} min instances with your current plan.`);
break;
case 'forbidden_max_instances':
error(`You can't scale to more than ${err.max} max instances with your current plan.`);
break;
case 'wrong_min_max_relation':
error(`Min number of instances can't be higher than max.`);
break;
default:
throw err;
}
return 1;
} else {
throw err;
}
// Set the deployment scale
const scaleStamp = stamp()
const result = await patchDeploymentScale(output, now, deployment.uid, scaleArgs, deployment.url)
if (result instanceof Errors.ForbiddenScaleMinInstances) {
output.error(`You can't scale to more than ${result.meta.max} min instances with your current plan.`)
now.close();
return 1
} else if (result instanceof Errors.ForbiddenScaleMaxInstances) {
output.error(`You can't scale to more than ${result.meta.max} max instances with your current plan.`)
now.close();
return 1
} else if (result instanceof Errors.InvalidScaleMinMaxRelation) {
output.error(`Min number of instances can't be higher than max.`)
now.close();
return 1
} else if (result instanceof Errors.NotSupportedMinScaleSlots) {
output.error(`Cloud v2 does not yet support setting a non-zero min number of instances.`)
output.log('Read more: https://err.sh/now-cli/v2-no-min')
now.close();
return 1
}
const successMsg = `Scale rules for ${
dcIds.map(d => chalk.bold(d)).join(', ')
} (min: ${chalk.bold(min)}, max: ${chalk.bold(max)}) saved ${elapsed(Date.now() - startScale)}`
console.log(`${chalk.gray('>')} Scale rules for ${
dcs.map(d => chalk.bold(d)).join(', ')
} (min: ${chalk.bold(min)}, max: ${chalk.bold(max)}) saved ${scaleStamp()}`)
if (deployment.type === 'BINARY' || argv['--no-verify']) {
console.log(successMsg)
if (argv['--no-verify']) {
now.close();
return 0;
}
console.log(successMsg)
const startVerification = Date.now()
const cancelVerifyWait = waitDcs(scaleArgs, output)
const cancelExit = onExit(() => {
cancelVerifyWait();
log('Verification aborted. Scale settings were saved')
exit(0);
});
try {
await waitForScale(
now,
deployment.uid,
scaleArgs,
output,
{
timeout: ms(argv['--verify-timeout'] != null ? argv['--verify-timeout'] : '5m'),
checkInterval: 500,
onDCScaled(id, instanceCount) {
cancelVerifyWait(id, instanceCount);
}
}
);
cancelVerifyWait()
} catch (err) {
cancelVerifyWait()
throw err
} finally {
cancelExit();
}
success(`Scale state verified ${elapsed(Date.now() - startVerification)}`);
now.close();
return 0;
}
// version of wait() that also displays progress
// for all dcs
function waitDcs(scaleArgs, { log }) {
let cancelMainWait;
const waitStart = Date.now();
const remaining = new Set(Object.keys(scaleArgs));
const renderWait = () => {
cancelMainWait = wait(`Waiting for instances in ${
Array.from(remaining).map(id => chalk.bold(id)).join(', ')
} to match constraints`)
}
renderWait();
return (dcId = null, instanceCount = null) => {
if (dcId !== null && instanceCount !== null) {
remaining.delete(dcId);
cancelMainWait();
log(`${chalk.cyan(tick)} Verified ${chalk.bold(dcId)} (${instanceCount}) ${elapsed(Date.now() - waitStart)}`);
renderWait();
} else {
cancelMainWait();
// Verify that the scale presets are there
const verifyStamp = stamp()
const updatedDeployment = await getDeploymentByIdOrThrow(now, contextName, deployment.uid)
if (updatedDeployment.type === 'NPM' || updatedDeployment.type === 'DOCKER') {
const result = await waitVerifyDeploymentScale(output, now, deployment.uid, updatedDeployment.scale)
if (result instanceof VerifyScaleTimeout) {
output.error(`Instance verification timed out (${ms(result.meta.timeout)})`, 'verification-timeout')
now.close()
return 1
}
}
}
// invokes the passed function upon process exit
function onExit(fn: Function) {
let exit = false;
const onExit_ = () => {
if (exit) return;
fn();
exit = true;
output.success(`Scale state verified ${verifyStamp()}`);
}
process.on('SIGTERM', onExit_);
process.on('SIGINT', onExit_);
process.on('exit', onExit_);
return () => {
process.removeListener('SIGTERM', onExit_);
process.removeListener('SIGINT', onExit_);
process.removeListener('exit', onExit_);
}
}
function setScale(now, deploymentId, scale) {
return now.fetch(
`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances`,
{
method: 'PATCH',
body: scale
}
)
}
// waits until the deployment's instances count reflects the intended
// scale that the user is configuring with the command
async function waitForScale(now, deploymentId, intendedScale, { debug }, { timeout = ms('5m'), checkInterval = 500, onDCScaled = null } = {}) {
const start = Date.now()
const intendedScaleDcs = new Set(Object.keys(intendedScale));
// eslint-disable-next-line no-constant-condition
while (true) {
if (start + timeout <= Date.now()) {
throw new Error('Timeout while verifying instance count (10m)');
}
const data = await now.fetch(`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances?init=1`)
for (const dc of intendedScaleDcs) {
const currentScale = data[dc];
if (!currentScale) {
debug(`missing data for dc ${dc}`)
break;
}
const instanceCount = data[dc].instances.length;
const { min, max } = intendedScale[dc];
if (isInstanceCountBetween(instanceCount, min, max)) {
if (onDCScaled !== null) {
onDCScaled(dc, instanceCount);
}
intendedScaleDcs.delete(dc);
debug(`dc "${dc}" match`);
} else {
debug(`dc "${dc}" miss. intended: ${min}-${max}. current: ${instanceCount}`);
}
}
if (intendedScaleDcs.size === 0) {
return;
}
await sleep(checkInterval);
}
}
// whether it's a numeric or "auto"
function isMinOrMaxArgument (v: string) {
return v === AUTO || isNumeric(v);
}
function isInstanceCountBetween(v: number, min: number, max: number) {
if (v < min) {
return false;
}
if (v > (max === AUTO ? Infinity : max)) {
return false;
}
return true;
}
// converts "3" to 3, and "auto" to 0
function toMin (v: string) {
return v === AUTO ? v : Number(v);
}
// converts "3" to 3, and "auto" to "auto"
function toMax (v: string) {
return v === AUTO ? v : Number(v);
}
// validates whether a string is "numeric", like "3"
function isNumeric (v: string) {
return /^\d+$/.test(v)
now.close()
return 0
}

View File

@@ -28,7 +28,7 @@ const help = () => {
${chalk.dim('Examples:')}
${chalk.gray('')} Show the current team context
${chalk.gray('')} Shows the currently logged in username
${chalk.cyan('$ now whoami')}
`)

View File

@@ -1,884 +0,0 @@
// Packages
const fetch = require('node-fetch')
const loadJSON = require('load-json-file')
const publicSuffixList = require('psl')
const mri = require('mri')
const ms = require('ms')
const chalk = require('chalk')
const plural = require('pluralize')
const { write: copy } = require('clipboardy')
// Ours
const promptBool = require('../../../util/input/prompt-bool')
const info = require('../../../util/output/info')
const param = require('../../../util/output/param')
const wait = require('../../../util/output/wait')
const success = require('../../../util/output/success')
const uid = require('../../../util/output/uid')
const eraseLines = require('../../../util/output/erase-lines')
const stamp = require('../../../util/output/stamp')
const error = require('../../../util/output/error')
const treatBuyError = require('../util/domains/treat-buy-error')
const scaleInfo = require('./scale-info')
const isZeitWorld = require('./is-zeit-world')
const isValidDomain = require('./domains/is-valid-domain')
const toHost = require('./to-host')
const exit = require('../../../util/exit')
const Now = require('./')
const NowScale = require('../util/scale')
const argv = mri(process.argv.slice(2), {
boolean: ['no-clipboard'],
alias: { 'no-clipboard': 'C' }
})
const isTTY = process.stdout.isTTY
const clipboard = !argv['no-clipboard']
const domainRegex = /^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/
const DNS_VERIFICATION_ERROR = `Please make sure that your nameservers point to ${chalk.underline(
'zeit.world'
)}.
> Examples: (full list at ${chalk.underline('https://zeit.world')})
> ${chalk.gray('-')} ${chalk.underline('a.zeit.world')} ${chalk.dim(
'96.45.80.1'
)}
> ${chalk.gray('-')} ${chalk.underline('b.zeit.world')} ${chalk.dim(
'46.31.236.1'
)}
> ${chalk.gray('-')} ${chalk.underline('c.zeit.world')} ${chalk.dim(
'43.247.170.1'
)}`;
const DOMAIN_VERIFICATION_ERROR =
DNS_VERIFICATION_ERROR +
`\n> Alternatively, ensure it resolves to ${chalk.underline(
'alias.zeit.co'
)} via ${chalk.dim('CNAME')} / ${chalk.dim('ALIAS')}.`
module.exports = class Alias extends Now {
constructor(args) {
super(args)
this.scale = new NowScale(args)
}
async ls(deployment) {
if (deployment) {
const target = await this.findDeployment(deployment)
if (!target) {
const err = new Error(
`Aliases not found by "${deployment}". Run ${chalk.dim(
'`now alias ls`'
)} to see your aliases.`
)
err.userError = true
throw err
}
return this.listAliases(target.uid)
}
return this.listAliases()
}
async rm(_alias) {
return this.retry(async bail => {
const res = await this._fetch(`/now/aliases/${_alias.uid}`, {
method: 'DELETE'
})
if (res.status === 403) {
return bail(new Error('Unauthorized'))
}
if (res.status !== 200) {
const err = new Error('Deletion failed. Try again later.')
throw err
}
})
}
async findDeployment(deployment) {
const list = await this.list()
let key
let val
if (/\./.test(deployment)) {
val = toHost(deployment)
key = 'url'
} else {
val = deployment
key = 'uid'
}
const depl = list.find(d => {
if (d[key] === val) {
if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by ${key} ${val}`)
}
return true
}
// Match prefix
if (`${val}.now.sh` === d.url) {
if (this._debug) {
console.log(`> [debug] matched deployment ${d.uid} by url ${d.url}`)
}
return true
}
return false
})
return depl
}
async updatePathBasedroutes(alias, rules, domains) {
alias = await this.maybeSetUpDomain(alias, domains)
return this.upsertPathAlias(alias, rules)
}
async upsertPathAlias(alias, rules) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] /now/aliases #${attempt}`)
}
const rulesData = this.readRulesFile(rules)
const ruleCount = rulesData.rules.length
const res = await this._fetch(`/now/aliases`, {
method: 'POST',
body: { alias, rules: rulesData.rules }
})
const body = await res.json()
body.ruleCount = ruleCount
if (this._debug) {
console.timeEnd(`> [debug] /now/aliases #${attempt}`)
}
// 409 conflict is returned if it already exists
if (res.status === 409) {
return { uid: body.error.uid }
}
if (res.status === 422) {
return body
}
// No retry on authorization problems
if (res.status === 403) {
const code = body.error.code
if (code === 'custom_domain_needs_upgrade') {
const err = new Error(
`Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray(
'`'
)}${chalk.cyan('now upgrade')}${chalk.gray('`')}.`
)
err.userError = true
return bail(err)
}
if (code === 'alias_in_use') {
const err = new Error(
`The alias you are trying to configure (${chalk.underline(
chalk.bold(alias)
)}) is already in use by a different account.`
)
err.userError = true
return bail(err)
}
if (code === 'forbidden') {
const err = new Error(
'The domain you are trying to use as an alias is already in use by a different account.'
)
err.userError = true
return bail(err)
}
return bail(new Error('Authorization error'))
}
// All other errors
if (body.error) {
const code = body.error.code
if (code === 'cert_missing') {
console.log(
`> Provisioning certificate for ${chalk.underline(
chalk.bold(alias)
)}`
)
try {
await this.createCert(alias)
} catch (err) {
// We bail to avoid retrying the whole process
// of aliasing which would involve too many
// retries on certificate provisioning
return bail(err)
}
// Try again, but now having provisioned the certificate
return this.upsertPathAlias(alias, rules)
}
if (code === 'cert_expired') {
console.log(
`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`
)
try {
await this.createCert(alias, { renew: true })
} catch (err) {
return bail(err)
}
}
return bail(new Error(body.error.message))
}
// The two expected successful codes are 200 and 304
if (res.status !== 200 && res.status !== 304) {
throw new Error('Unhandled error')
}
return body
})
}
readRulesFile(rules) {
try {
return loadJSON.sync(rules)
} catch (err) {
console.error(`Reading rules file ${rules} failed: ${err}`)
}
}
async set(deployment, alias, domains, currentTeam, user) {
alias = alias.replace(/^https:\/\//i, '')
if (alias.indexOf('.') === -1) {
// `.now.sh` domain is implied if just the subdomain is given
alias += '.now.sh'
}
if (!isValidDomain(alias)) {
const err = new Error(
`${chalk.bold(alias)} is not a valid domain`
)
err.userError = true
throw err
}
const depl = await this.findDeployment(deployment)
if (!depl) {
const err = new Error(
`Deployment not found by "${deployment}". Run ${chalk.dim(
'`now ls`'
)} to see your deployments.`
)
err.userError = true
throw err
}
const aliasDepl = (await this.listAliases()).find(e => e.alias === alias)
if (aliasDepl && aliasDepl.rules) {
if (isTTY) {
try {
const msg =
`> Path alias exists with ${aliasDepl.rules.length} rule${aliasDepl
.rules.length > 1
? 's'
: ''}.\n` +
`> Are you sure you want to update ${alias} to be a normal alias?\n`
const confirmation = await promptBool(msg, {
trailing: '\n'
})
if (!confirmation) {
info('Aborted')
return exit(1)
}
} catch (err) {
console.log(err)
}
} else {
console.log(
`Overwriting path alias with ${aliasDepl.rules.length} rule${aliasDepl
.rules.length > 1
? 's'
: ''} to be a normal alias.`
)
}
}
let aliasedDeployment = null
let shouldScaleDown = false
if (aliasDepl && depl.scale) {
aliasedDeployment = await this.findDeployment(aliasDepl.deploymentId)
if (
aliasedDeployment &&
aliasedDeployment.scale &&
aliasedDeployment.scale.current >= depl.scale.current &&
(aliasedDeployment.scale.min > depl.scale.min ||
aliasedDeployment.scale.max > depl.scale.max)
) {
shouldScaleDown = true
console.log(
`> Alias ${alias} points to ${chalk.bold(
aliasedDeployment.url
)} (${chalk.bold(
plural('instance', aliasedDeployment.scale.current, true)
)})`
)
// Test if we need to change the scale or just update the rules
console.log(
`> Scaling ${depl.url} to ${chalk.bold(
plural('instance', aliasedDeployment.scale.current, true)
)} atomically` // Not a typo
)
if (depl.scale.current !== aliasedDeployment.scale.current) {
// Scale it to current limit
if (depl.scale.current !== aliasedDeployment.scale.current) {
if (this._debug) {
console.log(`> Scaling deployment to match current scale.`)
}
await this.scale.setScale(depl.uid, {
min: aliasedDeployment.scale.current,
max: aliasedDeployment.scale.current
})
}
await scaleInfo(this, depl.url)
if (this._debug) {
console.log(`> Updating scaling rules for deployment.`)
}
}
await this.scale.setScale(depl.uid, {
min: Math.max(aliasedDeployment.scale.min, depl.scale.min),
max: Math.max(aliasedDeployment.scale.max, depl.scale.max)
})
}
}
alias = await this.maybeSetUpDomain(alias, domains, currentTeam, user)
const aliasTime = Date.now()
const newAlias = await this.createAlias(depl, alias)
if (!newAlias) {
throw new Error(
`Unexpected error occurred while setting up alias: ${JSON.stringify(
newAlias
)}`
)
}
const { created, uid } = newAlias
if (created) {
const output = `${chalk.cyan(
'> Success!'
)} ${alias} now points to ${chalk.bold(depl.url)}! ${chalk.grey(
'[' + ms(Date.now() - aliasTime) + ']'
)}`
if (isTTY && clipboard) {
try {
await copy(depl.url)
} catch (err) {
} finally {
console.log(output)
}
} else {
console.log(output)
}
} else {
console.log(
`${chalk.cyan('> Success!')} Alias already exists ${chalk.dim(
`(${uid})`
)}.`
)
}
if (aliasedDeployment && shouldScaleDown) {
const scaleDown = Date.now()
await this.scale.setScale(aliasedDeployment.uid, { min: 0, max: 1 })
console.log(
`> Scaled ${chalk.gray(
aliasedDeployment.url
)} down to 1 instance ${chalk.gray(
'[' + ms(Date.now() - scaleDown) + ']'
)}`
)
}
}
createAlias(depl, alias) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(
`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`
)
}
const res = await this._fetch(`/now/deployments/${depl.uid}/aliases`, {
method: 'POST',
body: { alias }
})
const body = await res.json()
if (this._debug) {
console.timeEnd(
`> [debug] /now/deployments/${depl.uid}/aliases #${attempt}`
)
}
// 409 conflict is returned if it already exists
if (res.status === 409) {
return { uid: body.error.uid }
}
// No retry on authorization problems
if (res.status === 403) {
const code = body.error.code
if (code === 'custom_domain_needs_upgrade') {
const err = new Error(
`Custom domains are only enabled for premium accounts. Please upgrade by running ${chalk.gray(
'`'
)}${chalk.cyan('now upgrade')}${chalk.gray('`')}.`
)
err.userError = true
return bail(err)
}
if (code === 'alias_in_use') {
const err = new Error(
`The alias you are trying to configure (${chalk.underline(
chalk.bold(alias)
)}) is already in use by a different account.`
)
err.userError = true
return bail(err)
}
if (code === 'forbidden') {
const err = new Error(
'The domain you are trying to use as an alias is already in use by a different account.'
)
err.userError = true
return bail(err)
}
return bail(new Error('Authorization error'))
}
// All other errors
if (body.error) {
const code = body.error.code
if (code === 'deployment_not_found') {
return bail(new Error('Deployment not found'))
}
if (code === 'cert_missing') {
console.log(
`> Provisioning certificate for ${chalk.underline(
chalk.bold(alias)
)}`
)
try {
await this.createCert(alias)
} catch (err) {
// We bail to avoid retrying the whole process
// of aliasing which would involve too many
// retries on certificate provisioning
return bail(err)
}
// Try again, but now having provisioned the certificate
return this.createAlias(depl, alias)
}
if (code === 'cert_expired') {
console.log(
`> Renewing certificate for ${chalk.underline(chalk.bold(alias))}`
)
try {
await this.createCert(alias, { renew: true })
} catch (err) {
return bail(err)
}
}
if (code === 'invalid_alias') {
return bail(new Error('We do not support nested subdomains.'))
}
return bail(new Error(body.error.message))
}
// The two expected successful codes are 200 and 304
if (res.status !== 200 && res.status !== 304) {
throw new Error('Unhandled error')
}
return body
})
}
async setupRecord(domain, name) {
await this.setupDomain(domain)
if (this._debug) {
console.log(`> [debug] Setting up record "${name}" for "${domain}"`)
}
const type = name === '' ? 'ALIAS' : 'CNAME'
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] /domains/${domain}/records #${attempt}`)
}
const res = await this._fetch(`/domains/${domain}/records`, {
method: 'POST',
body: { type, name: name === '' ? name : '*', value: 'alias.zeit.co' }
})
if (this._debug) {
console.timeEnd(`> [debug] /domains/${domain}/records #${attempt}`)
}
if (res.status === 403) {
return bail(new Error('Unauthorized'))
}
const body = await res.json()
if (res.status === 409 && body.error.code === 'record_conflict') {
if (this._debug) {
console.log(`> [debug] ${body.error.oldId} is a conflicting record for "${name}"`)
}
return
}
if (res.status !== 200) {
throw new Error(body.error.message)
}
return
})
}
async maybeSetUpDomain(alias, domains, currentTeam, user) {
const gracefulExit = () => {
this.close()
domains.close()
// eslint-disable-next-line unicorn/no-process-exit
process.exit()
}
// Make alias lowercase
alias = alias.toLowerCase()
// Trim leading and trailing dots
// for example: `google.com.` => `google.com`
alias = alias.replace(/^\.+/, '').replace(/\.+$/, '')
// Evaluate the alias
if (/\./.test(alias)) {
alias = toHost(alias)
} else {
if (this._debug) {
console.log(`> [debug] suffixing \`.now.sh\` to alias ${alias}`)
}
alias = `${alias}.now.sh`
}
if (!domainRegex.test(alias)) {
const err = new Error(`Invalid alias "${alias}"`)
err.userError = true
throw err
}
if (!/\.now\.sh$/.test(alias)) {
console.log(`> ${chalk.bold(chalk.underline(alias))} is a custom domain.`)
let stopSpinner = wait('Fetching domain info')
let elapsed = stamp()
const parsed = publicSuffixList.parse(alias)
const pricePromise = domains.price(parsed.domain).catch(() => {
// Can be safely ignored
})
const canBePurchased = await domains.status(parsed.domain)
const aliasParam = param(parsed.domain)
let price
let period
stopSpinner()
if (canBePurchased) {
try {
const json = await pricePromise
price = json.price
period = json.period
} catch (err) {
// Can be safely ignored
}
}
if (canBePurchased && price && period) {
const periodMsg = `${period}yr${period > 1 ? 's' : ''}`
info(
`The domain ${aliasParam} is ${chalk.italic(
'available'
)} to buy under ${chalk.bold(
(currentTeam && currentTeam.slug) || user.username || user.email
)}! ${elapsed()}`
)
const confirmation = await promptBool(
`Buy now for ${chalk.bold(`$${price}`)} (${periodMsg})?`
)
eraseLines(1)
if (!confirmation) {
info('Aborted')
gracefulExit()
}
elapsed = stamp()
stopSpinner = wait('Purchasing')
let domain
try {
domain = await domains.buy(parsed.domain)
} catch (err) {
stopSpinner()
treatBuyError(err)
gracefulExit()
}
stopSpinner()
success(`Domain purchased and created ${uid(domain.uid)} ${elapsed()}`)
stopSpinner = wait('Verifying nameservers')
let domainInfo
try {
domainInfo = await this.setupDomain(parsed.domain)
} catch (err) {
if (this._debug) {
console.log('> [debug] Error while trying to setup the domain', err)
}
}
stopSpinner()
if (!domainInfo.verified) {
const tld = param(`.${parsed.tld}`)
console.error(error(
'The nameservers are pending propagation. Please try again shortly'
))
info(
`The ${tld} servers might take some extra time to reflect changes`
)
gracefulExit()
}
}
console.log(
`> Verifying the DNS settings for ${chalk.bold(
chalk.underline(alias)
)} (see ${chalk.underline('https://zeit.world')} for help)`
)
const _domain = publicSuffixList.parse(alias).domain
let _domainInfo
try {
_domainInfo = await this.getDomain(_domain)
} catch (err) {
if (err.status === 404) {
// It's ok if the domain was not found we'll add it when creating
// the alias
} else {
throw err
}
}
const domainInfo =
_domainInfo && !_domainInfo.error ? _domainInfo : undefined
const { domain, nameservers } = domainInfo
? { domain: _domain }
: await this.getNameservers(alias)
const usingZeitWorld = domainInfo
? !domainInfo.isExternal
: isZeitWorld(nameservers)
let skipDNSVerification = false
if (this._debug) {
if (domainInfo) {
console.log(
`> [debug] Found domain ${domain} with verified:${domainInfo.verified}`
)
} else {
console.log(
`> [debug] Found domain ${domain} and nameservers ${nameservers}`
)
}
}
if (!usingZeitWorld && domainInfo) {
if (domainInfo.verified) {
skipDNSVerification = true
} else if (domainInfo.uid) {
const { verified, created } = await this.setupDomain(domain, {
isExternal: true
})
if (!(created && verified)) {
const e = new Error(
`> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`
)
e.userError = true
throw e
}
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(domain)
)} verified`
)
}
}
try {
if (!skipDNSVerification) {
await this.verifyDomain(alias)
}
} catch (err) {
if (err.userError) {
// A user error would imply that verification failed
// in which case we attempt to correct the dns
// configuration (if we can!)
try {
if (usingZeitWorld) {
console.log(
`> Detected ${chalk.bold(
chalk.underline('zeit.world')
)} nameservers! Configuring records.`
)
const record = alias.substr(0, alias.length - domain.length)
// Lean up trailing and leading dots
const _record = record.replace(/^\./, '').replace(/\.$/, '')
const _domain = domain.replace(/^\./, '').replace(/\.$/, '')
if (_record === '') {
await this.setupRecord(_domain, '*')
}
await this.setupRecord(_domain, _record)
this.recordSetup = true
console.log('> DNS Configured! Verifying propagation…')
try {
await this.retry(() => this.verifyDomain(alias), {
retries: 10,
maxTimeout: 8000
})
} catch (err2) {
const e = new Error(
'> We configured the DNS settings for your alias, but we were unable to ' +
"verify that they've propagated. Please try the alias again later."
)
e.userError = true
throw e
}
} else {
console.log(
`> Resolved IP: ${err.ip
? `${chalk.underline(err.ip)} (unknown)`
: chalk.dim('none')}`
)
console.log(
`> Nameservers: ${nameservers && nameservers.length
? nameservers.map(ns => chalk.underline(ns)).join(', ')
: chalk.dim('none')}`
)
throw err
}
} catch (e) {
if (e.userError) {
throw e
}
throw err
}
} else {
throw err
}
}
if (!usingZeitWorld && !skipDNSVerification) {
if (this._debug) {
console.log(
`> [debug] Trying to register a non-ZeitWorld domain ${domain} for the current user`
)
}
const { uid, verified, created } = await this.setupDomain(domain, {
isExternal: true
})
if (!(created && verified)) {
const e = new Error(
`> Failed to verify the ownership of ${domain}, please refer to 'now domain --help'.`
)
e.userError = true
throw e
}
console.log(
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
chalk.underline(domain)
)} ${chalk.dim(`(${uid})`)} added`
)
}
console.log(`> Verification ${chalk.bold('OK')}!`)
}
return alias
}
verifyDomain(domain) {
return this.retry(
async bail => {
const url = `http://${domain}`
let res
try {
res = await fetch(url, { method: 'HEAD', redirect: 'manual' })
} catch (err) {
if (err.code === 'ENOTFOUND') {
// This means that the domain resolves to nowhere
// Therefore, it has no DNS records
// So let's just mark it as an userError so we try to setup the
// DNS records
const err = new Error(DOMAIN_VERIFICATION_ERROR)
err.userError = true
return bail(err)
} else {
throw new Error(`Failed to fetch "${url}"`)
}
}
if (res.headers.get('server') !== 'now') {
const err = new Error(DOMAIN_VERIFICATION_ERROR)
err.userError = true
return bail(err)
}
},
{ retries: 5 }
)
}
}

View File

@@ -0,0 +1,12 @@
// @flow
import { Now } from '../types'
import type { Alias } from '../types'
async function getAliases(now: Now, deploymentId?: string): Promise<Array<Alias>> {
const payload = await now.fetch(deploymentId
? `/now/deployments/${deploymentId}/aliases`
: '/now/aliases')
return payload.aliases || []
}
export default getAliases

View File

@@ -0,0 +1,8 @@
// @flow
import { Now } from '../types'
export default async function removeAliasById(now: Now, id: string) {
return now.fetch(`/now/aliases/${id}`, {
method: 'DELETE'
})
}

View File

@@ -1,251 +0,0 @@
{
"United States": "US",
"Afghanistan": "AF",
"Åland Islands": "AX",
"Albania": "AL",
"Algeria": "DZ",
"American Samoa": "AS",
"Andorra": "AD",
"Angola": "AO",
"Anguilla": "AI",
"Antarctica": "AQ",
"Antigua and Barbuda": "AG",
"Argentina": "AR",
"Armenia": "AM",
"Aruba": "AW",
"Australia": "AU",
"Austria": "AT",
"Azerbaijan": "AZ",
"Bahamas": "BS",
"Bahrain": "BH",
"Bangladesh": "BD",
"Barbados": "BB",
"Belarus": "BY",
"Belgium": "BE",
"Belize": "BZ",
"Benin": "BJ",
"Bermuda": "BM",
"Bhutan": "BT",
"Bolivia, Plurinational State of": "BO",
"Bonaire, Sint Eustatius and Saba": "BQ",
"Bosnia and Herzegovina": "BA",
"Botswana": "BW",
"Bouvet Island": "BV",
"Brazil": "BR",
"British Indian Ocean Territory": "IO",
"Brunei Darussalam": "BN",
"Bulgaria": "BG",
"Burkina Faso": "BF",
"Burundi": "BI",
"Cambodia": "KH",
"Cameroon": "CM",
"Canada": "CA",
"Cape Verde": "CV",
"Cayman Islands": "KY",
"Central African Republic": "CF",
"Chad": "TD",
"Chile": "CL",
"China": "CN",
"Christmas Island": "CX",
"Cocos (Keeling) Islands": "CC",
"Colombia": "CO",
"Comoros": "KM",
"Congo": "CG",
"CD": "Congo, the Democratic Republic of the",
"Cook Islands": "CK",
"Costa Rica": "CR",
"Côte d'Ivoire": "CI",
"Croatia": "HR",
"Cuba": "CU",
"Curaçao": "CW",
"Cyprus": "CY",
"Czech Republic": "CZ",
"Denmark": "DK",
"Djibouti": "DJ",
"Dominica": "DM",
"Dominican Republic": "DO",
"Ecuador": "EC",
"Egypt": "EG",
"El Salvador": "SV",
"Equatorial Guinea": "GQ",
"Eritrea": "ER",
"Estonia": "EE",
"Ethiopia": "ET",
"Falkland Islands (Malvinas)": "FK",
"Faroe Islands": "FO",
"Fiji": "FJ",
"Finland": "FI",
"France": "FR",
"French Guiana": "GF",
"French Polynesia": "PF",
"French Southern Territories": "TF",
"Gabon": "GA",
"Gambia": "GM",
"Georgia": "GE",
"Germany": "DE",
"Ghana": "GH",
"Gibraltar": "GI",
"Greece": "GR",
"Greenland": "GL",
"Grenada": "GD",
"Guadeloupe": "GP",
"Guam": "GU",
"Guatemala": "GT",
"Guernsey": "GG",
"Guinea": "GN",
"Guinea-Bissau": "GW",
"Guyana": "GY",
"Haiti": "HT",
"Heard Island and McDonald Islands": "HM",
"Holy See (Vatican City State)": "VA",
"Honduras": "HN",
"Hong Kong": "HK",
"Hungary": "HU",
"Iceland": "IS",
"India": "IN",
"Indonesia": "ID",
"Iran, Islamic Republic of": "IR",
"Iraq": "IQ",
"Ireland": "IE",
"Isle of Man": "IM",
"Israel": "IL",
"Italy": "IT",
"Jamaica": "JM",
"Japan": "JP",
"Jersey": "JE",
"Jordan": "JO",
"Kazakhstan": "KZ",
"Kenya": "KE",
"Kiribati": "KI",
"KP": "Korea, Democratic People's Republic of",
"Korea, Republic of": "KR",
"Kuwait": "KW",
"Kyrgyzstan": "KG",
"Lao People's Democratic Republic": "LA",
"Latvia": "LV",
"Lebanon": "LB",
"Lesotho": "LS",
"Liberia": "LR",
"Libya": "LY",
"Liechtenstein": "LI",
"Lithuania": "LT",
"Luxembourg": "LU",
"Macao": "MO",
"MK": "Macedonia, the former Yugoslav Republic of",
"Madagascar": "MG",
"Malawi": "MW",
"Malaysia": "MY",
"Maldives": "MV",
"Mali": "ML",
"Malta": "MT",
"Marshall Islands": "MH",
"Martinique": "MQ",
"Mauritania": "MR",
"Mauritius": "MU",
"Mayotte": "YT",
"Mexico": "MX",
"Micronesia, Federated States of": "FM",
"Moldova, Republic of": "MD",
"Monaco": "MC",
"Mongolia": "MN",
"Montenegro": "ME",
"Montserrat": "MS",
"Morocco": "MA",
"Mozambique": "MZ",
"Myanmar": "MM",
"Namibia": "NA",
"Nauru": "NR",
"Nepal": "NP",
"Netherlands": "NL",
"New Caledonia": "NC",
"New Zealand": "NZ",
"Nicaragua": "NI",
"Niger": "NE",
"Nigeria": "NG",
"Niue": "NU",
"Norfolk Island": "NF",
"Northern Mariana Islands": "MP",
"Norway": "NO",
"Oman": "OM",
"Pakistan": "PK",
"Palau": "PW",
"Palestinian Territory, Occupied": "PS",
"Panama": "PA",
"Papua New Guinea": "PG",
"Paraguay": "PY",
"Peru": "PE",
"Philippines": "PH",
"Pitcairn": "PN",
"Poland": "PL",
"Portugal": "PT",
"Puerto Rico": "PR",
"Qatar": "QA",
"Réunion": "RE",
"Romania": "RO",
"Russian Federation": "RU",
"Rwanda": "RW",
"Saint Barthélemy": "BL",
"SH": "Saint Helena, Ascension and Tristan da Cunha",
"Saint Kitts and Nevis": "KN",
"Saint Lucia": "LC",
"Saint Martin (French part)": "MF",
"Saint Pierre and Miquelon": "PM",
"Saint Vincent and the Grenadines": "VC",
"Samoa": "WS",
"San Marino": "SM",
"Sao Tome and Principe": "ST",
"Saudi Arabia": "SA",
"Senegal": "SN",
"Serbia": "RS",
"Seychelles": "SC",
"Sierra Leone": "SL",
"Singapore": "SG",
"Sint Maarten (Dutch part)": "SX",
"Slovakia": "SK",
"Slovenia": "SI",
"Solomon Islands": "SB",
"Somalia": "SO",
"South Africa": "ZA",
"GS": "South Georgia and the South Sandwich Islands",
"South Sudan": "SS",
"Spain": "ES",
"Sri Lanka": "LK",
"Sudan": "SD",
"Suriname": "SR",
"Svalbard and Jan Mayen": "SJ",
"Swaziland": "SZ",
"Sweden": "SE",
"Switzerland": "CH",
"Syrian Arab Republic": "SY",
"Taiwan, Province of China": "TW",
"Tajikistan": "TJ",
"Tanzania, United Republic of": "TZ",
"Thailand": "TH",
"Timor-Leste": "TL",
"Togo": "TG",
"Tokelau": "TK",
"Tonga": "TO",
"Trinidad and Tobago": "TT",
"Tunisia": "TN",
"Turkey": "TR",
"Turkmenistan": "TM",
"Turks and Caicos Islands": "TC",
"Tuvalu": "TV",
"Uganda": "UG",
"Ukraine": "UA",
"United Arab Emirates": "AE",
"United Kingdom": "GB",
"UM": "United States Minor Outlying Islands",
"Uruguay": "UY",
"Uzbekistan": "UZ",
"Vanuatu": "VU",
"Venezuela, Bolivarian Republic of": "VE",
"Viet Nam": "VN",
"Virgin Islands, British": "VG",
"Virgin Islands, U.S.": "VI",
"Wallis and Futuna": "WF",
"Western Sahara": "EH",
"Yemen": "YE",
"Zambia": "ZM",
"Zimbabwe": "ZW"
}

View File

@@ -1,33 +0,0 @@
// Packages
const gMaps = require('@google/maps')
const MAPS_API_KEY = 'AIzaSyALfKTQ6AiIoJ8WGDXR3E7IBOwlHoTPfYY'
// eslint-disable-next-line camelcase
module.exports = function({ country, zipCode: postal_code }) {
return new Promise(resolve => {
const maps = gMaps.createClient({ key: MAPS_API_KEY })
maps.geocode(
{
address: `${postal_code} ${country}` // eslint-disable-line camelcase
},
(err, res) => {
if (err || res.json.results.length === 0) {
resolve()
}
const data = res.json.results[0]
const components = {}
data.address_components.forEach(c => {
components[c.types[0]] = c
})
const state = components.administrative_area_level_1
const city = components.locality
resolve({
state: state && state.long_name,
city: city && city.long_name
})
}
)
})
}

View File

@@ -0,0 +1,10 @@
// @flow
import { Now } from '../types'
import type { CreditCard } from '../types'
export default async function getCreditCards(now: Now) {
const payload = await now.fetch('/stripe/sources/')
const cards: CreditCard[] = payload.sources
return cards;
}

View File

@@ -1,22 +1,20 @@
// @flow
import psl from 'psl'
import chalk from 'chalk'
import retry from 'async-retry'
import * as Errors from '../errors'
import { Now } from '../types'
import type { Certificate } from '../types'
import wait from '../../../../util/output/wait'
type Options = {
preferDNS: boolean,
}
async function createCertForCns(now: Now, cns: string[], context: string, options?: Options) {
const preferDNS = (options || {}).preferDNS
async function createCertForCns(now: Now, cns: string[], context: string) {
const cancelWait = wait(`Issuing a certificate for ${chalk.bold(cns.join(', '))}`);
try {
const certificate: Certificate = await retry(async (bail) => {
try {
return await now.fetch('/v3/now/certs', {
method: 'POST',
body: { domains: cns, preferDNS },
body: { domains: cns },
})
} catch (error) {
// When it's a configuration error we should retry because of the DNS propagation
@@ -28,8 +26,10 @@ async function createCertForCns(now: Now, cns: string[], context: string, option
}
}
}, { retries: 3, minTimeout: 5000, maxTimeout: 15000 })
cancelWait();
return certificate
} catch (error) {
cancelWait();
if (error.code === 'configuration_error') {
const {domain, subdomain} = psl.parse(error.domain)
return new Errors.DomainConfigurationError(domain, subdomain, error.external)
@@ -40,11 +40,13 @@ async function createCertForCns(now: Now, cns: string[], context: string, option
} else if (error.code === 'rate_limited') {
return new Errors.TooManyCertificates(error.domains)
} else if (error.code === 'too_many_requests') {
return new Errors.TooManyRequests('certificates')
return new Errors.TooManyRequests({api: 'certificates', retryAfter: error.retryAfter})
} else if (error.code === 'validation_running') {
return new Errors.DomainValidationRunning(error.domain)
} else if (error.code === 'should_share_root_domain') {
return new Errors.DomainsShouldShareRoot(error.domains)
} else if (error.code === 'cant_solve_challenge') {
return new Errors.CantSolveChallenge(error.domain, error.type)
} else if (error.code === 'invalid_wildcard_domain') {
return new Errors.InvalidWildcardDomain(error.domain)
} else {

View File

@@ -0,0 +1,49 @@
// @flow
import chalk from 'chalk'
import psl from 'psl'
import { Now } from '../types'
import * as Errors from '../errors'
import wait from '../../../../util/output/wait'
import type { Certificate } from '../types'
export default async function startCertOrder(now: Now, cns: string[], context: string) {
const cancelWait = wait(`Finishing certificate issuance for ${chalk.bold(cns.join(', '))}`);
try {
const cert: Certificate = await now.fetch('/v3/now/certs', {
method: 'PATCH',
body: {
op: "finalizeOrder",
domains: cns
},
})
cancelWait()
return cert
} catch (error) {
cancelWait()
if (error.code === 'cert_order_not_found') {
return new Errors.CertOrderNotFound(cns);
} else if (error.code === 'configuration_error') {
const {domain, subdomain} = psl.parse(error.domain)
return new Errors.DomainConfigurationError(domain, subdomain, error.external)
} else if (error.code === 'forbidden') {
return new Errors.DomainPermissionDenied(error.domain, context)
} else if (error.code === 'wildcard_not_allowed') {
return new Errors.CantGenerateWildcardCert()
} else if (error.code === 'rate_limited') {
return new Errors.TooManyCertificates(error.domains)
} else if (error.code === 'too_many_requests') {
return new Errors.TooManyRequests({api: 'certificates', retryAfter: error.retryAfter})
} else if (error.code === 'validation_running') {
return new Errors.DomainValidationRunning(error.domain)
} else if (error.code === 'should_share_root_domain') {
return new Errors.DomainsShouldShareRoot(error.domains)
} else if (error.code === 'cant_solve_challenge') {
return new Errors.CantSolveChallenge(error.domain, error.type)
} else if (error.code === 'invalid_wildcard_domain') {
return new Errors.InvalidWildcardDomain(error.domain)
} else {
// Throw unexpected errors
throw error
}
}
}

View File

@@ -1,11 +1,10 @@
// @flow
import getCerts from './get-certs'
import { Output, Now } from '../types'
import type { CertificateDetails } from '../types'
async function getCertById(output: Output, now: Now, id: string) {
const certs = await getCerts(output, now)
return certs.find(c => c.uid === id)
const cert: CertificateDetails = await now.fetch(`/v3/now/certs/${id}`)
return cert;
}
export default getCertById

View File

@@ -0,0 +1,10 @@
// @flow
import { Output, Now } from '../types'
import getCerts from './get-certs'
async function getCertsForDomain(output: Output, now: Now, domain: string) {
const certs = await getCerts(output, now)
return certs.filter(cert => cert.cns[0].endsWith(domain))
}
export default getCertsForDomain

View File

@@ -0,0 +1,6 @@
export default function getCnsFromArgs(args) {
return args.reduce((res, item) => ([
...res,
...item.split(',')]
), []).filter(i => i)
}

View File

@@ -0,0 +1,23 @@
// @flow
import chalk from 'chalk'
import { Now } from '../types'
import wait from '../../../../util/output/wait'
import type { CertificateOrder } from '../types'
export default async function startCertOrder(now: Now, cns: string[], contextName: string) {
const cancelWait = wait(`Starting certificate issuance for ${chalk.bold(cns.join(', '))} under ${chalk.bold(contextName)}`);
try {
const order: CertificateOrder = await now.fetch('/v3/now/certs', {
method: 'PATCH',
body: {
op: "startOrder",
domains: cns
},
})
cancelWait()
return order
} catch (error) {
cancelWait()
throw error
}
}

View File

@@ -4,64 +4,70 @@ const Now = require('.')
module.exports = class CreditCards extends Now {
async ls() {
const res = await this._fetch('/cards')
const res = await this._fetch('/stripe/sources/')
const body = await res.json()
if (res.status !== 200) {
const e = new Error(body.error.message)
e.code = body.error.code
throw e
}
return body
}
async setDefault(cardId) {
await this._fetch('/cards/default', {
method: 'PUT',
body: { cardId }
async setDefault(source) {
await this._fetch('/stripe/sources/', {
method: 'POST',
body: {
source,
makeDefault: true
}
})
return true
}
async rm(cardId) {
await this._fetch(`/cards/${encodeURIComponent(cardId)}`, {
method: 'DELETE'
async rm(source) {
await this._fetch(`/stripe/sources/`, {
method: 'DELETE',
body: { source }
})
return true
}
/* eslint-disable camelcase */
add(card) {
return new Promise(async (resolve, reject) => {
const expDateParts = card.expDate.split(' / ')
card = {
name: card.name,
number: card.cardNumber,
cvc: card.ccv,
address_country: card.country,
address_zip: card.zipCode,
address_state: card.state,
address_city: card.city,
address_line1: card.address1
cvc: card.ccv
}
card.exp_month = expDateParts[0]
card.exp_year = expDateParts[1]
try {
const stripeToken = (await stripe.tokens.create({ card })).id
const res = await this._fetch('/cards', {
const token = (await stripe.tokens.create({ card })).id
const res = await this._fetch('/stripe/sources/', {
method: 'POST',
body: { stripeToken }
body: {
source: token
}
})
const body = await res.json()
const { source, error } = await res.json()
if (body && body.id) {
if (source && source.id) {
resolve({
last4: body.last4
last4: source.last4
})
} else if (body.error && body.error.message) {
reject(new Error(body.error.message))
} else if (error && error.message) {
reject(new Error(error.message))
} else {
reject(new Error('Unknown error'))
}

View File

@@ -1,62 +0,0 @@
//@flow
const REGIONS = new Set(["sfo", "bru"]);
const DCS = new Set(["sfo1", "bru1"]);
const ALL = 'all';
// if supplied with a region (eg: `sfo`) it returns
// the default dc for it (`sfo1`)
// if supplied with a dc id, it just returns it
function getDcId(r: string) {
return /\d$/.test(r) ? r : `${r}1`
}
// determines if the supplied string is a valid
// region name or dc id
function isValidRegionOrDcId(r: string) {
return REGIONS.has(r) || DCS.has(r);
}
// receives a list of region or ids, and returns it
// normalized as a list of dcs. the list can contain
// the special string `all`
function normalizeRegionsList(regions: Array<string>) {
let all = false;
let asDcs = [];
for (const r of regions) {
if (r === ALL) {
all = true;
} else {
if (all) {
const err = new Error('`all` cannot be used unless it is the only item on the list of regions')
//$FlowFixMe
err.code = 'INVALID_ALL'
throw err;
} else {
if (isValidRegionOrDcId(r)) {
asDcs.push(getDcId(r))
} else {
const err = new Error(`The supplied region or dc "${r}" is invalid`)
//$FlowFixMe
err.code = 'INVALID_ID'
//$FlowFixMe
err.id = r;
throw err;
}
}
}
}
if (all) {
return Array.from(DCS);
} else {
return asDcs;
}
}
module.exports = {
getDcId,
isValidRegionOrDcId,
normalizeRegionsList
}

View File

@@ -0,0 +1,68 @@
// @flow
import { Now, Output } from '../types'
import generateCertForDeploy from './generate-cert-for-deploy'
import * as Errors from '../errors'
export type CreateDeployError =
Errors.CantGenerateWildcardCert |
Errors.CantSolveChallenge |
Errors.CDNNeedsUpgrade |
Errors.DomainConfigurationError |
Errors.DomainNameserversNotFound |
Errors.DomainNotFound |
Errors.DomainNotVerified |
Errors.DomainPermissionDenied |
Errors.DomainsShouldShareRoot |
Errors.DomainValidationRunning |
Errors.DomainVerificationFailed |
Errors.InvalidWildcardDomain |
Errors.TooManyCertificates |
Errors.TooManyRequests
export default async function createDeploy(output: Output, now: Now, contextName: string, paths: string[], createArgs: Object) {
try {
return await now.create(paths, createArgs)
} catch (error) {
// Means that the domain used as a suffix no longer exists
if (error.code === 'domain_missing') {
return new Errors.DomainNotFound(error.value)
}
// If the domain used as a suffix is not verified, we fail
if (error.code === 'domain_not_verified') {
return new Errors.DomainNotVerified(error.value)
}
// If the user doesn't have permissions over the domain used as a suffix we fail
if (error.code === 'forbidden') {
return new Errors.DomainPermissionDenied(error.value, contextName)
}
// If the cert is missing we try to generate a new one and the retry
if (error.code === 'cert_missing') {
const result = await generateCertForDeploy(output, now, contextName, error.value)
if (
(result instanceof Errors.CantSolveChallenge) ||
(result instanceof Errors.CantGenerateWildcardCert) ||
(result instanceof Errors.DomainConfigurationError) ||
(result instanceof Errors.DomainNameserversNotFound) ||
(result instanceof Errors.DomainNotVerified) ||
(result instanceof Errors.DomainPermissionDenied) ||
(result instanceof Errors.DomainsShouldShareRoot) ||
(result instanceof Errors.DomainValidationRunning) ||
(result instanceof Errors.DomainVerificationFailed) ||
(result instanceof Errors.InvalidWildcardDomain) ||
(result instanceof Errors.CDNNeedsUpgrade) ||
(result instanceof Errors.TooManyCertificates) ||
(result instanceof Errors.TooManyRequests)
) {
return result
} else {
return createDeploy(output, now, contextName, paths, createArgs)
}
}
// If the error is unknown, we just throw
throw error
}
}

View File

@@ -0,0 +1,46 @@
// @flow
import psl from 'psl'
import * as Errors from '../errors'
import { Now, Output } from '../types'
import createCertForCns from '../certs/create-cert-for-cns'
import setupDomain from '../../commands/alias/setup-domain'
import wait from '../../../../util/output/wait'
export default async function generateCertForDeploy(output: Output, now: Now, contextName: string, deployURL: string) {
const {domain} = psl.parse(deployURL)
const cancelSetupWait = wait(`Setting custom suffix domain ${domain}`)
const result = await setupDomain(output, now, domain, contextName)
if (
(result instanceof Errors.DomainNameserversNotFound) ||
(result instanceof Errors.DomainNotVerified) ||
(result instanceof Errors.DomainPermissionDenied) ||
(result instanceof Errors.DomainVerificationFailed) ||
(result instanceof Errors.CDNNeedsUpgrade)
) {
cancelSetupWait()
return result
} else {
cancelSetupWait()
}
// Generate the certificate with the given parameters
const cancelCertWait = wait(`Generating a wildcard certificate for ${domain}`)
let cert = await createCertForCns(now, [domain, `*.${domain}`], contextName)
if (
(cert instanceof Errors.CantGenerateWildcardCert) ||
(cert instanceof Errors.CantSolveChallenge) ||
(cert instanceof Errors.DomainConfigurationError) ||
(cert instanceof Errors.DomainPermissionDenied) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
(cert instanceof Errors.DomainsShouldShareRoot) ||
(cert instanceof Errors.DomainValidationRunning) ||
(cert instanceof Errors.InvalidWildcardDomain) ||
(cert instanceof Errors.TooManyCertificates) ||
(cert instanceof Errors.TooManyRequests)
) {
cancelCertWait()
return cert
} else {
cancelCertWait()
}
}

View File

@@ -0,0 +1,40 @@
// @flow
import { Now } from '../../util/types'
import { DeploymentNotFound, DeploymentPermissionDenied } from '../errors'
import type { Deployment } from '../types'
import toHost from '../to-host'
async function getDeploymentByIdOrHost(now: Now, contextName: string, idOrHost: string) {
try {
const { deployment } = idOrHost.indexOf('.') !== -1
? await getDeploymentByHost(now, toHost(idOrHost))
: await getDeploymentById(now, idOrHost)
return deployment
} catch (error) {
if (error.status === 404) {
return new DeploymentNotFound(idOrHost, contextName)
} else if (error.status === 403) {
return new DeploymentPermissionDenied(idOrHost, contextName)
} else {
throw error;
}
}
}
async function getDeploymentById(now: Now, id: string): Promise<{ deployment: Deployment }> {
const deployment = await now.fetch(`/v4/now/deployments/${encodeURIComponent(id)}`)
return { deployment }
}
type DeploymentHostResponse = {
deployment: {
id: string
}
}
async function getDeploymentByHost(now: Now, host: string): Promise<{ deployment: Deployment }> {
const response: DeploymentHostResponse = await now.fetch(`/v4/now/hosts/${encodeURIComponent(host)}?resolve=1`)
return getDeploymentById(now, response.deployment.id)
}
export default getDeploymentByIdOrHost

View File

@@ -0,0 +1,15 @@
// @flow
import { Now } from '../types'
import { DeploymentPermissionDenied, DeploymentNotFound } from '../errors'
import getDeploymentByIdOrHost from './get-deployment-by-id-or-host'
async function getDeploymentOrFail(now: Now, contextName: string, idOrHost: string) {
const deployment = await getDeploymentByIdOrHost(now, contextName, idOrHost)
if ((deployment instanceof DeploymentPermissionDenied) || (deployment instanceof DeploymentNotFound)) {
throw deployment
} else {
return deployment
}
}
export default getDeploymentOrFail

View File

@@ -0,0 +1,9 @@
// @flow
import { Now } from '../types'
import type { InstancesInfo } from '../types'
async function getDeploymentInstances(now: Now, deploymentId: string, requestId: string): Promise<InstancesInfo> {
return now.fetch(`/v3/now/deployments/${encodeURIComponent(deploymentId)}/instances?init=1&requestId=${requestId}`)
}
export default getDeploymentInstances

View File

@@ -0,0 +1,48 @@
// @flow
import through2 from 'through2'
import jsonlines from 'jsonlines'
import { stringify } from 'querystring'
import type { Readable } from 'stream'
import { Now } from '../../util/types'
import noop from '../../../../util/noop'
type Options = {
direction: 'forward' | 'backwards',
follow: boolean,
format?: 'lines',
instanceId?: string,
limit?: number,
query?: string,
since?: number,
types?: string[],
until?: number,
}
async function getEventsStream(now: Now, idOrHost: string, options: Options): Promise<Readable> {
const response = await now.fetch(`/v2/now/deployments/${idOrHost}/events?${stringify({
direction: options.direction,
follow: options.follow ? '1' : '',
format: options.format || 'lines',
instanceId: options.instanceId,
limit: options.limit,
q: options.query,
since: options.since,
types: (options.types || []).join(','),
until: options.until
})}`)
const stream = response.readable ? await response.readable() : response.body
const pipeStream = stream.pipe(jsonlines.parse()).pipe(ignoreEmptyObjects)
stream.on('error', noop)
pipeStream.on('error', noop)
return pipeStream
}
// Since we will be receiving empty object from the stream, this transform will ignore them
const ignoreEmptyObjects = through2.obj(function (chunk, enc, cb) {
if (Object.keys(chunk).length !== 0) {
this.push(chunk)
}
cb();
})
export default getEventsStream

View File

@@ -0,0 +1,14 @@
// @flow
export default function getInstanceIndex() {
const instancesIndex = {}
let items = 0
return (instanceId: string) => {
if (instancesIndex[instanceId] === undefined) {
instancesIndex[instanceId] = items
items += 1
}
return instancesIndex[instanceId]
}
}

Some files were not shown because too many files have changed in this diff Show More