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

Added feature to pick from multiple sample backends. #2853

Merged
merged 45 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
36eb075
Initial work
revanth0212 Nov 5, 2020
3e3e1c6
Added magento backend validation.
revanth0212 Nov 6, 2020
05a9edf
Updated intercept to fetch backends.
revanth0212 Nov 9, 2020
f2b1074
Fetching sample backends while creating a pwa app.
revanth0212 Nov 9, 2020
5e21ed0
Minor.
revanth0212 Nov 9, 2020
237dd12
Added try catches.
revanth0212 Nov 9, 2020
487bc7b
Updated docs.
revanth0212 Nov 9, 2020
6c6786a
Minor.
revanth0212 Nov 9, 2020
6dfc72b
Added node-fetch peer dep.
revanth0212 Nov 9, 2020
1ab9cce
Minor pretty print stuff.
revanth0212 Nov 9, 2020
d8e25ef
Added lodash and node-fetch deps.
revanth0212 Nov 10, 2020
0a80953
Updated extension desc.
revanth0212 Nov 10, 2020
1ed5283
Updated tests.
revanth0212 Nov 11, 2020
240c0a2
Minor.
revanth0212 Nov 11, 2020
fac9c0b
Updated remaining tests.
revanth0212 Nov 11, 2020
c57b13d
Added runEnvValidators.js tests.
revanth0212 Nov 11, 2020
df3dc18
Fixed linter issues.
revanth0212 Nov 11, 2020
d0516fa
Minor.
revanth0212 Nov 12, 2020
8b5cf97
Added intercept tests.
revanth0212 Nov 12, 2020
423f002
Using debug instead of console.error.
revanth0212 Nov 12, 2020
dc3da4d
Moving backend related code to run after configureWebpack.
revanth0212 Nov 12, 2020
b3110ef
Updated the skipped test.
revanth0212 Nov 16, 2020
71e35eb
Update ENV error reporting message.
revanth0212 Nov 16, 2020
7323d16
Merge remote-tracking branch 'origin/develop' into revanth/venia-samp…
revanth0212 Nov 16, 2020
bd02431
Minor.
revanth0212 Nov 16, 2020
315b8cd
Minor.
revanth0212 Nov 17, 2020
e56371e
Updated error snapshot test.
revanth0212 Nov 17, 2020
dd19131
Minor.
revanth0212 Nov 17, 2020
4856d57
Merge branch 'develop' into revanth/venia-sample-backends
dpatil-magento Nov 18, 2020
ee19c77
Minor.
revanth0212 Nov 18, 2020
acebbce
Added or condition for path variable in tests.
revanth0212 Nov 19, 2020
774fa80
Minor.
revanth0212 Nov 19, 2020
f04d371
Updated tests to use snapshots.
revanth0212 Nov 19, 2020
0445d2e
Mock everything, lets get this working.
revanth0212 Nov 19, 2020
1533416
Removed unecessary mocks.
revanth0212 Nov 19, 2020
fed64cc
Updated tests.
revanth0212 Nov 19, 2020
deccd28
Using try catch to avoid prj creations.
revanth0212 Nov 24, 2020
8aaf222
Reporting different message if otherBackends is empty.
revanth0212 Nov 30, 2020
ebb42e0
Updated tests.
revanth0212 Nov 30, 2020
dbb8cf9
Merge branch 'develop' into revanth/venia-sample-backends
dpatil-magento Nov 30, 2020
34b1a30
Prettier fix.
revanth0212 Nov 30, 2020
abd0613
Merge branch 'revanth/venia-sample-backends' of https://github.com/ma…
revanth0212 Nov 30, 2020
b55b6df
Added console warning for production deployment.
revanth0212 Dec 1, 2020
a4d2154
Updated production launch checklist docs.
revanth0212 Dec 1, 2020
98abb1b
Merge branch 'develop' into revanth/venia-sample-backends
dpatil-magento Dec 2, 2020
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
31 changes: 30 additions & 1 deletion packages/create-pwa/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
const { basename, resolve } = require('path');
const os = require('os');
const fetch = require('node-fetch');
const changeCase = require('change-case');
const inquirer = require('inquirer');
const execa = require('execa');
const chalk = require('chalk');
const gitUserInfo = require('git-user-info');
const isInvalidPath = require('is-invalid-path');
const isValidNpmName = require('is-valid-npm-name');
const { uniqBy } = require('lodash');
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to bring up per method imports, but it looks like lodash is getting rid of them in v5. My instinct here is that this is a heavy dependency for the utility, but I'm not sure if this same rule applies to creators. I'm not even sure a direct import would help. I'm leaning towards per method for now, but not required if you think this is reasonable as-is.

Suggested change
const { uniqBy } = require('lodash');
const uniqBy = require('lodash/uniqBy');

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am a fan of the method imports myself. But I realized we import the whole lodash library as part of buildpack anyway. Hence I tried to add it as a peer dependency. Also, Lodash one of the libraries which are really good for tree shaking. Webpack should be able to remove all unnecessary code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can stay. It's not an ES6 import, it's a destructure of a full import of Lodash. But that's okay!

Code in this package won't be built by buildpack; it'll run in Node. Many of the dependent packages are already importing all of lodash (yarn why lodash sums it up), so this import doesn't make a difference.

Copy link
Contributor

Choose a reason for hiding this comment

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

Works for me 👍 @revanth0212 lodash is currently in the dependencies list, did you intend to put it under peerDependencies? As long as it's not grabbing two different versions it shouldn't matter, just wanted to point it out.


const pkg = require('../package.json');
const {
sampleBackends
sampleBackends: defaultSampleBackends
} = require('@magento/pwa-buildpack/lib/cli/create-project');

const removeDuplicateBackends = backendEnvironments =>
uniqBy(backendEnvironments, 'url');

const fetchSampleBackends = async () => {
try {
const res = await fetch(
'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am looking at AWS docs to use a DNS name instead of this raw path. It will be changed in the future (before this PR is merged).

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a lambda? Does it read configuration somewhere? I'm a bit surprised we don't just maintain a list in the extension.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The plan is to pull these backend URLs dynamically so we don't need to publish every single time a backend changes, which we will have to if these are part of the package. The URL above is an AWS Gateway URL. The plan is to use a domain-specific URL so in the future if we move between cloud providers (Azure, Ethos, etc), we can keep using the same URL but the implementation in the back can change.

Copy link
Contributor

@sirugh sirugh Nov 12, 2020

Choose a reason for hiding this comment

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

so we don't need to publish every single time a backend changes

Something will have to change somewhere whether that's a file in s3, a lambda script, or a local file in a pwa studio extension/package (like create-pwa). And as we saw in the demo, there is already a file for hosting these, but within buildpack.

I personally prefer keeping this file locally within create-pwa or buildpack and just publishing a new version of those packages if the backend changes. It's straightforward and requires no knowledge of lambdas, aws routing, etc. There is also no concern with api downtime or worry about attack vectors with a public route.

If the team want to keep this solution as is, I'd really like to see some documentation with steps to update this file. I also think we should understand the costs of this approach. How much per request does this lambda/endpoint cost us?

Edit: To be clear, I appreciate the approach as an example for how we could interact with a micro-service. It's a neat example for sure. I just think it's overkill for us.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The points you have mentioned are perfectly reasonable. I did have a thought of making it a package to avoid the cost of hosting and also avoid potential DoS security issues. But the biggest problem with publishing on NPM is that developers don't generally run yarn install every time they build or might be using an older version of the package causing older environments to be used instead. The cost of using AWS Lambda and AWS S3 is minimal and we have enabled throttling as an initial security check.

Copy link
Contributor

@sirugh sirugh Nov 13, 2020

Choose a reason for hiding this comment

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

But the biggest problem with publishing on NPM is that developers don't generally run yarn install every time they build or might be using an older version of the package causing older environments to be used instead.

Two questions:

  1. How often does the getSampleBackends service actually need to be used? It should just be used when someone spins up their project for the first time, right?

  2. Is the getSampleBackends API used during the build or do an up-check to see if the backend is accessible during build?

I think I see that it uses it on initial scaffold but then only later if the up-check fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. How often does the getSampleBackends service actually need to be used? It should just be used when someone spins up their project for the first time, right?

Along with bootstrapping, It will be used every time someone adds the venia-sample-backends extension and builds their project.

  1. Is the getSampleBackends API used during the build or do an up-check to see if the backend is accessible during build?

It is used during the build stage if the extension is installed. It will be used to fetch the sample backends, if the current backend is inactive. We won't be calling the service if the backend mentioned in the ENV file is active.

Copy link
Contributor

@davemacaulay davemacaulay Nov 19, 2020

Choose a reason for hiding this comment

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

What happens if this URL goes down? Will the yarn install / build hang for X seconds?

Also if we're maintaining this micro server we need to setup monitoring to ensure we can be aware of uptime issues. We should also consider any security implications of having this service running.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice question @davemacaulay. I have used a wrong URL to simulate the service failure and you can see here, if the service is down, the script will use the internal URLs shipped as part of the sampleBackends.json file.

image

You are right about monitoring. I'll create a ticket to investigate the monitoring capabilities of our team/AWS.

Coming to security, we are not giving the public access to the Lamda function or the S3 object. It is an AWS Gateway Service that we are using. Also, the Lamda function that we are using only has read access to that particular object.

Copy link
Contributor

@dpatil-magento dpatil-magento Nov 24, 2020

Choose a reason for hiding this comment

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

@revanth0212 It would nice if we can move the lambda setup to aws prod account and check if any Audits fail. Rest all looks good.

);
const { sampleBackends } = await res.json();

return sampleBackends.environments;
} catch {
return [];
}
};

module.exports = async () => {
console.log(chalk.greenBright(`${pkg.name} v${pkg.version}`));
console.log(
Expand All @@ -20,6 +39,16 @@ module.exports = async () => {
const userAgent = process.env.npm_config_user_agent || '';
const isYarn = userAgent.includes('yarn');

const sampleBackendEnvironments = await fetchSampleBackends();
const filteredBackendEnvironments = removeDuplicateBackends([
...sampleBackendEnvironments,
...defaultSampleBackends.environments
]);
const sampleBackends = {
...defaultSampleBackends,
environments: filteredBackendEnvironments
};

const questions = [
{
name: 'directory',
Expand Down
2 changes: 2 additions & 0 deletions packages/create-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"inquirer": "^6.3.1",
"is-invalid-path": "^1.0.2",
"is-valid-npm-name": "^0.0.4",
"lodash": "~4.17.11",
"node-fetch": "~2.3.0",
"webpack": "^4.29.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should call onFail if backend is inactive 1`] = `
"https://www.magento-backend-2.3.4.com/ is inactive. Please consider using one of these other backends:

[{\\"name\\":\\"2.3.3-venia-cloud\\",\\"description\\":\\"Magento 2.3.3 with Venia sample data installed\\",\\"url\\":\\"https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/\\"}]
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
jest.mock('node-fetch');
const fetch = require('node-fetch');

const { validateSampleBackend } = require('../intercept');

const env = {
MAGENTO_BACKEND_URL: 'https://www.magento-backend-2.3.4.com/'
};
const onFail = jest.fn().mockName('onFail');
const debug = jest.fn().mockName('debug');

const args = { env, onFail, debug };

test('should not call onFail if backend is active', async () => {
fetch.mockResolvedValueOnce({ ok: true });

await validateSampleBackend(args);

expect(onFail).not.toHaveBeenCalled();
});

test('should call onFail if backend is inactive', async () => {
fetch.mockResolvedValueOnce({ ok: false }).mockResolvedValueOnce({
json: jest.fn().mockResolvedValue({
sampleBackends: {
environments: [
{
name: '2.3.3-venia-cloud',
description:
'Magento 2.3.3 with Venia sample data installed',
url:
'https://master-7rqtwti-mfwmkrjfqvbjk.us-4.magentosite.cloud/'
},
{
name: '2.3.4-venia-cloud',
description:
'Magento 2.3.4 with Venia sample data installed',
url: 'https://www.magento-backend-2.3.4.com/'
}
]
}
})
});

await validateSampleBackend(args);

expect(onFail).toHaveBeenCalled();
expect(onFail.mock.calls[0][0]).toMatchSnapshot();
});
82 changes: 82 additions & 0 deletions packages/extensions/venia-sample-backends/intercept.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const fetch = require('node-fetch');

const isBackendActive = async (env, debug) => {
try {
const magentoBackend = env.MAGENTO_BACKEND_URL;
const res = await fetch(magentoBackend);

return res.ok;
} catch (err) {
debug(err);

return false;
}
};

const fetchBackends = async debug => {
try {
const res = await fetch(
'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
);
const { sampleBackends } = await res.json();

return sampleBackends.environments;
} catch (err) {
debug(err);

return [];
}
};

/**
* Validation function to check if the backend being used is one of the sample backends provided
* by PWA Studio. If yes, the function validates if the backend is active. If not, it reports an
* error by calling the onFail function. In the error being reported, it sends the other sample
* backends that the developers can use.
*
* @param {Object} config.env - The ENV provided to the app, usually avaialable through process.ENV
* @param {Function} config.onFail - callback function to call on validation fail
* @param {Function} config.debug - function to log debug messages in console in debug mode
*
* To watch the debug messages, run the command with DEBUG=*runEnvValidators*
*/
const validateSampleBackend = async config => {
const { env, onFail, debug } = config;

const backendIsActive = await isBackendActive(env, debug);

if (!backendIsActive) {
debug(`${env.MAGENTO_BACKEND_URL} is inactive`);

debug('Fetching other backends');

const sampleBackends = await fetchBackends(debug);
const otherBackends = sampleBackends.filter(
({ url }) => url !== env.MAGENTO_BACKEND_URL
);

debug(
'PWA Studio supports the following backends',
sampleBackends.join('\n')
);

debug('Reporting backend URL validation failure');
onFail(
`${
env.MAGENTO_BACKEND_URL
} is inactive. Please consider using one of these other backends: \n\n ${JSON.stringify(
otherBackends
)} \n`
);
} else {
debug(`${env.MAGENTO_BACKEND_URL} is active`);
}
};

module.exports = targets => {
targets
.of('@magento/pwa-buildpack')
.validateEnv.tapPromise(validateSampleBackend);
};

module.exports.validateSampleBackend = validateSampleBackend;
24 changes: 24 additions & 0 deletions packages/extensions/venia-sample-backends/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@magento/venia-sample-backends",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"description": "Provides demo backends and backend validation utils for PWA Studio.",
"main": "./intercept.js",
"scripts": {
"clean": " ",
"test": "jest"
},
"repository": "github:magento/pwa-studio",
"license": "(OSL-3.0 OR AFL-3.0)",
"peerDependencies": {
"@magento/pwa-buildpack": "~7.0.0",
"node-fetch": "~2.3.0"
},
"pwa-studio": {
"targets": {
"intercept": "./intercept"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
"intercept": "./i18n-intercept"
}
}
}
}
47 changes: 46 additions & 1 deletion packages/pwa-buildpack/lib/BuildBus/declare-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,26 @@ module.exports = targets => {
* @member {tapable.AsyncSeriesHook}
* @param {transformUpwardIntercept} interceptor
*/
transformUpward: new targets.types.AsyncSeries(['definitions'])
transformUpward: new targets.types.AsyncSeries(['definitions']),

/**
* Collect all ENV validation functions that will run against the
* project's ENV. The functions can be async and they will run in
* parallel. If a validation function wants to stop the whole process
* for instance in case of a serious security issue, it can do so
* by throwing an error. If it wants to report an error, it can do so
* by using the onFail callback provided as an argument. A validation
* function can submit multiple errors by calling the onFail function
* multiple times. All the errors will be queued into an array and
* displayed on the console at the end of the process.
*
* @example
* targets.of('@magento/pwa-buildpack').validateEnv.tapPromise(validateBackendUrl);
*
* @member {tapable.AsyncParallelHook}
* @param {envValidationInterceptor} validator
*/
validateEnv: new targets.types.AsyncParallel(['validator'])
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellently documented! Thank you very very much.

};

/**
Expand Down Expand Up @@ -282,3 +301,29 @@ module.exports = targets => {
* @param {object} definition - Parsed UPWARD definition object.
* @returns {Promise}
*/

/** Type definitions related to: validateEnv */

/**
* Intercept function signature for the validateEnv target.
*
* Interceptors of the `validateEnv` target receive a config object.
* The config object contains the project env, an onFail callback and
* the debug function to be used in case of the debug mode to log more
* inforamtion to the console.
*
* This Target can be used asynchronously in the parallel mode. If a
* validator needs to stop the process immediately, it can throw an error.
* If it needs to report an error but not stop the whole process, it can do
* so by calling the onFail function with the error message it wants to report.
* It can call the onFail multiple times if it wants to report multiple errors.
*
* All the errors will be queued and printed into the console at the end of the
* validation process and the build process will be stopeed.
*
* @callback envValidationInterceptor
* @param {Object} config.env - Project ENV
* @param {Function} config.onFail - On fail callback
* @param {Function} config.debug - Debug function to be used for additional reporting in debug mode
* @returns {Boolean}
*/
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ Array [
],
]
`;

exports[`throws on load if variable defs are invalid 1`] = `
"Bad environment variable definition. Section inscrutable variable {
\\"type\\": \\"ineffable\\"
} declares an unknown type ineffable"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should throw error if there are validation errors reported by interceptors 1`] = `
"Environment has 2 validation errors:
(1) Danger,
(2) Another error"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ jest.mock('../../util/pretty-logger', () => ({
error: jest.fn()
}));
jest.mock('../getEnvVarDefinitions');
jest.mock('../runEnvValidators', () => jest.fn().mockResolvedValue(true));
const dotenv = require('dotenv');
const getEnvVarDefinitions = require('../getEnvVarDefinitions');
const createDotEnvFile = require('../createDotEnvFile');
Expand Down Expand Up @@ -45,55 +46,55 @@ beforeEach(() => {
mockLog.error.mockClear();
});

test('logs errors to default logger if env is not valid', () => {
test('logs errors to default logger if env is not valid', async () => {
mockEnvVars.set({
MAGENTO_BACKEND_URL: mockEnvVars.UNSET
});
createDotEnvFile('./');
await createDotEnvFile('./');
expect(prettyLogger.warn).toHaveBeenCalled();
});

test('uses alternate logger', () => {
test('uses alternate logger', async () => {
mockEnvVars.set({
MAGENTO_BACKEND_URL: mockEnvVars.UNSET
});
createDotEnvFile('./', { logger: mockLog });
await createDotEnvFile('./', { logger: mockLog });
expect(mockLog.warn).toHaveBeenCalled();
});

test('returns valid dotenv file if env is valid', () => {
test('returns valid dotenv file if env is valid', async () => {
mockEnvVars.set(examples);
const fileText = createDotEnvFile('./', { logger: mockLog });
const fileText = await createDotEnvFile('./', { logger: mockLog });
expect(snapshotEnvFile(fileText)).toMatchSnapshot();
expect(dotenv.parse(fileText)).toMatchObject(examples);
});

test('populates with examples where available', () => {
test('populates with examples where available', async () => {
const unsetExamples = {};
for (const key of Object.keys(examples)) {
unsetExamples[key] = mockEnvVars.UNSET;
}
mockEnvVars.set(unsetExamples);
const fileText = createDotEnvFile('./', { useExamples: true });
const fileText = await createDotEnvFile('./', { useExamples: true });
expect(dotenv.parse(fileText)).toMatchObject(examples);
});

test('does not print example comment if value is set custom', () => {
test('does not print example comment if value is set custom', async () => {
const fakeEnv = {
...examples,
MAGENTO_BACKEND_URL: 'https://custom.url',
IMAGE_SERVICE_CACHE_EXPIRES: 'a million years'
};
mockEnvVars.set(fakeEnv);
const fileText = createDotEnvFile(fakeEnv);
const fileText = await createDotEnvFile(fakeEnv);
expect(fileText).not.toMatch(MAGENTO_BACKEND_URL_EXAMPLE);
expect(fileText).not.toMatch(
`Example: ${examples.IMAGE_SERVICE_CACHE_EXPIRES}`
);
expect(dotenv.parse(fileText)).not.toMatchObject(examples);
});

test('passing an env object works, but warns deprecation and assumes cwd is context', () => {
test('passing an env object works, but warns deprecation and assumes cwd is context', async () => {
getEnvVarDefinitions.mockReturnValue({
sections: [
{
Expand All @@ -117,7 +118,7 @@ test('passing an env object works, but warns deprecation and assumes cwd is cont
});
expect(
snapshotEnvFile(
createDotEnvFile({
await createDotEnvFile({
TEST_ENV_VAR_NOTHING: 'foo'
})
)
Expand Down
Loading