mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-24 11:49:13 +00:00
Compare commits
180 Commits
@now/cgi@0
...
now-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1b099285 | ||
|
|
87b1f5a5fb | ||
|
|
ae8cbdce0a | ||
|
|
95de48c03f | ||
|
|
5ea4fd9a5e | ||
|
|
1f3d9e7e0f | ||
|
|
05e4528b1c | ||
|
|
40789776bc | ||
|
|
75759edd26 | ||
|
|
2a5dc1dba6 | ||
|
|
0cbf50b7ff | ||
|
|
a3f5b805bb | ||
|
|
c337877501 | ||
|
|
a6e9dd850a | ||
|
|
a14d4057de | ||
|
|
210240ce66 | ||
|
|
d21f7fe75c | ||
|
|
54805cd2a0 | ||
|
|
c7da2e732a | ||
|
|
4b4beaa892 | ||
|
|
e13596acae | ||
|
|
8eb065181f | ||
|
|
1d97219fef | ||
|
|
d312f94825 | ||
|
|
478a6f7369 | ||
|
|
e309123296 | ||
|
|
777646cb0e | ||
|
|
39e23f7144 | ||
|
|
b68b5c0ea3 | ||
|
|
46dfeb8ca9 | ||
|
|
6771ad43af | ||
|
|
1881b9c6cb | ||
|
|
dae6b7a980 | ||
|
|
aed6209f6a | ||
|
|
b44a590aa0 | ||
|
|
936b5fc983 | ||
|
|
473d6617f2 | ||
|
|
fcf50b5aeb | ||
|
|
a3835d2e8a | ||
|
|
d89481966e | ||
|
|
3148ce0f31 | ||
|
|
f65363856a | ||
|
|
e205b57352 | ||
|
|
f44a1dbf21 | ||
|
|
b7fe2f606c | ||
|
|
6e81c1795c | ||
|
|
141d8e4467 | ||
|
|
edbfeab75f | ||
|
|
e48ea10ebb | ||
|
|
8565af5526 | ||
|
|
511d3bae8f | ||
|
|
8df233fef1 | ||
|
|
4b97410cc0 | ||
|
|
aadf8097d1 | ||
|
|
40d8b74b1b | ||
|
|
8c4d42891c | ||
|
|
3672374d23 | ||
|
|
9ff9d3f174 | ||
|
|
86540fa7fd | ||
|
|
1538c80a7d | ||
|
|
0225fcfe51 | ||
|
|
57f3861de6 | ||
|
|
04c365a251 | ||
|
|
7dfe4690ce | ||
|
|
92bcf1b7c9 | ||
|
|
8a15d5c65a | ||
|
|
f64374225d | ||
|
|
7537eac6a7 | ||
|
|
e2880d2434 | ||
|
|
8296de16ef | ||
|
|
5cff5e9dfd | ||
|
|
cb07a748c2 | ||
|
|
c7b985bdc6 | ||
|
|
bf32ca0e4a | ||
|
|
a4e52de0e3 | ||
|
|
e43e9b11a0 | ||
|
|
847b9e97c4 | ||
|
|
baad689286 | ||
|
|
5e7afc4385 | ||
|
|
d724b7a631 | ||
|
|
5e17fe5ad6 | ||
|
|
b216adadc0 | ||
|
|
5078c95667 | ||
|
|
7612d77647 | ||
|
|
0d76041c10 | ||
|
|
59be596d24 | ||
|
|
63e51a3c98 | ||
|
|
7f3128b3e5 | ||
|
|
c235813ae7 | ||
|
|
3ee18e7051 | ||
|
|
14fc5d8796 | ||
|
|
9fb0077385 | ||
|
|
2e9c7265b6 | ||
|
|
d9e77b784a | ||
|
|
b5b296ad7f | ||
|
|
acdfde5aa2 | ||
|
|
0eddfbd28c | ||
|
|
037f0610bc | ||
|
|
dd45f8f2ab | ||
|
|
d454c84f61 | ||
|
|
a4c98e07a5 | ||
|
|
85715630bd | ||
|
|
45d8d4a84f | ||
|
|
690882c97a | ||
|
|
e03836e4a1 | ||
|
|
d4ec54135a | ||
|
|
b29785d851 | ||
|
|
148870d706 | ||
|
|
ed1f7e335d | ||
|
|
eaf4695194 | ||
|
|
c312d42302 | ||
|
|
a8a2a6c066 | ||
|
|
8b3512cb07 | ||
|
|
945facdd2c | ||
|
|
8676ed4cff | ||
|
|
e4d4afa840 | ||
|
|
81cf286ea4 | ||
|
|
e834625728 | ||
|
|
a7c22eb08c | ||
|
|
837c358371 | ||
|
|
9743db27e7 | ||
|
|
ce725143e6 | ||
|
|
677805c33a | ||
|
|
3d3f1fe39b | ||
|
|
123c68ad2b | ||
|
|
86861c58af | ||
|
|
d4ddb6b3f9 | ||
|
|
0fc7de40d4 | ||
|
|
3cb4a9c8dd | ||
|
|
d681289457 | ||
|
|
bc1c3c3f5b | ||
|
|
5ae4287c0f | ||
|
|
cde9f56886 | ||
|
|
2b6b006bbd | ||
|
|
54e1bcafc0 | ||
|
|
7e98d0a22b | ||
|
|
c27b4a6aaf | ||
|
|
8f17ffd817 | ||
|
|
0a7d688d32 | ||
|
|
8cb2fe1284 | ||
|
|
e6e375232e | ||
|
|
08c4ab8a0c | ||
|
|
f86647fc26 | ||
|
|
f310f6a86f | ||
|
|
58c6acd265 | ||
|
|
c7d97e3866 | ||
|
|
0890144c61 | ||
|
|
69a7d91b57 | ||
|
|
a9016c88f6 | ||
|
|
e8990742cf | ||
|
|
f51400a3a1 | ||
|
|
e763ee5301 | ||
|
|
fd978699e8 | ||
|
|
bbd9585829 | ||
|
|
54cf1ebb31 | ||
|
|
6fc77c6cfa | ||
|
|
f8372e3bb9 | ||
|
|
6728be7b1a | ||
|
|
65e1a1e731 | ||
|
|
cd9478e853 | ||
|
|
b9364ed4fc | ||
|
|
2715e8e9d8 | ||
|
|
f987c93cf0 | ||
|
|
5c254a7151 | ||
|
|
f253e29f33 | ||
|
|
f37fa13eab | ||
|
|
64765b393a | ||
|
|
54c84b4ce0 | ||
|
|
146bcba794 | ||
|
|
d3dd1b731d | ||
|
|
d608ee7390 | ||
|
|
4e2e0950c7 | ||
|
|
ddc7e97ab6 | ||
|
|
a21759ee42 | ||
|
|
cc4beb94cf | ||
|
|
0bfafa9311 | ||
|
|
4eefc34629 | ||
|
|
4d3f882dc0 | ||
|
|
3a802fbb70 | ||
|
|
6f00b03d24 |
37
.editorconfig
Normal file
37
.editorconfig
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
[](https://circleci.com/gh/zeit/workflows/now-cli) [](https://spectrum.chat/now)
|
||||
[](https://circleci.com/gh/zeit/workflows/now-cli) [](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).
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
} ]
|
||||
}
|
||||
}
|
||||
|
||||
27
errors/cant-solve-challenge.md
Normal file
27
errors/cant-solve-challenge.md
Normal 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 couldn’t solve the requested challenges.
|
||||
|
||||
## How to Fix It
|
||||
|
||||
If your domain is pointing to ZEIT World DNS and you’re 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 you’re 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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
23
errors/dns-configuration-error.md
Normal file
23
errors/dns-configuration-error.md
Normal 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 you’re 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 you’re 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.
|
||||
@@ -13,7 +13,7 @@ with default scale settings.
|
||||
|
||||
```json
|
||||
{
|
||||
"regions": ["sfo", "bru"]
|
||||
"regions": ["sfo", "bru", "gru", "iad"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
20
errors/solve-challenges-manually.md
Normal file
20
errors/solve-challenges-manually.md
Normal 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
17
errors/v2-no-min.md
Normal 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
|
||||
```
|
||||
|
||||
29
errors/verification-timeout.md
Normal file
29
errors/verification-timeout.md
Normal 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>`)
|
||||
37
package.json
37
package.json
@@ -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",
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
91
src/now.js
91
src/now.js
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
15
src/providers/sh/commands/alias/find-alias-by-alias-or-id.js
Normal file
15
src/providers/sh/commands/alias/find-alias-by-alias-or-id.js
Normal 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))}`);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
111
src/providers/sh/commands/alias/ls.js
Normal file
111
src/providers/sh/commands/alias/ls.js
Normal 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'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
|
||||
73
src/providers/sh/commands/alias/rm.js
Normal file
73
src/providers/sh/commands/alias/rm.js
Normal 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?'))
|
||||
}
|
||||
@@ -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
|
||||
293
src/providers/sh/commands/alias/set.js
Normal file
293
src/providers/sh/commands/alias/set.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)}`
|
||||
))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
165
src/providers/sh/commands/certs/issue.js
Normal file
165
src/providers/sh/commands/certs/issue.js
Normal 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
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
1
src/providers/sh/commands/deploy/index.js
Normal file
1
src/providers/sh/commands/deploy/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./deploy')
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
102
src/providers/sh/commands/dns/add.js
Normal file
102
src/providers/sh/commands/dns/add.js
Normal 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
|
||||
105
src/providers/sh/commands/dns/index.js
Normal file
105
src/providers/sh/commands/dns/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
91
src/providers/sh/commands/dns/ls.js
Normal file
91
src/providers/sh/commands/dns/ls.js
Normal 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
|
||||
77
src/providers/sh/commands/dns/rm.js
Normal file
77
src/providers/sh/commands/dns/rm.js
Normal 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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
150
src/providers/sh/commands/domains/add.js
Normal file
150
src/providers/sh/commands/domains/add.js
Normal 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 can’t 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`)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
110
src/providers/sh/commands/domains/index.js
Normal file
110
src/providers/sh/commands/domains/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
64
src/providers/sh/commands/domains/ls.js
Normal file
64
src/providers/sh/commands/domains/ls.js
Normal 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;
|
||||
104
src/providers/sh/commands/domains/rm.js
Normal file
104
src/providers/sh/commands/domains/rm.js
Normal 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;
|
||||
@@ -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`);
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
`)
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
12
src/providers/sh/util/alias/get-aliases.js
Normal file
12
src/providers/sh/util/alias/get-aliases.js
Normal 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
|
||||
8
src/providers/sh/util/alias/remove-alias-by-id.js
Normal file
8
src/providers/sh/util/alias/remove-alias-by-id.js
Normal 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'
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
10
src/providers/sh/util/billing/get-credit-cards.js
Normal file
10
src/providers/sh/util/billing/get-credit-cards.js
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
49
src/providers/sh/util/certs/finish-cert-order.js
Normal file
49
src/providers/sh/util/certs/finish-cert-order.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
10
src/providers/sh/util/certs/get-certs-for-domain.js
Normal file
10
src/providers/sh/util/certs/get-certs-for-domain.js
Normal 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
|
||||
6
src/providers/sh/util/certs/get-cns-from-args.js
Normal file
6
src/providers/sh/util/certs/get-cns-from-args.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function getCnsFromArgs(args) {
|
||||
return args.reduce((res, item) => ([
|
||||
...res,
|
||||
...item.split(',')]
|
||||
), []).filter(i => i)
|
||||
}
|
||||
23
src/providers/sh/util/certs/start-cert-order.js
Normal file
23
src/providers/sh/util/certs/start-cert-order.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
68
src/providers/sh/util/deploy/create-deploy.js
Normal file
68
src/providers/sh/util/deploy/create-deploy.js
Normal 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
|
||||
}
|
||||
}
|
||||
46
src/providers/sh/util/deploy/generate-cert-for-deploy.js
Normal file
46
src/providers/sh/util/deploy/generate-cert-for-deploy.js
Normal 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()
|
||||
}
|
||||
}
|
||||
40
src/providers/sh/util/deploy/get-deployment-by-id-or-host.js
Normal file
40
src/providers/sh/util/deploy/get-deployment-by-id-or-host.js
Normal 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
|
||||
@@ -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
|
||||
9
src/providers/sh/util/deploy/get-deployment-instances.js
Normal file
9
src/providers/sh/util/deploy/get-deployment-instances.js
Normal 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
|
||||
48
src/providers/sh/util/deploy/get-events-stream.js
Normal file
48
src/providers/sh/util/deploy/get-events-stream.js
Normal 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
|
||||
14
src/providers/sh/util/deploy/get-instance-index.js
Normal file
14
src/providers/sh/util/deploy/get-instance-index.js
Normal 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
Reference in New Issue
Block a user