Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

fix(holocron): bad modules could cause crashes and prevent restart #631

Merged
merged 11 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions __tests__/integration/helpers/moduleMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ module.exports = {
retrieveModuleIntegrityDigests,
writeModuleMap,
readModuleMap,
testCdnUrl,
};
111 changes: 100 additions & 11 deletions __tests__/integration/one-app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
writeModuleMap,
readModuleMap,
retrieveModuleIntegrityDigests,
retrieveGitSha,
testCdnUrl,
} from './helpers/moduleMap';
import {
searchForNextLogMatch,
Expand All @@ -55,6 +57,51 @@ yargs.array('scanEnvironment');
jest.setTimeout(95000);

describe('Tests that require Docker setup', () => {
describe('one-app startup with bad module module', () => {
let originalModuleMap;
const oneAppLocalPortToUse = getRandomPortNumber();
const oneAppMetricsLocalPortToUse = getRandomPortNumber();
let browser;
const moduleName = 'unhealthy-frank';
const version = '0.0.0';
beforeAll(async () => {
originalModuleMap = readModuleMap();

await addModuleToModuleMap({
moduleName,
version,
});
({ browser } = await setUpTestRunner({ oneAppLocalPortToUse, oneAppMetricsLocalPortToUse }));
});

afterAll(async () => {
await tearDownTestRunner({ browser });
writeModuleMap(originalModuleMap);
});
test('one-app starts up successfully with a bad module', async () => {
const revertErrorMatch = /There was an error loading module (?<moduleName>.*) at (?<url>.*). Ignoring (?<workingModule>.*) until .*/;
const requiredExternalsError = searchForNextLogMatch(revertErrorMatch);
const loggedError = await requiredExternalsError;
const [,
problemModule,
problemModuleUrl,
workingUrl,
] = revertErrorMatch.exec(loggedError);
const gitSha = await retrieveGitSha();
await expect(requiredExternalsError).resolves.toMatch(revertErrorMatch);
expect(problemModule).toBe(moduleName);
expect(problemModuleUrl).toBe(`${testCdnUrl}/${gitSha}/${moduleName}/${version}/${moduleName}.node.js`);
// eslint-disable-next-line no-useless-escape
Matthew-Mallimo marked this conversation as resolved.
Show resolved Hide resolved
expect(workingUrl).toBe(moduleName);
});
test('one-app remains healthy with a bad module at start', async () => {
await browser.url('https://one-app:8443/success');
const header = await browser.$('.helloMessage');
const headerText = await header.getText();
expect(headerText).toBe('Hello! One App is successfully rendering its Modules!');
});
});

describe('one-app successfully started', () => {
const defaultFetchOptions = createFetchOptions();
let originalModuleMap;
Expand All @@ -71,6 +118,7 @@ describe('Tests that require Docker setup', () => {

beforeAll(async () => {
removeModuleFromModuleMap('late-frank');
removeModuleFromModuleMap('unhealthy-frank');
Matthew-Mallimo marked this conversation as resolved.
Show resolved Hide resolved
originalModuleMap = readModuleMap();
({ browser } = await setUpTestRunner({ oneAppLocalPortToUse, oneAppMetricsLocalPortToUse }));
});
Expand Down Expand Up @@ -700,30 +748,71 @@ describe('Tests that require Docker setup', () => {
});

describe('child module `requiredExternals` invalid usage', () => {
const requiredExternalsErrorMatch = /Failed to get external react-intl from root module/;

const moduleName = 'cultured-frankie';
const version = '0.0.1';

let requiredExternalsError;
afterEach(() => {
writeModuleMap(originalModuleMap);
});

beforeAll(async () => {
requiredExternalsError = searchForNextLogMatch(requiredExternalsErrorMatch);
test('fails to get external `react-intl` for child module as an unsupplied `requiredExternal` - logs failure', async () => {
const requiredExternalsErrorMatch = /Failed to get external react-intl from root module/;

const requiredExternalsError = searchForNextLogMatch(requiredExternalsErrorMatch);
await addModuleToModuleMap({
moduleName,
version,
});
// not ideal but need to wait for app to poll;

await waitFor(5000);
});

afterAll(() => {
writeModuleMap(originalModuleMap);
await expect(requiredExternalsError).resolves.toMatch(requiredExternalsErrorMatch);
});

test('fails to get external `react-intl` for child module as an unsupplied `requiredExternal`', async () => {
await expect(requiredExternalsError).resolves.toMatch(requiredExternalsErrorMatch);
test('fails to get external `react-intl` for child module as an unsupplied `requiredExternal` - Logs reverting message', async () => {
const revertErrorMatch = /There was an error loading module (?<moduleName>.*) at (?<url>.*). Reverting back to (?<workingModule>.*)/;
const requiredExternalsError = searchForNextLogMatch(revertErrorMatch);
await addModuleToModuleMap({
moduleName,
version,
});
// not ideal but need to wait for app to poll;
await waitFor(5000);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we loading the same module 'cultured-frankie'; again? if so we should try and merge this test with the one above if you are able to get both error messages at the same time. This is to avoid overloading the integration tests and make them take longer unnecessarily

Copy link
Member Author

Choose a reason for hiding this comment

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

I was only able to get one regex matcher to work at a time within a single test case. Thats why I added another test case

Copy link
Contributor

Choose a reason for hiding this comment

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

What does the log look like? does it log both lines?
/Failed to get external react-intl from root module/
and
/There was an error loading module (?<moduleName>.*) at (?<url>.*). Reverting back to (?<workingModule>.*)?

If so we should try and find why the regex is not matching in one single case, loading the same module to the module map twice and that 5sec delay is expensive if we can avoid it 👍

const loggedError = await requiredExternalsError;
const [,
problemModule,
problemModuleUrl,
workingUrl,
] = revertErrorMatch.exec(loggedError);
const gitSha = await retrieveGitSha();
await expect(requiredExternalsError).resolves.toMatch(revertErrorMatch);
expect(problemModule).toBe('cultured-frankie');
expect(problemModuleUrl).toBe(`${testCdnUrl}/${gitSha}/${moduleName}/${version}/${moduleName}.node.js`);
// eslint-disable-next-line no-useless-escape
expect(workingUrl).toBe(`${testCdnUrl}/${gitSha}/${moduleName}/0.0.0/${moduleName}.node.js\"}`);
});
test('fails to get external `semver` for child module as an unsupplied `requiredExternal` for new module in mooduleMap', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

have we not tested the same scenario above when requiring react-intl? the only difference I can see is unhealthy-frank tries to load a different external semver?

If this test is to test the scenario when 2 modules with broken required externals are deployed consecutively, then the afterEach in line 754 is resetting the module map anyway and removing cultured-frankie from the previous test

Copy link
Member Author

Choose a reason for hiding this comment

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

This test case test when a new module is added to the module map with an error. The previous test case tests when a module's version is updated with an issue

Copy link
Contributor

Choose a reason for hiding this comment

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

right so we are testing 1 case when the same module is updated (version bump) and it is broken and also when a brand new module which is broken is added for the first time? is that right?

const revertErrorMatch = /There was an error loading module (?<moduleName>.*) at (?<url>.*). Ignoring (?<ignoredModule>.*) until .*/;
const requiredExternalsError = searchForNextLogMatch(revertErrorMatch);
const modName = 'unhealthy-frank';
const modVersion = '0.0.0';
await addModuleToModuleMap({
moduleName: modName,
version: modVersion,
});
// not ideal but need to wait for app to poll;
Copy link
Member

Choose a reason for hiding this comment

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

Please set a lower max poll time instead of waiting a full 5s

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

await waitFor(5000);
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we set a lower max pall time for the integration tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

const loggedError = await requiredExternalsError;
const [,
problemModule,
problemModuleUrl,
ignoredModule,
] = revertErrorMatch.exec(loggedError);
const gitSha = await retrieveGitSha();
await expect(requiredExternalsError).resolves.toMatch(revertErrorMatch);
expect(problemModule).toBe(modName);
expect(problemModuleUrl).toBe(`${testCdnUrl}/${gitSha}/${modName}/${modVersion}/${modName}.node.js`);
expect(ignoredModule).toBe(modName);
});

test('does not modify the original version "0.0.0" of the failing module', async () => {
Expand Down
14 changes: 6 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"helmet": "^3.22.0",
"holocron": "^1.1.4",
"holocron": "^1.2.0",
"holocron-module-route": "^1.1.4",
"if-env": "^1.0.4",
"immutable": "^4.0.0",
Expand Down
3 changes: 3 additions & 0 deletions prod-sample/sample-modules/unhealthy-frank/0.0.0/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["amex"]
}
1 change: 1 addition & 0 deletions prod-sample/sample-modules/unhealthy-frank/0.0.0/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry=https://registry.npmjs.org
3 changes: 3 additions & 0 deletions prod-sample/sample-modules/unhealthy-frank/0.0.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# unhealthy-frank

A module that has mismatching requiredExternals.
32 changes: 32 additions & 0 deletions prod-sample/sample-modules/unhealthy-frank/0.0.0/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "unhealthy-frank",
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we can reuse any of the other franks rather than creating a brand new one? If we deploy the correct version of the root module that doesn't provide any externals, I think cultured-frankie@0.0.1 should be enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was easier to add a new module that only had one version with an issue, than to manage an existing module with one working version and one broken one

Copy link
Contributor

Choose a reason for hiding this comment

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

I am just thinking that it is more work to maintain another frank but I guess now we removed the package-locks from them we shouldn't need to change them too much.

Is it achievable with the franks that we already got? if so maybe double check with the team to see what's their preference

Copy link
Contributor

Choose a reason for hiding this comment

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

maintenance aside, building another module would also be expensive and increase integration tests build time

Copy link
Member

Choose a reason for hiding this comment

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

An additional version of an existing module would have the same downside.

"version": "0.0.0",
"description": "A module that works, nothing more, nothing less.",
"scripts": {
"prebuild": "npm run clean",
"build": "bundle-module",
"clean": "rimraf build",
"prepare": "npm run build"
},
"dependencies": {
"@americanexpress/one-app-router": "^1.1.0",
"holocron": "^1.1.4",
"holocron-module-route": "^1.1.4",
"prop-types": "^15.5.9",
"react": "^16.14.0",
"react-redux": "^7.2.4",
"semver": "7.3.5"
},
"devDependencies": {
"@americanexpress/one-app-bundler": "^6.14.1",
"babel-preset-amex": "^3.5.0",
"rimraf": "^2.5.2"
},
"one-amex": {
"bundler": {
"requiredExternals": [
"semver"
Matthew-Mallimo marked this conversation as resolved.
Show resolved Hide resolved
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2019 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import React from 'react';
import semver from 'semver';

export default function UnhealthyFrank() {
return (
<div>
<h1 className="helloFrank">Im Frank, and unhealthy. is 1.2.3 valid semver? {semver.valid('1.2.3')}</h1>
</div>
);
}
19 changes: 19 additions & 0 deletions prod-sample/sample-modules/unhealthy-frank/0.0.0/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2019 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import ModuleContainer from './components/UnhealthyFrank';

export default ModuleContainer;