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

11034 extend configuration #11558

Merged
merged 36 commits into from Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
73bd73c
Exposed variablesMeta in serverless
mklenbw Nov 24, 2022
b08bdc9
Added variables meta to serverless
mklenbw Nov 24, 2022
bb9223e
Added extendConfiguration
mklenbw Nov 24, 2022
17b32eb
Fixed merging
mklenbw Nov 24, 2022
d771c75
Fixed variablesMeta handling
mklenbw Nov 24, 2022
910b4e4
Cleaned up feature code
mklenbw Nov 25, 2022
5de376e
Added tests for extendConfiguration
mklenbw Nov 25, 2022
ae55f45
Added try/catch for deep copy
mklenbw Nov 28, 2022
161b212
Changed configurationPath to key array
mklenbw Nov 28, 2022
95777f7
Ensure array
mklenbw Nov 28, 2022
08ba688
Ensure array length
mklenbw Nov 28, 2022
1eecaf5
Revert variable path change
mklenbw Nov 28, 2022
2628e75
Revert variable path change
mklenbw Nov 29, 2022
c9ad3f5
Resolved review comments
mklenbw Dec 6, 2022
9840953
Added pending zero to testing
mklenbw Dec 6, 2022
f0f18e1
Added condition to ensure properties with same prefix are kept
mklenbw Dec 6, 2022
7d75b79
Removed unused parameter
mklenbw Dec 12, 2022
1d3fc6f
Always prepare resolverConfiguration;
mklenbw Dec 12, 2022
18b3a57
Run tests on fixture
mklenbw Dec 12, 2022
5c07007
Fixed minor errors
mklenbw Dec 12, 2022
68e20ab
Added comment
mklenbw Dec 13, 2022
7e3f5c7
Changed error text
mklenbw Dec 13, 2022
e6205bb
Changed error type
mklenbw Dec 13, 2022
202a12b
Removed ids as we are using default errors
mklenbw Dec 13, 2022
0879b0e
Changed error texts
mklenbw Dec 13, 2022
2306254
Add resolverConfiguraiton after init if config is extended
mklenbw Dec 13, 2022
4a0cea0
Simplified tests and fixture
mklenbw Dec 13, 2022
c96ff53
Moved paths to fixture
mklenbw Dec 13, 2022
b85712b
Added check for pathKeys not being an array
mklenbw Dec 13, 2022
532cf5e
Fixed searchpath
mklenbw Dec 15, 2022
1a48db8
Fail with throw
mklenbw Dec 15, 2022
3781996
Remove aws-region
mklenbw Dec 15, 2022
799f5f7
Removed try/catch for test
mklenbw Dec 15, 2022
5e0e9a0
Removed assertion
mklenbw Dec 15, 2022
ea29c16
Added documentation
mklenbw Dec 15, 2022
a3d4fe9
Clearified usage
mklenbw Dec 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 48 additions & 0 deletions lib/serverless.js
Expand Up @@ -2,6 +2,7 @@

const path = require('path');
const os = require('os');
const _ = require('lodash');
const ensureString = require('type/string/ensure');
const ensureValue = require('type/value/ensure');
const ensureArray = require('type/array/ensure');
Expand All @@ -23,6 +24,7 @@ const eventuallyUpdate = require('./utils/eventually-update');
const commmandsSchema = require('./cli/commands-schema');
const resolveCliInput = require('./cli/resolve-input');
const isDashboardEnabled = require('./configuration/is-dashboard-enabled');
const parseEntries = require('./configuration/variables/resolve-meta').parseEntries;

// Old local fallback is triggered in older versions by Serverless constructor directly
const isStackFromOldLocalFallback = RegExp.prototype.test.bind(
Expand Down Expand Up @@ -99,6 +101,7 @@ class Serverless {
// Old variables resolver is dropped, yet some plugins access service properties through
// `variables` class. Below patch ensures those plugins won't get broken
this.variables = { service: this.service };
this.variablesMeta = config.variablesMeta;
this.pluginManager = new PluginManager(this);
this.configSchemaHandler = new ConfigSchemaHandler(this);

Expand All @@ -116,6 +119,7 @@ class Serverless {
this.serverlessDirPath = path.join(os.homedir(), '.serverless');
this.isStandaloneExecutable = isStandaloneExecutable;
this.triggeredDeprecations = logDeprecation.triggeredDeprecations;
this.isConfigurationExtendable = true;

// TODO: Remove once "@serverless/dashboard-plugin" is integrated into this repository
this._commandsSchema = commmandsSchema;
Expand All @@ -136,6 +140,7 @@ class Serverless {
await this.service.load(this.processedInput.options);
// load all plugins
await this.pluginManager.loadAllPlugins(this.service.plugins);
this.isConfigurationExtendable = false;
// give the CLI the plugins and commands so that it can print out
// information such as options when the user enters --help
this.cli.setLoadedPlugins(this.pluginManager.getPlugins());
Expand Down Expand Up @@ -215,6 +220,49 @@ class Serverless {
logDeprecation(code, message) {
return this._logDeprecation(`EXT_${ensureString(code)}`, ensureString(message));
}

extendConfiguration(configurationPathKeys, value) {
ensureArray(configurationPathKeys, {
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
ensureItem: ensureString,
});
if (configurationPathKeys.length < 1) {
throw new ServerlessError(
'ConfigurationPathKeys needs to contain at least one element.',
'INVALID_EXTEND_AFTER_INIT'
);
}

if (!this.isConfigurationExtendable) {
throw new ServerlessError(
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
'ExtendConfiguration cannot be used after init.',
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
'INVALID_EXTEND_AFTER_INIT'
);
}
try {
value = JSON.parse(JSON.stringify(value));
} catch (error) {
throw new ServerlessError(
'ExtendConfiguration called with invalid data. Value is not json-serializable. Error: ${error}',
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
'INVALID_EXTEND_VALUE'
);
}

const configurationPath = configurationPathKeys.join('.');
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
_.set(this.configurationInput, configurationPath, value);

const metaPathPrefix = configurationPathKeys.join('\0');
for (const key of this.variablesMeta.keys()) {
if (key.startsWith(metaPathPrefix)) {
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
this.variablesMeta.delete(key);
}
}
if (typeof value !== 'object') {
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
const lastKey = configurationPathKeys.pop();
parseEntries({ [lastKey]: value }, configurationPathKeys, this.variablesMeta);
} else {
parseEntries(Object.entries(value), configurationPathKeys, this.variablesMeta);
}
}
}

module.exports = Serverless;
1 change: 1 addition & 0 deletions scripts/serverless.js
Expand Up @@ -598,6 +598,7 @@ processSpanPromise = (async () => {
commands[0] === 'plugin' || Boolean(variablesMeta && !variablesMeta.size),
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
commands,
options,
variablesMeta,
});

try {
Expand Down
149 changes: 149 additions & 0 deletions test/unit/lib/serverless.test.js
@@ -1,8 +1,10 @@
'use strict';

const chai = require('chai');
const sinon = require('sinon');

chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

const { expect } = chai;

Expand Down Expand Up @@ -174,4 +176,151 @@ describe('test/unit/lib/serverless.test.js', () => {
expect(serverless.config).to.have.property('servicePath');
});
});

describe('Extend configuration', () => {
let serverless;
let parseEntriesStub;

before(async () => {
parseEntriesStub = sinon.stub().returns(new Map([]));
try {
await runServerless({
noService: true,
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
command: 'info',
modulesCacheStub: {
'./lib/configuration/variables/resolve-meta': {
parseEntries: parseEntriesStub,
},
},
hooks: {
beforeInstanceInit: (serverlessInstance) => {
mklenbw marked this conversation as resolved.
Show resolved Hide resolved
serverless = serverlessInstance;
throw Error('Stop serverless before init.');
},
},
});
} catch (error) {
// Not an error
}

serverless.pluginManager = sinon.createStubInstance(PluginManager);
serverless.classes.CLI = sinon.stub(serverless.classes.CLI);
});

after(() => {
sinon.restore();
});

beforeEach(async () => {
serverless.isConfigurationExtendable = true;
serverless.variablesMeta = new Map([]);
});

it('Should add configuration path if it not exists', async () => {
const initialConfig = {
'pre-existing': {
path: true,
},
};

const path = 'path.to.insert';
const value = {
newKey: 'value',
};
serverless.configurationInput = JSON.parse(JSON.stringify(initialConfig));
serverless.extendConfiguration(path.split('.'), value);

expect(serverless.configurationInput).to.deep.include(initialConfig);
expect(serverless.configurationInput).to.deep.nested.include({ [path]: value });
});

it('Should assign configuration object if it exists', async () => {
const path = 'pre-existing';
const initialConfig = {
[path]: {
path: true,
},
};

const value = {
newKey: 'value',
};
serverless.configurationInput = JSON.parse(JSON.stringify(initialConfig));
serverless.extendConfiguration(path.split('.'), value);

expect(serverless.configurationInput[path]).to.deep.include(value);
});

it('Should assign trivial types', async () => {
const path = 'pre-existing';
const initialConfig = {
[path]: 'some string',
};
const value = 'other string';
serverless.configurationInput = JSON.parse(JSON.stringify(initialConfig));
serverless.extendConfiguration(path.split('.'), value);
expect(serverless.configurationInput[path]).to.equal(value);
});

it('Should deeply copy the new value', async () => {
const path = 'level1';
const subpath = 'level2';
const subvalue = {
level3: 'copy',
};
const value = {
[subpath]: subvalue,
};
serverless.configurationInput = {};
serverless.extendConfiguration(path.split('.'), value);

expect(serverless.configurationInput[path]).to.not.equal(value);
expect(serverless.configurationInput[path][subpath]).to.not.equal(subvalue);
});

it('Should clean variables meta in configurationPath', async () => {
const path = 'path.to.insert';
const value = {
newKey: 'value',
};
serverless.configurationInput = {};
const metaToKeep = ['path.to.keep'].map((_) => _.replace(/\./g, '\0'));
const metaToDelete = [path, `${path}.child`].map((_) => _.replace(/\./g, '\0'));
const variablesMeta = metaToKeep.concat(metaToDelete);

const keyStub = sinon.stub(serverless.variablesMeta, 'keys');
keyStub.returns(variablesMeta);

const deleteStub = sinon.stub(serverless.variablesMeta, 'delete');

serverless.extendConfiguration(path.split('.'), value);

metaToDelete.forEach((meta) => {
expect(deleteStub).to.have.been.calledWith(meta);
});
});

it('Should throw if called after init', async () => {
let localServerless;
await runServerless({
config: {
service: 'test',
provider: {
name: 'aws',
},
},
command: 'print',
hooks: {
beforeInstanceInit: (serverlessInstance) => {
localServerless = serverlessInstance;
const { hooks: lifecycleHooks } = serverlessInstance.pluginManager;
for (const hookName of Object.keys(lifecycleHooks)) {
delete lifecycleHooks[hookName];
}
},
},
});
expect(() => localServerless.extendConfiguration([], {})).to.throw(ServerlessError);
});
});
});