Skip to content

Commit a4b8601

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 a4b8601

File tree

10 files changed

+237
-78
lines changed

10 files changed

+237
-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

+144-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { filterEnvVariablesForDeploy } from '../../../src/config/utils/env';
1+
import { stripIndent } from 'common-tags';
2+
import mockFs from 'mock-fs';
3+
import {
4+
filterEnvVariablesForDeploy,
5+
readLocalEnvFile,
6+
} from '../../../src/config/utils/env';
27
import { EnvironmentVariablesWithAuth } from '../../../src/types/generic';
38

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

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)