@adobe/aio-cli-plugin-api-mesh 3.3.0 → 3.3.1-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/oclif.manifest.json +1 -1
- package/package.json +5 -3
- package/src/commands/__fixtures__/sample_secrets_mesh.json +18 -0
- package/src/commands/__fixtures__/secrets_invalid.yaml +3 -0
- package/src/commands/__fixtures__/secrets_valid.yaml +2 -0
- package/src/commands/__fixtures__/secrets_with_batch_variables.yaml +4 -0
- package/src/commands/api-mesh/__tests__/create.test.js +246 -0
- package/src/commands/api-mesh/__tests__/run.test.js +126 -0
- package/src/commands/api-mesh/create.js +16 -0
- package/src/commands/api-mesh/run.js +17 -0
- package/src/commands/api-mesh/update.js +16 -0
- package/src/helpers.js +21 -0
- package/src/server.js +7 -0
- package/src/serverUtils.js +21 -0
- package/src/utils.js +104 -0
package/oclif.manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"3.3.
|
|
1
|
+
{"version":"3.3.1-alpha","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:init":{"id":"api-mesh:init","description":"This command will create a workspace where you can organise your API mesh configuration and other files","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[{"description":"API mesh workspace init","command":"aio api-mesh init commerce-mesh"},{"description":"API mesh workspace init with flags","command":"aio api-mesh init commerce-mesh --path ./mesh_projects/test_mesh --git y --packageManager yarn"}],"flags":{"path":{"name":"path","type":"option","char":"p","default":"."},"packageManager":{"name":"packageManager","type":"option","char":"m","options":["npm","yarn"]},"git":{"name":"git","type":"option","char":"g","options":["y","n"]}},"args":[{"name":"projectName","description":"Project name","required":true}]},"api-mesh:run":{"id":"api-mesh:run","description":"Run a local development server that builds and compiles a mesh locally","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[],"flags":{"port":{"name":"port","type":"option","char":"p","description":"Port number for the local dev server"},"debug":{"name":"debug","type":"boolean","description":"Enable debugging mode","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"select":{"name":"select","type":"boolean","description":"Retrieve existing artifacts from the mesh","allowNo":false},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file","description":"Mesh File"}]},"api-mesh:status":{"id":"api-mesh:status","description":"Get a mesh status with a given meshid.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm install action prompt. CLI will not check ask user to install source.","allowNo":false}},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get -s=<version>@<source_name>","$ aio api-mesh:source:get -s<source_name>","$ aio api-mesh:source:get -m"],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm print action prompt. CLI will not check ask user to print source.","allowNo":false},"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]},"api-mesh:source:install":{"id":"api-mesh:source:install","description":"Command to install the source to your API mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:install <version>@<source_name>","$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>","$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm override action prompt. CLI will not check ask user to override source.","allowNo":false},"variable":{"name":"variable","type":"option","char":"v","description":"Variables required for the source"},"variable-file":{"name":"variable-file","type":"option","char":"f","description":"Variables file path"},"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"source"}]}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aio-cli-plugin-api-mesh",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1-alpha",
|
|
4
4
|
"description": "Adobe I/O CLI plugin to develop and manage API mesh sources",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oclif-plugin"
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@graphql-mesh/plugin-http-details-extensions": "0.1.21",
|
|
53
53
|
"@graphql-mesh/runtime": "0.46.21",
|
|
54
54
|
"@graphql-mesh/soap": "0.14.25",
|
|
55
|
+
"@graphql-mesh/store": "0.9.20",
|
|
55
56
|
"@graphql-mesh/transform-encapsulate": "0.4.21",
|
|
56
57
|
"@graphql-mesh/transform-federation": "0.11.14",
|
|
57
58
|
"@graphql-mesh/transform-filter-schema": "0.15.23",
|
|
@@ -64,7 +65,6 @@
|
|
|
64
65
|
"@graphql-mesh/transform-resolvers-composition": "0.13.20",
|
|
65
66
|
"@graphql-mesh/transform-type-merging": "0.5.20",
|
|
66
67
|
"@graphql-mesh/types": "0.91.12",
|
|
67
|
-
"@graphql-mesh/store": "0.9.20",
|
|
68
68
|
"@oclif/command": "^1.6.1",
|
|
69
69
|
"@oclif/config": "^1.15.1",
|
|
70
70
|
"@oclif/core": "^1.14.1",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"child_process": "^1.0.2",
|
|
75
75
|
"content-disposition": "^0.5.4",
|
|
76
76
|
"dotenv": "^16.0.3",
|
|
77
|
+
"envsub": "^4.1.0",
|
|
77
78
|
"eslint-plugin-promise": "^6.0.0",
|
|
78
79
|
"eslint-plugin-security": "^1.5.0",
|
|
79
80
|
"eslint-plugin-sonarjs": "^0.16.0",
|
|
@@ -92,7 +93,8 @@
|
|
|
92
93
|
"pupa": "^3.1.0",
|
|
93
94
|
"source-registry-storage-adapter": "github:devx-services/source-registry-storage-adapter#main",
|
|
94
95
|
"util": "^0.12.5",
|
|
95
|
-
"uuid": "^8.3.2"
|
|
96
|
+
"uuid": "^8.3.2",
|
|
97
|
+
"yaml": "^2.4.2"
|
|
96
98
|
},
|
|
97
99
|
"devDependencies": {
|
|
98
100
|
"@babel/eslint-parser": "^7.15.8",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meshConfig": {
|
|
3
|
+
"sources": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Commerce",
|
|
6
|
+
"handler": {
|
|
7
|
+
"graphql": {
|
|
8
|
+
"endpoint": "https://venia.magento.com/graphql",
|
|
9
|
+
"operationHeaders": {
|
|
10
|
+
"Authorization": "{context.secrets.Token}"
|
|
11
|
+
},
|
|
12
|
+
"useGETForQueries": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -47,12 +47,38 @@ const {
|
|
|
47
47
|
} = require('../../../lib/devConsole');
|
|
48
48
|
|
|
49
49
|
const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type: 'entp' };
|
|
50
|
+
|
|
51
|
+
const os = require('os');
|
|
52
|
+
|
|
50
53
|
const selectedProject = { id: '5678', title: 'Project01' };
|
|
51
54
|
const selectedWorkspace = { id: '123456789', title: 'Workspace01' };
|
|
52
55
|
|
|
56
|
+
jest.mock('@adobe/aio-cli-lib-console', () => ({
|
|
57
|
+
init: jest.fn().mockResolvedValue(mockConsoleCLIInstance),
|
|
58
|
+
cleanStdOut: jest.fn(),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
jest.mock('axios');
|
|
62
|
+
jest.mock('@adobe/aio-lib-ims');
|
|
63
|
+
jest.mock('@adobe/aio-lib-env');
|
|
64
|
+
jest.mock('@adobe/aio-cli-lib-console');
|
|
65
|
+
jest.mock('../../../helpers', () => ({
|
|
66
|
+
initSdk: jest.fn().mockResolvedValue({}),
|
|
67
|
+
initRequestId: jest.fn().mockResolvedValue({}),
|
|
68
|
+
promptConfirm: jest.fn().mockResolvedValue(true),
|
|
69
|
+
interpolateMesh: jest.fn().mockResolvedValue({}),
|
|
70
|
+
importFiles: jest.fn().mockResolvedValue(),
|
|
71
|
+
}));
|
|
72
|
+
jest.mock('../../../lib/devConsole');
|
|
73
|
+
jest.mock('chalk', () => ({
|
|
74
|
+
red: jest.fn(text => text), // Return the input text without any color formatting
|
|
75
|
+
bold: jest.fn(text => text),
|
|
76
|
+
}));
|
|
77
|
+
|
|
53
78
|
let logSpy = null;
|
|
54
79
|
let errorLogSpy = null;
|
|
55
80
|
let parseSpy = null;
|
|
81
|
+
let platformSpy = null;
|
|
56
82
|
|
|
57
83
|
const mockIgnoreCacheFlag = Promise.resolve(true);
|
|
58
84
|
const mockAutoApproveAction = Promise.resolve(false);
|
|
@@ -69,6 +95,12 @@ describe('create command tests', () => {
|
|
|
69
95
|
projectName: selectedProject.title,
|
|
70
96
|
});
|
|
71
97
|
|
|
98
|
+
global.requestId = 'dummy_request_id';
|
|
99
|
+
|
|
100
|
+
logSpy = jest.spyOn(CreateCommand.prototype, 'log');
|
|
101
|
+
errorLogSpy = jest.spyOn(CreateCommand.prototype, 'error');
|
|
102
|
+
platformSpy = jest.spyOn(os, 'platform');
|
|
103
|
+
|
|
72
104
|
createMesh.mockResolvedValue({
|
|
73
105
|
mesh: {
|
|
74
106
|
meshId: 'dummy_mesh_id',
|
|
@@ -109,6 +141,10 @@ describe('create command tests', () => {
|
|
|
109
141
|
});
|
|
110
142
|
});
|
|
111
143
|
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
platformSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
|
|
112
148
|
test('must return proper object structure used by adobe/generator-app-api-mesh', async () => {
|
|
113
149
|
parseSpy.mockResolvedValueOnce({
|
|
114
150
|
args: { file: 'src/commands/__fixtures__/sample_mesh.json' },
|
|
@@ -171,6 +207,15 @@ describe('create command tests', () => {
|
|
|
171
207
|
"parse": [Function],
|
|
172
208
|
"type": "boolean",
|
|
173
209
|
},
|
|
210
|
+
"secrets": {
|
|
211
|
+
"char": "s",
|
|
212
|
+
"default": false,
|
|
213
|
+
"description": "Path to secrets file",
|
|
214
|
+
"input": [],
|
|
215
|
+
"multiple": false,
|
|
216
|
+
"parse": [Function],
|
|
217
|
+
"type": "option",
|
|
218
|
+
},
|
|
174
219
|
}
|
|
175
220
|
`);
|
|
176
221
|
expect(CreateCommand.aliases).toMatchInlineSnapshot(`[]`);
|
|
@@ -1831,4 +1876,205 @@ describe('create command tests', () => {
|
|
|
1831
1876
|
'https://graph.adobe.io/api/dummy_mesh_id/graphql?api_key=dummy_api_key',
|
|
1832
1877
|
);
|
|
1833
1878
|
});
|
|
1879
|
+
|
|
1880
|
+
test('should return error if mesh has placeholders and the provided secrets file is invalid', async () => {
|
|
1881
|
+
parseSpy.mockResolvedValueOnce({
|
|
1882
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
1883
|
+
flags: {
|
|
1884
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
1885
|
+
autoConfirmAction: Promise.resolve(true),
|
|
1886
|
+
secrets: 'src/commands/__fixtures__/secrets_invalid.yaml',
|
|
1887
|
+
},
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
const runResult = CreateCommand.run();
|
|
1891
|
+
|
|
1892
|
+
await expect(runResult).rejects.toEqual(
|
|
1893
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
1894
|
+
);
|
|
1895
|
+
|
|
1896
|
+
expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
|
|
1897
|
+
[
|
|
1898
|
+
[
|
|
1899
|
+
"Unable to import secrets. Please check the file and try again.",
|
|
1900
|
+
],
|
|
1901
|
+
]
|
|
1902
|
+
`);
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
test('should return error if mesh has placeholders and the provided secrets file is not yaml or yml', async () => {
|
|
1906
|
+
parseSpy.mockResolvedValueOnce({
|
|
1907
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
1908
|
+
flags: {
|
|
1909
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
1910
|
+
autoConfirmAction: Promise.resolve(true),
|
|
1911
|
+
secrets: 'src/commands/__fixtures__/.secrets_file.env',
|
|
1912
|
+
},
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
const runResult = CreateCommand.run();
|
|
1916
|
+
|
|
1917
|
+
await expect(runResult).rejects.toEqual(
|
|
1918
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
1919
|
+
);
|
|
1920
|
+
|
|
1921
|
+
expect(logSpy.mock.calls).toMatchInlineSnapshot(`
|
|
1922
|
+
[
|
|
1923
|
+
[
|
|
1924
|
+
"Invalid file format. Please provide a YAML file (.yaml or .yml).",
|
|
1925
|
+
],
|
|
1926
|
+
]
|
|
1927
|
+
`);
|
|
1928
|
+
|
|
1929
|
+
expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
|
|
1930
|
+
[
|
|
1931
|
+
[
|
|
1932
|
+
"Unable to import secrets. Please check the file and try again.",
|
|
1933
|
+
],
|
|
1934
|
+
]
|
|
1935
|
+
`);
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
test('should successfully create a mesh if provided secrets file is valid', async () => {
|
|
1939
|
+
parseSpy.mockResolvedValueOnce({
|
|
1940
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
1941
|
+
flags: {
|
|
1942
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
1943
|
+
autoConfirmAction: Promise.resolve(true),
|
|
1944
|
+
secrets: 'src/commands/__fixtures__/secrets_valid.yaml',
|
|
1945
|
+
},
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
const runResult = await CreateCommand.run();
|
|
1949
|
+
expect(runResult).toMatchInlineSnapshot(`
|
|
1950
|
+
{
|
|
1951
|
+
"apiKey": "dummy_api_key",
|
|
1952
|
+
"mesh": {
|
|
1953
|
+
"meshConfig": {
|
|
1954
|
+
"sources": [
|
|
1955
|
+
{
|
|
1956
|
+
"handler": {
|
|
1957
|
+
"graphql": {
|
|
1958
|
+
"endpoint": "<gql_endpoint>",
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
"name": "<api_name>",
|
|
1962
|
+
},
|
|
1963
|
+
],
|
|
1964
|
+
},
|
|
1965
|
+
"meshId": "dummy_mesh_id",
|
|
1966
|
+
},
|
|
1967
|
+
"sdkList": [
|
|
1968
|
+
"dummy_service",
|
|
1969
|
+
],
|
|
1970
|
+
}
|
|
1971
|
+
`);
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
test('should return error if ran against windows platform with batch variables', async () => {
|
|
1975
|
+
platformSpy.mockReturnValue('win32');
|
|
1976
|
+
parseSpy.mockResolvedValueOnce({
|
|
1977
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
1978
|
+
flags: {
|
|
1979
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
1980
|
+
autoConfirmAction: Promise.resolve(true),
|
|
1981
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
1982
|
+
},
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
const runResult = CreateCommand.run();
|
|
1986
|
+
|
|
1987
|
+
await expect(runResult).rejects.toEqual(
|
|
1988
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
1989
|
+
);
|
|
1990
|
+
|
|
1991
|
+
expect(logSpy.mock.calls).toMatchInlineSnapshot(`
|
|
1992
|
+
[
|
|
1993
|
+
[
|
|
1994
|
+
"Batch variables are not supported in YAML files on Windows.",
|
|
1995
|
+
],
|
|
1996
|
+
]
|
|
1997
|
+
`);
|
|
1998
|
+
expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
|
|
1999
|
+
[
|
|
2000
|
+
[
|
|
2001
|
+
"Unable to import secrets. Please check the file and try again.",
|
|
2002
|
+
],
|
|
2003
|
+
]
|
|
2004
|
+
`);
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
test('should pass if ran against linux platform with batch variables', async () => {
|
|
2008
|
+
platformSpy.mockReturnValue('linux');
|
|
2009
|
+
parseSpy.mockResolvedValueOnce({
|
|
2010
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
2011
|
+
flags: {
|
|
2012
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
2013
|
+
autoConfirmAction: Promise.resolve(true),
|
|
2014
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
2015
|
+
},
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
const runResult = await CreateCommand.run();
|
|
2019
|
+
expect(runResult).toMatchInlineSnapshot(`
|
|
2020
|
+
{
|
|
2021
|
+
"apiKey": "dummy_api_key",
|
|
2022
|
+
"mesh": {
|
|
2023
|
+
"meshConfig": {
|
|
2024
|
+
"sources": [
|
|
2025
|
+
{
|
|
2026
|
+
"handler": {
|
|
2027
|
+
"graphql": {
|
|
2028
|
+
"endpoint": "<gql_endpoint>",
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
"name": "<api_name>",
|
|
2032
|
+
},
|
|
2033
|
+
],
|
|
2034
|
+
},
|
|
2035
|
+
"meshId": "dummy_mesh_id",
|
|
2036
|
+
},
|
|
2037
|
+
"sdkList": [
|
|
2038
|
+
"dummy_service",
|
|
2039
|
+
],
|
|
2040
|
+
}
|
|
2041
|
+
`);
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
test('should pass if ran against darwin(macOS) platform with batch variables', async () => {
|
|
2045
|
+
platformSpy.mockReturnValue('darwin');
|
|
2046
|
+
parseSpy.mockResolvedValueOnce({
|
|
2047
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
2048
|
+
flags: {
|
|
2049
|
+
ignoreCache: mockIgnoreCacheFlag,
|
|
2050
|
+
autoConfirmAction: Promise.resolve(true),
|
|
2051
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
2052
|
+
},
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
const runResult = await CreateCommand.run();
|
|
2056
|
+
expect(runResult).toMatchInlineSnapshot(`
|
|
2057
|
+
{
|
|
2058
|
+
"apiKey": "dummy_api_key",
|
|
2059
|
+
"mesh": {
|
|
2060
|
+
"meshConfig": {
|
|
2061
|
+
"sources": [
|
|
2062
|
+
{
|
|
2063
|
+
"handler": {
|
|
2064
|
+
"graphql": {
|
|
2065
|
+
"endpoint": "<gql_endpoint>",
|
|
2066
|
+
},
|
|
2067
|
+
},
|
|
2068
|
+
"name": "<api_name>",
|
|
2069
|
+
},
|
|
2070
|
+
],
|
|
2071
|
+
},
|
|
2072
|
+
"meshId": "dummy_mesh_id",
|
|
2073
|
+
},
|
|
2074
|
+
"sdkList": [
|
|
2075
|
+
"dummy_service",
|
|
2076
|
+
],
|
|
2077
|
+
}
|
|
2078
|
+
`);
|
|
2079
|
+
});
|
|
1834
2080
|
});
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
promptConfirm,
|
|
19
19
|
setUpTenantFiles,
|
|
20
20
|
initSdk,
|
|
21
|
+
writeSecretsFile,
|
|
21
22
|
} = require('../../../helpers');
|
|
22
23
|
const { getMeshId, getMeshArtifact } = require('../../../lib/devConsole');
|
|
23
24
|
require('@adobe-apimesh/mesh-builder');
|
|
@@ -35,12 +36,16 @@ jest.mock('../../../helpers', () => ({
|
|
|
35
36
|
importFiles: jest.fn().mockResolvedValue(),
|
|
36
37
|
promptConfirm: jest.fn().mockResolvedValue(true),
|
|
37
38
|
setUpTenantFiles: jest.fn().mockResolvedValue(),
|
|
39
|
+
writeSecretsFile: jest.fn().mockResolvedValue(),
|
|
38
40
|
}));
|
|
39
41
|
|
|
40
42
|
jest.mock('../../../lib/devConsole', () => ({
|
|
41
43
|
getMeshId: jest.fn().mockResolvedValue('mockMeshId'),
|
|
42
44
|
getMeshArtifact: jest.fn().mockResolvedValue(),
|
|
43
45
|
}));
|
|
46
|
+
jest.mock('chalk', () => ({
|
|
47
|
+
red: jest.fn(text => text), // Return the input text without any color formatting
|
|
48
|
+
}));
|
|
44
49
|
|
|
45
50
|
jest.mock('@adobe-apimesh/mesh-builder', () => {
|
|
46
51
|
return {
|
|
@@ -55,12 +60,15 @@ jest.mock('@adobe-apimesh/mesh-builder', () => {
|
|
|
55
60
|
let logSpy = null;
|
|
56
61
|
let errorLogSpy = null;
|
|
57
62
|
let parseSpy = null;
|
|
63
|
+
let platformSpy = null;
|
|
58
64
|
|
|
59
65
|
const originalEnv = {
|
|
60
66
|
API_MESH_TIER: 'NON-TI',
|
|
61
67
|
};
|
|
62
68
|
|
|
63
69
|
const defaultPort = 5000;
|
|
70
|
+
const os = require('os');
|
|
71
|
+
|
|
64
72
|
describe('run command tests', () => {
|
|
65
73
|
beforeEach(() => {
|
|
66
74
|
global.requestId = 'dummy_request_id';
|
|
@@ -68,10 +76,14 @@ describe('run command tests', () => {
|
|
|
68
76
|
logSpy = jest.spyOn(RunCommand.prototype, 'log');
|
|
69
77
|
errorLogSpy = jest.spyOn(RunCommand.prototype, 'error');
|
|
70
78
|
parseSpy = jest.spyOn(RunCommand.prototype, 'parse');
|
|
79
|
+
platformSpy = jest.spyOn(os, 'platform');
|
|
71
80
|
process.env = {
|
|
72
81
|
...originalEnv,
|
|
73
82
|
};
|
|
74
83
|
});
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
platformSpy.mockRestore();
|
|
86
|
+
});
|
|
75
87
|
|
|
76
88
|
test('snapshot run command description', () => {
|
|
77
89
|
expect(RunCommand.description).toMatchInlineSnapshot(
|
|
@@ -121,6 +133,15 @@ describe('run command tests', () => {
|
|
|
121
133
|
"parse": [Function],
|
|
122
134
|
"type": "option",
|
|
123
135
|
},
|
|
136
|
+
"secrets": {
|
|
137
|
+
"char": "s",
|
|
138
|
+
"default": false,
|
|
139
|
+
"description": "Path to secrets file",
|
|
140
|
+
"input": [],
|
|
141
|
+
"multiple": false,
|
|
142
|
+
"parse": [Function],
|
|
143
|
+
"type": "option",
|
|
144
|
+
},
|
|
124
145
|
"select": {
|
|
125
146
|
"allowNo": false,
|
|
126
147
|
"default": false,
|
|
@@ -836,4 +857,109 @@ describe('run command tests', () => {
|
|
|
836
857
|
);
|
|
837
858
|
expect(setUpTenantFiles).toHaveBeenCalled();
|
|
838
859
|
});
|
|
860
|
+
|
|
861
|
+
test('should return error for run command if mesh has placeholders and the provided secrets file is invalid', async () => {
|
|
862
|
+
parseSpy.mockResolvedValueOnce({
|
|
863
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
864
|
+
flags: {
|
|
865
|
+
secrets: 'src/commands/__fixtures__/secrets_invalid.yaml',
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const runResult = RunCommand.run();
|
|
870
|
+
|
|
871
|
+
await expect(runResult).rejects.toEqual(
|
|
872
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
873
|
+
);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('should return error for run command if mesh has placeholders and the provided secrets file is not yaml or yml', async () => {
|
|
877
|
+
parseSpy.mockResolvedValueOnce({
|
|
878
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
879
|
+
flags: {
|
|
880
|
+
secrets: 'src/commands/__fixtures__/.secrets_file.env',
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const runResult = RunCommand.run();
|
|
885
|
+
|
|
886
|
+
await expect(runResult).rejects.toEqual(
|
|
887
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
expect(logSpy.mock.calls).toMatchInlineSnapshot(`
|
|
891
|
+
[
|
|
892
|
+
[
|
|
893
|
+
"Invalid file format. Please provide a YAML file (.yaml or .yml).",
|
|
894
|
+
],
|
|
895
|
+
]
|
|
896
|
+
`);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test('should successfully run the mesh if provided secrets file is valid', async () => {
|
|
900
|
+
parseSpy.mockResolvedValueOnce({
|
|
901
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
902
|
+
flags: {
|
|
903
|
+
secrets: 'src/commands/__fixtures__/secrets_valid.yaml',
|
|
904
|
+
debug: false,
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
await RunCommand.run();
|
|
909
|
+
expect(writeSecretsFile).toHaveBeenCalled();
|
|
910
|
+
expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test('should return error if ran with secrets against windows platform with batch variables', async () => {
|
|
914
|
+
platformSpy.mockReturnValue('win32');
|
|
915
|
+
parseSpy.mockResolvedValueOnce({
|
|
916
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
917
|
+
flags: {
|
|
918
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const runResult = RunCommand.run();
|
|
923
|
+
await expect(runResult).rejects.toEqual(
|
|
924
|
+
new Error('Unable to import secrets. Please check the file and try again.'),
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
expect(logSpy.mock.calls).toMatchInlineSnapshot(`
|
|
928
|
+
[
|
|
929
|
+
[
|
|
930
|
+
"Batch variables are not supported in YAML files on Windows.",
|
|
931
|
+
],
|
|
932
|
+
]
|
|
933
|
+
`);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test('should pass if ran with secrets against linux platform with batch variables', async () => {
|
|
937
|
+
platformSpy.mockReturnValue('linux');
|
|
938
|
+
parseSpy.mockResolvedValueOnce({
|
|
939
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
940
|
+
flags: {
|
|
941
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
942
|
+
debug: false,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
await RunCommand.run();
|
|
947
|
+
expect(writeSecretsFile).toHaveBeenCalled();
|
|
948
|
+
expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test('should pass if ran with secrets against darwin(macOS) platform with batch variables', async () => {
|
|
952
|
+
platformSpy.mockReturnValue('darwin');
|
|
953
|
+
parseSpy.mockResolvedValueOnce({
|
|
954
|
+
args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
|
|
955
|
+
flags: {
|
|
956
|
+
secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
|
|
957
|
+
debug: false,
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await RunCommand.run();
|
|
962
|
+
expect(writeSecretsFile).toHaveBeenCalled();
|
|
963
|
+
expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
|
|
964
|
+
});
|
|
839
965
|
});
|
|
@@ -19,9 +19,12 @@ const {
|
|
|
19
19
|
jsonFlag,
|
|
20
20
|
getFilesInMeshConfig,
|
|
21
21
|
envFileFlag,
|
|
22
|
+
secretsFlag,
|
|
22
23
|
checkPlaceholders,
|
|
23
24
|
readFileContents,
|
|
24
25
|
validateAndInterpolateMesh,
|
|
26
|
+
interpolateSecrets,
|
|
27
|
+
validateSecretsFile,
|
|
25
28
|
} = require('../../utils');
|
|
26
29
|
const { createMesh, getTenantFeatures } = require('../../lib/devConsole');
|
|
27
30
|
const { buildEdgeMeshUrl, buildMeshUrl } = require('../../urlBuilder');
|
|
@@ -33,6 +36,7 @@ class CreateCommand extends Command {
|
|
|
33
36
|
autoConfirmAction: autoConfirmActionFlag,
|
|
34
37
|
json: jsonFlag,
|
|
35
38
|
env: envFileFlag,
|
|
39
|
+
secrets: secretsFlag,
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
static enableJsonFlag = true;
|
|
@@ -53,6 +57,7 @@ class CreateCommand extends Command {
|
|
|
53
57
|
const ignoreCache = await flags.ignoreCache;
|
|
54
58
|
const autoConfirmAction = await flags.autoConfirmAction;
|
|
55
59
|
const envFilePath = await flags.env;
|
|
60
|
+
const secretsFilePath = await flags.secrets;
|
|
56
61
|
const {
|
|
57
62
|
imsOrgId,
|
|
58
63
|
imsOrgCode,
|
|
@@ -103,6 +108,17 @@ class CreateCommand extends Command {
|
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
// if secrets is present, include that in data.secrets
|
|
112
|
+
if (secretsFilePath) {
|
|
113
|
+
try {
|
|
114
|
+
await validateSecretsFile(secretsFilePath);
|
|
115
|
+
data.secrets = await interpolateSecrets(secretsFilePath, this);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
this.log(err.message);
|
|
118
|
+
this.error('Unable to import secrets. Please check the file and try again.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
let shouldContinue = true;
|
|
107
123
|
|
|
108
124
|
if (!autoConfirmAction) {
|
|
@@ -15,11 +15,14 @@ const {
|
|
|
15
15
|
debugFlag,
|
|
16
16
|
selectFlag,
|
|
17
17
|
envFileFlag,
|
|
18
|
+
secretsFlag,
|
|
18
19
|
autoConfirmActionFlag,
|
|
19
20
|
readFileContents,
|
|
20
21
|
validateAndInterpolateMesh,
|
|
21
22
|
checkPlaceholders,
|
|
22
23
|
getFilesInMeshConfig,
|
|
24
|
+
validateSecretsFile,
|
|
25
|
+
interpolateSecrets,
|
|
23
26
|
} = require('../../utils');
|
|
24
27
|
const meshBuilder = require('@adobe-apimesh/mesh-builder');
|
|
25
28
|
const fs = require('fs');
|
|
@@ -31,6 +34,7 @@ const {
|
|
|
31
34
|
startGraphqlServer,
|
|
32
35
|
importFiles,
|
|
33
36
|
setUpTenantFiles,
|
|
37
|
+
writeSecretsFile,
|
|
34
38
|
} = require('../../helpers');
|
|
35
39
|
const logger = require('../../classes/logger');
|
|
36
40
|
const { getMeshId, getMeshArtifact } = require('../../lib/devConsole');
|
|
@@ -55,6 +59,7 @@ class RunCommand extends Command {
|
|
|
55
59
|
env: envFileFlag,
|
|
56
60
|
autoConfirmAction: autoConfirmActionFlag,
|
|
57
61
|
select: selectFlag,
|
|
62
|
+
secrets: secretsFlag,
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
static enableJsonFlag = true;
|
|
@@ -67,6 +72,7 @@ class RunCommand extends Command {
|
|
|
67
72
|
logger.info(`RequestId: ${global.requestId}`);
|
|
68
73
|
|
|
69
74
|
const { args, flags } = await this.parse(RunCommand);
|
|
75
|
+
const secretsFilePath = await flags.secrets;
|
|
70
76
|
|
|
71
77
|
//Initialize the meshId based on
|
|
72
78
|
let meshId = null;
|
|
@@ -165,6 +171,17 @@ class RunCommand extends Command {
|
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
let portNo;
|
|
174
|
+
//secrets management
|
|
175
|
+
if (secretsFilePath) {
|
|
176
|
+
try {
|
|
177
|
+
await validateSecretsFile(secretsFilePath);
|
|
178
|
+
const stringifiedSecrets = await interpolateSecrets(secretsFilePath, this);
|
|
179
|
+
await writeSecretsFile(stringifiedSecrets, meshId);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.log(error.message);
|
|
182
|
+
this.error('Unable to import secrets. Please check the file and try again.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
168
185
|
|
|
169
186
|
//To set the port number using the environment file
|
|
170
187
|
if (process.env.PORT !== undefined) {
|
|
@@ -17,10 +17,13 @@ const {
|
|
|
17
17
|
ignoreCacheFlag,
|
|
18
18
|
autoConfirmActionFlag,
|
|
19
19
|
envFileFlag,
|
|
20
|
+
secretsFlag,
|
|
20
21
|
checkPlaceholders,
|
|
21
22
|
readFileContents,
|
|
22
23
|
validateAndInterpolateMesh,
|
|
23
24
|
getFilesInMeshConfig,
|
|
25
|
+
interpolateSecrets,
|
|
26
|
+
validateSecretsFile,
|
|
24
27
|
} = require('../../utils');
|
|
25
28
|
const { getMeshId, updateMesh } = require('../../lib/devConsole');
|
|
26
29
|
|
|
@@ -30,6 +33,7 @@ class UpdateCommand extends Command {
|
|
|
30
33
|
ignoreCache: ignoreCacheFlag,
|
|
31
34
|
autoConfirmAction: autoConfirmActionFlag,
|
|
32
35
|
env: envFileFlag,
|
|
36
|
+
secrets: secretsFlag,
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
async run() {
|
|
@@ -48,6 +52,7 @@ class UpdateCommand extends Command {
|
|
|
48
52
|
const ignoreCache = await flags.ignoreCache;
|
|
49
53
|
const autoConfirmAction = await flags.autoConfirmAction;
|
|
50
54
|
const envFilePath = await flags.env;
|
|
55
|
+
const secretsFilePath = await flags.secrets;
|
|
51
56
|
|
|
52
57
|
const { imsOrgId, projectId, workspaceId, orgName, projectName, workspaceName } = await initSdk(
|
|
53
58
|
{
|
|
@@ -103,6 +108,17 @@ class UpdateCommand extends Command {
|
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
// if secrets is present, include that in data.secrets
|
|
112
|
+
if (secretsFilePath) {
|
|
113
|
+
try {
|
|
114
|
+
await validateSecretsFile(secretsFilePath);
|
|
115
|
+
data.secrets = await interpolateSecrets(secretsFilePath, this);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
this.log(err.message);
|
|
118
|
+
this.error('Unable to import secrets. Please check the file and try again.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
if (meshId) {
|
|
107
123
|
let shouldContinue = true;
|
|
108
124
|
|
package/src/helpers.js
CHANGED
|
@@ -860,6 +860,26 @@ async function setUpTenantFiles(meshId) {
|
|
|
860
860
|
}
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
+
/**
|
|
864
|
+
* This function is to create secrets.yaml in mesh-artifacts for respective meshId. Used for local development run command
|
|
865
|
+
*
|
|
866
|
+
* @secretsData secretsData
|
|
867
|
+
* @meshId meshId
|
|
868
|
+
*/
|
|
869
|
+
async function writeSecretsFile(secretsData, meshId) {
|
|
870
|
+
if (!fs.existsSync(path.resolve(process.cwd(), 'mesh-artifact', meshId))) {
|
|
871
|
+
throw new Error(`Unexpected Error: issue creating secrets file.`);
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
const secretsFileName = 'secrets.yaml';
|
|
875
|
+
const folderPath = path.join(process.cwd(), 'mesh-artifact', meshId);
|
|
876
|
+
const filePath = path.join(folderPath, secretsFileName);
|
|
877
|
+
fs.writeFileSync(filePath, secretsData);
|
|
878
|
+
} catch (error) {
|
|
879
|
+
throw new Error(error.message);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
863
883
|
module.exports = {
|
|
864
884
|
objToString,
|
|
865
885
|
promptInput,
|
|
@@ -876,4 +896,5 @@ module.exports = {
|
|
|
876
896
|
updateFilesArray,
|
|
877
897
|
startGraphqlServer,
|
|
878
898
|
setUpTenantFiles,
|
|
899
|
+
writeSecretsFile,
|
|
879
900
|
};
|
package/src/server.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
removeRequestHeaders,
|
|
14
14
|
prepSourceResponseHeaders,
|
|
15
15
|
processResponseHeaders,
|
|
16
|
+
readSecretsFile,
|
|
16
17
|
} = require('./serverUtils');
|
|
17
18
|
|
|
18
19
|
let yogaServer = null;
|
|
@@ -64,6 +65,8 @@ const getYogaServer = async () => {
|
|
|
64
65
|
const tenantMesh = await getBuiltMesh();
|
|
65
66
|
const corsOptions = getCORSOptions();
|
|
66
67
|
|
|
68
|
+
const secrets = readSecretsFile(meshId);
|
|
69
|
+
|
|
67
70
|
logger.info('Creating graphQL server');
|
|
68
71
|
|
|
69
72
|
meshConfig = readMeshConfig(meshId);
|
|
@@ -73,6 +76,10 @@ const getYogaServer = async () => {
|
|
|
73
76
|
graphqlEndpoint: `/graphql`,
|
|
74
77
|
graphiql: true,
|
|
75
78
|
cors: corsOptions,
|
|
79
|
+
context: initialContext => ({
|
|
80
|
+
...initialContext,
|
|
81
|
+
secrets,
|
|
82
|
+
}),
|
|
76
83
|
});
|
|
77
84
|
|
|
78
85
|
return yogaServer;
|
package/src/serverUtils.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const LRUCache = require('lru-cache');
|
|
4
4
|
const logger = require('./classes/logger');
|
|
5
|
+
const YAML = require('yaml');
|
|
5
6
|
|
|
6
7
|
const headersCache = new LRUCache({
|
|
7
8
|
max: parseInt(process.env.CACHE_OPT_MAX || '500', 10),
|
|
@@ -315,9 +316,29 @@ function ccDirectivesToString(directives) {
|
|
|
315
316
|
return chStr.toString();
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Returns secrets content from artifacts
|
|
321
|
+
* @param meshId
|
|
322
|
+
* @returns
|
|
323
|
+
*/
|
|
324
|
+
function readSecretsFile(meshId) {
|
|
325
|
+
let secrets = {};
|
|
326
|
+
try {
|
|
327
|
+
const filePath = path.resolve(process.cwd(), 'mesh-artifact', `${meshId}`, 'secrets.yaml');
|
|
328
|
+
if (fs.existsSync(filePath)) {
|
|
329
|
+
secrets = YAML.parse(fs.readFileSync(filePath, 'utf8'));
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger.error('Unexpected error: unable to locate secrets file in mesh artifacts.');
|
|
333
|
+
throw new Error(error.message);
|
|
334
|
+
}
|
|
335
|
+
return secrets;
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
module.exports = {
|
|
319
339
|
readMeshConfig,
|
|
320
340
|
removeRequestHeaders,
|
|
321
341
|
prepSourceResponseHeaders,
|
|
322
342
|
processResponseHeaders,
|
|
343
|
+
readSecretsFile,
|
|
323
344
|
};
|
package/src/utils.js
CHANGED
|
@@ -5,6 +5,10 @@ const { Flags } = require('@oclif/core');
|
|
|
5
5
|
const { readFile } = require('fs/promises');
|
|
6
6
|
const { interpolateMesh } = require('./helpers');
|
|
7
7
|
const dotenv = require('dotenv');
|
|
8
|
+
const YAML = require('yaml');
|
|
9
|
+
const parseEnv = require('envsub/js/envsub-parser');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const chalk = require('chalk');
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* @returns returns the root directory of the project
|
|
@@ -41,6 +45,12 @@ const envFileFlag = Flags.string({
|
|
|
41
45
|
default: '.env',
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
const secretsFlag = Flags.string({
|
|
49
|
+
char: 's',
|
|
50
|
+
description: 'Path to secrets file',
|
|
51
|
+
default: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
44
54
|
const portNoFlag = Flags.integer({
|
|
45
55
|
char: 'p',
|
|
46
56
|
description: 'Port number for the local dev server',
|
|
@@ -391,6 +401,97 @@ async function validateAndInterpolateMesh(inputMeshData, envFilePath, command) {
|
|
|
391
401
|
}
|
|
392
402
|
}
|
|
393
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Validate secrets file
|
|
406
|
+
*
|
|
407
|
+
* @param secretsFile Validates that secrets file extension is in yaml
|
|
408
|
+
*/
|
|
409
|
+
async function validateSecretsFile(secretsFile) {
|
|
410
|
+
try {
|
|
411
|
+
const validExtensions = ['.yaml', '.yml'];
|
|
412
|
+
const fileExtension = secretsFile.split('.').pop().toLowerCase();
|
|
413
|
+
if (!validExtensions.includes('.' + fileExtension)) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
chalk.red('Invalid file format. Please provide a YAML file (.yaml or .yml).'),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
logger.error(error.message);
|
|
420
|
+
throw new Error(error.message);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Read the secrets file, checks validation and interpolate mesh
|
|
426
|
+
*
|
|
427
|
+
* @param secretsFilePath Secrets file path
|
|
428
|
+
* @param command
|
|
429
|
+
*/
|
|
430
|
+
async function interpolateSecrets(secretsFilePath, command) {
|
|
431
|
+
try {
|
|
432
|
+
const secretsContent = await readFileContents(secretsFilePath, command, 'secrets');
|
|
433
|
+
|
|
434
|
+
// Check if environment variables are used in the file content
|
|
435
|
+
if (os.platform() === 'win32' && /\$({)?[a-zA-Z_][a-zA-Z0-9_]*}?/.test(secretsContent)) {
|
|
436
|
+
throw new Error(chalk.red('Batch variables are not supported in YAML files on Windows.'));
|
|
437
|
+
}
|
|
438
|
+
const secrets = await parseSecrets(secretsContent);
|
|
439
|
+
return secrets;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
logger.error(err.message);
|
|
442
|
+
throw new Error(err.message);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Parse secrets YAML content.
|
|
448
|
+
*
|
|
449
|
+
* @param secretsFilePath Secrets file path
|
|
450
|
+
*/
|
|
451
|
+
async function parseSecrets(secretsContent) {
|
|
452
|
+
try {
|
|
453
|
+
const envParserConfig = {
|
|
454
|
+
outputFile: null,
|
|
455
|
+
options: {
|
|
456
|
+
all: false,
|
|
457
|
+
diff: false,
|
|
458
|
+
protect: false,
|
|
459
|
+
syntax: 'dollar-both',
|
|
460
|
+
},
|
|
461
|
+
cli: false,
|
|
462
|
+
};
|
|
463
|
+
const compiledSecretsFileContent = parseEnv(secretsContent, envParserConfig);
|
|
464
|
+
const parsedSecrets = YAML.parse(compiledSecretsFileContent);
|
|
465
|
+
//check if secrets file is empty
|
|
466
|
+
if (!parsedSecrets) {
|
|
467
|
+
throw new Error(chalk.red('Invalid YAML file contents. Please verify and try again.'));
|
|
468
|
+
}
|
|
469
|
+
//check if parsedSecrets is string and not in k:v pair
|
|
470
|
+
if (typeof parsedSecrets === 'string') {
|
|
471
|
+
throw new Error(chalk.red('Please provide a valid YAML in key:value format.'));
|
|
472
|
+
}
|
|
473
|
+
const secretsYamlString = YAML.stringify(parsedSecrets);
|
|
474
|
+
return secretsYamlString; //TODO: here we will encrypt secrets and return.
|
|
475
|
+
} catch (err) {
|
|
476
|
+
throw new Error(chalk.red(getSecretsYamlParseError(err)));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* This function returns user friendly errors that occurs while YAML.parse
|
|
482
|
+
*
|
|
483
|
+
* @param error errors from YAML.parse
|
|
484
|
+
*/
|
|
485
|
+
function getSecretsYamlParseError(error) {
|
|
486
|
+
if (error.code === 'BAD_INDENT') {
|
|
487
|
+
return 'Invalid YAML - Bad Indentation: ' + error.message;
|
|
488
|
+
} else if (error.code === 'DUPLICATE_KEY') {
|
|
489
|
+
return 'Invalid YAML - Found Duplicate Keys: ' + error.message;
|
|
490
|
+
} else {
|
|
491
|
+
return 'Unexpected Error: ' + error.message;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
394
495
|
module.exports = {
|
|
395
496
|
ignoreCacheFlag,
|
|
396
497
|
autoConfirmActionFlag,
|
|
@@ -405,4 +506,7 @@ module.exports = {
|
|
|
405
506
|
portNoFlag,
|
|
406
507
|
debugFlag,
|
|
407
508
|
selectFlag,
|
|
509
|
+
secretsFlag,
|
|
510
|
+
interpolateSecrets,
|
|
511
|
+
validateSecretsFile,
|
|
408
512
|
};
|