Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(twilio-run:start): options for ngrok config and named tunnel #177

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/serverless-runtime-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"url": "https://github.com/twilio-labs/serverless-toolkit/issues"
},
"dependencies": {
"twilio": "^3.33.0"
"twilio": "^3.60.0"
},
"devDependencies": {
"@types/express": "^4.17.11",
Expand Down
24 changes: 23 additions & 1 deletion packages/twilio-run/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,32 @@ twilio-run --inspect
# Exposes the Twilio functions via ngrok to share them
twilio-run --ngrok

# Exposes the Twilio functions via ngrok using a custom subdomain (requires a paid-for ngrok account)
# Uses a custom project ngrok config and named tunnel to expose the functions via ngrok
twilio-run --ngrok --ngrok-config=./ngrok.yml --ngrok-name=example
```

#### ngrok

`twilio-run` lets you open a tunnel using [ngrok](https://ngrok.com/) (see the examples above). By default, just setting the `--ngrok` flag will use a randomly generated subdomain. If you have a paid for ngrok account, you can customise this experience.

##### Custom ngrok subdomain

Run the following command:

```
twilio-run --ngrok=subdomain
```

This will start the `twilio-run` server and also tunnel to it over ngrok via the domain `subdomain.ngrok.io`.

##### Custom ngrok config

You can create an [ngrok config file](https://ngrok.com/docs#config-location) which allows you to name tunnels and include other settings for those named tunnels, such as the subdomain, auth, or host headers.

You can choose to run a named tunnel with `twilio-run` by passing the `--ngrok-name` flag. This will find the named tunnel in your default config, over-ride the `proto` to http and the `addr` to the port your Twilio Functions are running on, otherwise keeping the rest of the settings.

You can also choose to use a different config file by setting the `--ngrok-config` flag.

### `twilio-run deploy`

Deploys your project to Twilio. It will read dependencies automatically from your `package.json`'s `dependencies` field and install them. It will also upload and set the variables that are specified in your `.env` file. You can point it against a different `.env` file via command-line flags.
Expand Down
5 changes: 3 additions & 2 deletions packages/twilio-run/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@
"serialize-error": "^7.0.1",
"terminal-link": "^1.3.0",
"title": "^3.4.1",
"twilio": "^3.43.1",
"twilio": "^3.60.0",
"type-fest": "^0.15.1",
"window-size": "^1.1.1",
"wrap-ansi": "^5.1.0",
"yaml": "^1.10.0",
"yargs": "^13.2.2"
},
"optionalDependencies": {
"ngrok": "^3.3.0"
"ngrok": "^4.0.1"
},
"devDependencies": {
"@types/cheerio": "^0.22.12",
Expand Down
8 changes: 8 additions & 0 deletions packages/twilio-run/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ export const cliInfo: CliInfo = {
describe:
'Uses ngrok to create a public url. Pass a string to set the subdomain (requires a paid-for ngrok account).',
},
'ngrok-config': {
type: 'string',
describe: 'Path to custom ngrok config for project specific config.',
},
'ngrok-name': {
type: 'string',
describe: 'Name of ngrok tunnel config.',
},
logs: {
type: 'boolean',
default: true,
Expand Down
87 changes: 75 additions & 12 deletions packages/twilio-run/src/config/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { EnvironmentVariables } from '@twilio-labs/serverless-api';
import dotenv from 'dotenv';
import { readFileSync } from 'fs';
import path, { resolve } from 'path';
import { readFile } from 'fs';
import { promisify } from 'util';
const readFilePromise = promisify(readFile);
import path, { resolve, join } from 'path';
import { homedir } from 'os';
import { Arguments } from 'yargs';
import { ExternalCliOptions, SharedFlags } from '../commands/shared';
import { CliInfo } from '../commands/types';
Expand All @@ -10,14 +13,11 @@ import { fileExists } from '../utils/fs';
import { getDebugFunction, logger } from '../utils/logger';
import { readSpecializedConfig } from './global';
import { mergeFlagsAndConfig } from './utils/mergeFlagsAndConfig';
import { Ngrok } from 'ngrok';
import { parse } from 'yaml';

const debug = getDebugFunction('twilio-run:cli:config');

type NgrokConfig = {
addr: string | number;
subdomain?: string;
};

type InspectInfo = {
hostPort: string;
break: boolean;
Expand Down Expand Up @@ -47,6 +47,8 @@ export type StartCliFlags = Arguments<
env?: string;
port: string;
ngrok?: string | boolean;
ngrokConfig?: string;
ngrokName?: string;
logs: boolean;
detailedLogs: boolean;
live: boolean;
Expand All @@ -67,12 +69,73 @@ export async function getUrl(cli: StartCliFlags, port: string | number) {
let url = `http://localhost:${port}`;
if (typeof cli.ngrok !== 'undefined') {
debug('Starting ngrok tunnel');
const ngrokConfig: NgrokConfig = { addr: port };
// Setup default ngrok config, setting the protocol and the port number to
// forward to.
const defaultConfig: Ngrok.Options = { addr: port, proto: 'http' };
let tunnelConfig = defaultConfig;
let ngrokConfig;
if (typeof cli.ngrokConfig === 'string') {
// If we set a config path then try to load that config. If the config
// fails to load then we'll try to load the default config instead.
const configPath = join(cli.cwd || process.cwd(), cli.ngrokConfig);
try {
ngrokConfig = parse(await readFilePromise(configPath, 'utf-8'));
} catch (err) {
logger.warn(`Could not find ngrok config file at ${configPath}`);
}
}
if (!ngrokConfig) {
// Try to load default config. If there is no default config file, set
// `ngrokConfig` to be an empty object.
const configPath = join(homedir(), '.ngrok2', 'ngrok.yml');
try {
ngrokConfig = parse(await readFilePromise(configPath, 'utf-8'));
} catch (err) {
ngrokConfig = {};
}
}
if (
typeof cli.ngrokName === 'string' &&
typeof ngrokConfig.tunnels === 'object'
) {
// If we've asked for a named ngrok tunnel and there are available tunnels
// in the config, then set the `tunnelConfig` to the options from the
// config, overriding the addr and proto to the defaults.
tunnelConfig = { ...ngrokConfig.tunnels[cli.ngrokName], ...tunnelConfig };
if (!tunnelConfig) {
// If the config does not include the named tunnel, then set it back to
// the default options.
logger.warn(
`Could not find config for named tunnel "${cli.ngrokName}". Falling back to other options.`
);
tunnelConfig = defaultConfig;
}
}
if (typeof ngrokConfig.authtoken === 'string') {
// If there is an authtoken in the config, add it to the tunnel config.
tunnelConfig.authToken = ngrokConfig.authtoken;
}
if (typeof cli.ngrok === 'string' && cli.ngrok.length > 0) {
ngrokConfig.subdomain = cli.ngrok;
// If we've asked for a custom subdomain, override the tunnel config with
// it.
tunnelConfig.subdomain = cli.ngrok;
}
const ngrok = require('ngrok');
try {
// Try to open the ngrok tunnel.
url = await ngrok.connect(tunnelConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not just use configPath option passed into ngrok.connect and avoid trying to manually parse out params for ngrok? We could definitely do a fs.exist(...) on the file to ensure presence, but we should not be required to fallback and load the default ngrok file stored in the home location (that's free by default).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the ngrok tunnel to connect to the http server we're starting on the port we're starting it on. The config options could be trying to setup a tcp tunnel to a different port. So, I chose to parse the config and turn it into something that will work within the context of a twilio-run powered application.

Note, even if you create a named tunnel that points to port 3000, if we start twilio-run and something else is already running on port 3000 we offer a new random port number to start on instead. Parsing and manipulating the config in this way will result in a correct tunnel in that case, which would be the expected behaviour.

My idea here is that the tunnel we create will always point to the server we are running, even if it overrides the actual config, because that is what the user would expect to happen.

} catch (error) {
// If it fails, it is likely to be because the tunnel config we pass is
// not allowed (e.g. using a custom subdomain without an authtoken). The
// error message from ngrok itself should describe the issue.
logger.warn(error.message);
if (
typeof error.details !== 'undefined' &&
typeof error.details.err !== 'undefined'
) {
logger.warn(error.details.err);
}
}

url = await require('ngrok').connect(ngrokConfig);
debug('ngrok tunnel URL: %s', url);
}

Expand Down Expand Up @@ -106,7 +169,7 @@ export async function getEnvironment(
if (await fileExists(fullEnvPath)) {
try {
debug(`Read .env file at "%s"`, fullEnvPath);
const envContent = readFileSync(fullEnvPath, 'utf8');
const envContent = await readFilePromise(fullEnvPath, 'utf8');
const envValues = dotenv.parse(envContent);
for (const [key, val] of Object.entries(envValues)) {
env[key] = val;
Expand Down