Skip to content

Commit f23f077

Browse files
committed
feat: add better system env support
This PR adds support for a --load-system-env flag. This works similar to the existing --load-local-env variable on the start command but is required to be used in tandem with the --env flag and will use the file specified in --env as a filter. Only empty values in the env file will be populated by the system env variables. This PR doesn't add that behavior to the "start" command as this would be a breaking change that will be introduced in a separate version. fix #144
1 parent 79af28c commit f23f077

File tree

10 files changed

+242
-78
lines changed

10 files changed

+242
-78
lines changed

__tests__/config/utils/credentials.test.ts

+20-52
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
jest.mock('../../../src/config/utils/env');
2-
31
import { getCredentialsFromFlags } from '../../../src/config/utils/credentials';
42

53
const baseFlags = {
64
config: '.twilio-function',
75
cwd: process.cwd(),
86
logLevel: 'info' as 'info',
7+
loadSystemEnv: false,
98
};
109

1110
describe('getCredentialsFromFlags', () => {
1211
test('should return empty if nothing is passed', async () => {
13-
require('../../../src/config/utils/env').__setVariables({}, '');
14-
15-
const credentials = await getCredentialsFromFlags(baseFlags, undefined);
12+
const credentials = await getCredentialsFromFlags(baseFlags, {}, undefined);
1613
expect(credentials).toEqual({
1714
accountSid: '',
1815
authToken: '',
1916
});
2017
});
2118

2219
test('should return flag values if passed', async () => {
23-
require('../../../src/config/utils/env').__setVariables({}, '');
24-
2520
const credentials = await getCredentialsFromFlags(
2621
{ ...baseFlags, accountSid: 'ACxxxxx', authToken: 'some-token' },
22+
{},
2723
undefined
2824
);
2925
expect(credentials).toEqual({
@@ -33,16 +29,12 @@ describe('getCredentialsFromFlags', () => {
3329
});
3430

3531
test('should read from env file', async () => {
36-
require('../../../src/config/utils/env').__setVariables(
32+
const credentials = await getCredentialsFromFlags(
33+
{ ...baseFlags },
3734
{
3835
ACCOUNT_SID: 'ACyyyyyyyyy',
3936
AUTH_TOKEN: 'example-token',
4037
},
41-
''
42-
);
43-
44-
const credentials = await getCredentialsFromFlags(
45-
{ ...baseFlags },
4638
undefined
4739
);
4840
expect(credentials).toEqual({
@@ -52,10 +44,9 @@ describe('getCredentialsFromFlags', () => {
5244
});
5345

5446
test('should take external default options if nothing is passed', async () => {
55-
require('../../../src/config/utils/env').__setVariables({}, '');
56-
5747
const credentials = await getCredentialsFromFlags(
5848
{ ...baseFlags },
49+
{},
5950
{ username: 'ACzzzzzzz', password: 'api-secret', profile: undefined }
6051
);
6152
expect(credentials).toEqual({
@@ -65,16 +56,12 @@ describe('getCredentialsFromFlags', () => {
6556
});
6657

6758
test('env variables should override external default options', async () => {
68-
require('../../../src/config/utils/env').__setVariables(
59+
const credentials = await getCredentialsFromFlags(
60+
{ ...baseFlags },
6961
{
7062
ACCOUNT_SID: 'ACyyyyyyyyy',
7163
AUTH_TOKEN: 'example-token',
7264
},
73-
''
74-
);
75-
76-
const credentials = await getCredentialsFromFlags(
77-
{ ...baseFlags },
7865
{ username: 'ACzzzzzzz', password: 'api-secret', profile: undefined }
7966
);
8067
expect(credentials).toEqual({
@@ -84,16 +71,12 @@ describe('getCredentialsFromFlags', () => {
8471
});
8572

8673
test('external options with profile should override env variables', async () => {
87-
require('../../../src/config/utils/env').__setVariables(
74+
const credentials = await getCredentialsFromFlags(
75+
{ ...baseFlags },
8876
{
8977
ACCOUNT_SID: 'ACyyyyyyyyy',
9078
AUTH_TOKEN: 'example-token',
9179
},
92-
''
93-
);
94-
95-
const credentials = await getCredentialsFromFlags(
96-
{ ...baseFlags },
9780
{ username: 'ACzzzzzzz', password: 'api-secret', profile: 'demo' }
9881
);
9982
expect(credentials).toEqual({
@@ -105,18 +88,15 @@ describe('getCredentialsFromFlags', () => {
10588
test('external options with project should override env variables', async () => {
10689
// project flag is deprecated and removed in v3 @twilio/cli-core but
10790
// included here just to make sure
108-
require('../../../src/config/utils/env').__setVariables(
109-
{
110-
ACCOUNT_SID: 'ACyyyyyyyyy',
111-
AUTH_TOKEN: 'example-token',
112-
},
113-
''
114-
);
11591

11692
const credentials = await getCredentialsFromFlags(
11793
{
11894
...baseFlags,
11995
},
96+
{
97+
ACCOUNT_SID: 'ACyyyyyyyyy',
98+
AUTH_TOKEN: 'example-token',
99+
},
120100
{
121101
username: 'ACzzzzzzz',
122102
password: 'api-secret',
@@ -130,16 +110,12 @@ describe('getCredentialsFromFlags', () => {
130110
});
131111

132112
test('should prefer external CLI if profile is passed', async () => {
133-
require('../../../src/config/utils/env').__setVariables(
113+
const credentials = await getCredentialsFromFlags(
114+
{ ...baseFlags },
134115
{
135116
ACCOUNT_SID: 'ACyyyyyyyyy',
136117
AUTH_TOKEN: 'example-token',
137118
},
138-
''
139-
);
140-
141-
const credentials = await getCredentialsFromFlags(
142-
{ ...baseFlags },
143119
{ username: 'ACzzzzzzz', password: 'api-secret', profile: 'demo' }
144120
);
145121
expect(credentials).toEqual({
@@ -151,16 +127,12 @@ describe('getCredentialsFromFlags', () => {
151127
test('should prefer external CLI if project is passed', async () => {
152128
// project flag is deprecated and removed in v3 @twilio/cli-core but
153129
// included here just to make sure
154-
require('../../../src/config/utils/env').__setVariables(
130+
const credentials = await getCredentialsFromFlags(
131+
{ ...baseFlags },
155132
{
156133
ACCOUNT_SID: 'ACyyyyyyyyy',
157134
AUTH_TOKEN: 'example-token',
158135
},
159-
''
160-
);
161-
162-
const credentials = await getCredentialsFromFlags(
163-
{ ...baseFlags },
164136
{ username: 'ACzzzzzzz', password: 'api-secret', project: 'demo' }
165137
);
166138
expect(credentials).toEqual({
@@ -170,16 +142,12 @@ describe('getCredentialsFromFlags', () => {
170142
});
171143

172144
test('should prefer flag over everything', async () => {
173-
require('../../../src/config/utils/env').__setVariables(
145+
const credentials = await getCredentialsFromFlags(
146+
{ ...baseFlags, accountSid: 'ACxxxxx', authToken: 'some-token' },
174147
{
175148
ACCOUNT_SID: 'ACyyyyyyyyy',
176149
AUTH_TOKEN: 'example-token',
177150
},
178-
''
179-
);
180-
181-
const credentials = await getCredentialsFromFlags(
182-
{ ...baseFlags, accountSid: 'ACxxxxx', authToken: 'some-token' },
183151
{
184152
username: 'ACzzzzzzz',
185153
password: 'api-secret',

__tests__/config/utils/env.test.ts

+149-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import { filterEnvVariablesForDeploy } from '../../../src/config/utils/env';
1+
import { stripIndent } from 'common-tags';
2+
import mockFs from 'mock-fs';
3+
import path from 'path';
4+
import {
5+
filterEnvVariablesForDeploy,
6+
readLocalEnvFile,
7+
} from '../../../src/config/utils/env';
28
import { EnvironmentVariablesWithAuth } from '../../../src/types/generic';
39

10+
function normalize(unixPath) {
11+
return path.join(...unixPath.split('/'));
12+
}
13+
414
describe('filterEnvVariablesForDeploy', () => {
515
const testVars: EnvironmentVariablesWithAuth = {
616
ACCOUNT_SID: 'ACCOUNT_SID',
@@ -25,3 +35,141 @@ describe('filterEnvVariablesForDeploy', () => {
2535
expect(deployVars['hello']).toEqual('world');
2636
});
2737
});
38+
39+
describe('readLocalEnvFile', () => {
40+
let backupSystemEnv = {};
41+
42+
const baseFlags = {
43+
cwd: '/tmp/project',
44+
env: undefined,
45+
loadSystemEnv: false,
46+
};
47+
48+
beforeEach(() => {
49+
mockFs({
50+
'/tmp/project': {
51+
'.env': stripIndent`
52+
ACCOUNT_SID=ACxxxxxxx
53+
AUTH_TOKEN=123456789f
54+
MY_PHONE_NUMBER=+12345
55+
SECRET_API_KEY=abc
56+
`,
57+
'.env.prod': stripIndent`
58+
ACCOUNT_SID=
59+
AUTH_TOKEN=
60+
MY_PHONE_NUMBER=+3333333
61+
SECRET_API_KEY=
62+
`,
63+
},
64+
'/tmp/project-two': {
65+
'.env': stripIndent`
66+
ACCOUNT_SID=ACyyyyyyy
67+
AUTH_TOKEN=123456789a
68+
TWILIO=https://www.twilio.com
69+
`,
70+
'.env.prod': stripIndent`
71+
ACCOUNT_SID=
72+
AUTH_TOKEN=
73+
TWILIO=https://www.twilio.com
74+
`,
75+
},
76+
});
77+
backupSystemEnv = { ...process.env };
78+
});
79+
80+
afterEach(() => {
81+
mockFs.restore();
82+
process.env = { ...backupSystemEnv };
83+
});
84+
85+
it('should throw an error if you use --load-system-env without --env', async () => {
86+
const errorMessage = stripIndent`
87+
If you are using --load-system-env you'll also have to supply a --env flag.
88+
89+
The .env file you are pointing at will be used to primarily load environment variables.
90+
Any empty entries in the .env file will fall back to the system's environment variables.
91+
`;
92+
93+
expect(
94+
readLocalEnvFile({ ...baseFlags, loadSystemEnv: true })
95+
).rejects.toEqual(new Error(errorMessage));
96+
});
97+
98+
it('should load the default env variables', async () => {
99+
expect(await readLocalEnvFile(baseFlags)).toEqual({
100+
localEnv: {
101+
ACCOUNT_SID: 'ACxxxxxxx',
102+
AUTH_TOKEN: '123456789f',
103+
MY_PHONE_NUMBER: '+12345',
104+
SECRET_API_KEY: 'abc',
105+
},
106+
envPath: normalize('/tmp/project/.env'),
107+
});
108+
});
109+
110+
it('should load env variables from a different filename', async () => {
111+
expect(await readLocalEnvFile({ ...baseFlags, env: '.env.prod' })).toEqual({
112+
localEnv: {
113+
ACCOUNT_SID: '',
114+
AUTH_TOKEN: '',
115+
MY_PHONE_NUMBER: '+3333333',
116+
SECRET_API_KEY: '',
117+
},
118+
envPath: normalize('/tmp/project/.env.prod'),
119+
});
120+
});
121+
122+
it('should load the default env variables with different cwd', async () => {
123+
expect(
124+
await readLocalEnvFile({ ...baseFlags, cwd: '/tmp/project-two' })
125+
).toEqual({
126+
localEnv: {
127+
ACCOUNT_SID: 'ACyyyyyyy',
128+
AUTH_TOKEN: '123456789a',
129+
TWILIO: 'https://www.twilio.com',
130+
},
131+
envPath: normalize('/tmp/project-two/.env'),
132+
});
133+
});
134+
135+
it('should load env variables from a different filename & cwd', async () => {
136+
expect(
137+
await readLocalEnvFile({
138+
...baseFlags,
139+
cwd: '/tmp/project-two',
140+
env: '.env.prod',
141+
})
142+
).toEqual({
143+
localEnv: {
144+
ACCOUNT_SID: '',
145+
AUTH_TOKEN: '',
146+
TWILIO: 'https://www.twilio.com',
147+
},
148+
envPath: normalize('/tmp/project-two/.env.prod'),
149+
});
150+
});
151+
152+
it('should fallback to system env variables for empty variables with loadSystemEnv', async () => {
153+
process.env = {
154+
TWILIO: 'https://www.twilio.com/blog',
155+
ACCOUNT_SID: 'ACzzzzzzz',
156+
SECRET_API_KEY: 'psst',
157+
};
158+
159+
expect(
160+
await readLocalEnvFile({
161+
...baseFlags,
162+
env: '.env.prod',
163+
loadSystemEnv: true,
164+
})
165+
).toEqual({
166+
localEnv: {
167+
ACCOUNT_SID: 'ACzzzzzzz',
168+
AUTH_TOKEN: '',
169+
MY_PHONE_NUMBER: '+3333333',
170+
SECRET_API_KEY: 'psst',
171+
},
172+
envPath: normalize('/tmp/project/.env.prod'),
173+
});
174+
});
175+
});

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"@types/lodash.flatten": "^4.4.6",
9595
"@types/lodash.kebabcase": "^4.1.6",
9696
"@types/lodash.startcase": "^4.4.6",
97+
"@types/mock-fs": "^4.10.0",
9798
"@types/prompts": "^2.0.1",
9899
"@types/supertest": "^2.0.8",
99100
"@types/title": "^1.0.5",
@@ -107,6 +108,7 @@
107108
"jest-express": "^1.10.1",
108109
"lint-staged": "^8.2.1",
109110
"listr-silent-renderer": "^1.1.1",
111+
"mock-fs": "^4.12.0",
110112
"nock": "^12.0.2",
111113
"npm-run-all": "^4.1.5",
112114
"prettier": "^1.18.2",

src/commands/shared.ts

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type SharedFlagsWithCredentials = SharedFlags & {
1616
env?: string;
1717
region?: string;
1818
edge?: string;
19+
loadSystemEnv: boolean;
1920
};
2021

2122
export type ExternalCliOptions = {
@@ -60,6 +61,12 @@ export const sharedApiRelatedCliOptions: { [key: string]: Options } = {
6061
describe:
6162
'Use a specific auth token for deployment. Uses fields from .env otherwise',
6263
},
64+
'load-system-env': {
65+
default: false,
66+
type: 'boolean',
67+
describe:
68+
'Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified.',
69+
},
6370
};
6471

6572
export const sharedCliOptions: { [key: string]: Options } = {

0 commit comments

Comments
 (0)