Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion lib/classes/yaml-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const path = require('path');
const { pathToFileURL } = require('url');
const yaml = require('js-yaml');
const cloudformationSchema = require('../utils/serverless-utils/cloudformation-schema');
const isPlainObject = require('type/plain-object/is');
const ServerlessError = require('../serverless-error');
const { isExternalRefAccessDeniedError } = require('./yaml-parser/external-ref-errors');
Expand Down Expand Up @@ -64,7 +65,9 @@ const loadExternalDocument = (documentUrl, state) => {
state.documents.set(
documentUrl,
readExternalDocument(documentUrl, state.externalRefs).then((document) =>
yaml.load(Buffer.isBuffer(document) ? document.toString('utf8') : String(document))
yaml.load(Buffer.isBuffer(document) ? document.toString('utf8') : String(document), {
schema: cloudformationSchema,
})
)
);
}
Expand Down
24 changes: 1 addition & 23 deletions lib/utils/fs/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,12 @@ const jc = require('json-cycle');
const yaml = require('js-yaml');
const cloudformationSchema = require('../serverless-utils/cloudformation-schema');

const loadYaml = (contents, options) => {
let data;
let error;
try {
data = yaml.load(contents.toString(), options || {});
} catch (exception) {
error = exception;
}
return { data, error };
};

function parse(filePath, contents) {
// Auto-parse JSON
if (filePath.endsWith('.json') || filePath.endsWith('.tfstate')) {
return jc.parse(contents);
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
const options = {
filename: filePath,
};
let result = loadYaml(contents.toString(), options);
if (result.error && result.error.name === 'YAMLException') {
options.schema = cloudformationSchema;
result = loadYaml(contents.toString(), options);
}
if (result.error) {
throw result.error;
}
return result.data;
return yaml.load(contents.toString(), { filename: filePath, schema: cloudformationSchema });
}
return contents.toString().trim();
}
Expand Down
4 changes: 4 additions & 0 deletions lib/utils/serverless-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Source of truth:

Notes:

- `cloudformation-schema.js` intentionally diverges from upstream: it removes
implicit YAML timestamp resolution and re-registers `!!timestamp` as an
explicit type (https://github.com/oss-serverless/osls/issues/438). Preserve
this when re-syncing.
- `config.js` is a locally owned fork. It intentionally keeps the synchronous
`get('frameworkId')` and `get('meta.created_at')` lookups that Bref v2/v3 use
for best-effort telemetry if a future compatibility shim routes
Expand Down
10 changes: 9 additions & 1 deletion lib/utils/serverless-utils/cloudformation-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ const createSchema = () => {
const types = functionNames.flatMap((functionName) =>
['mapping', 'scalar', 'sequence'].map((kind) => yamlType(functionName, kind))
);
return yaml.DEFAULT_SCHEMA.extend(types);
// Drop implicit timestamps so date-shaped plain scalars (e.g. an IAM policy
// `Version: 2012-10-17`) stay strings; an explicit `!!timestamp` tag still constructs a Date
const implicitTypes = yaml.DEFAULT_SCHEMA.implicit.filter(
(type) => type.tag !== 'tag:yaml.org,2002:timestamp'
);
return new yaml.Schema({
implicit: implicitTypes,
explicit: [...yaml.DEFAULT_SCHEMA.explicit, yaml.types.timestamp],
}).extend(types);
};

module.exports = createSchema();
17 changes: 17 additions & 0 deletions test/unit/lib/classes/yaml-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ describe('YamlParser', () => {
.to.equal('bar');
});

it('should parse date-shaped values and shorthand tags in referenced files', () => {
const tmpDirPath = getTmpDirPath();

serverless.utils.writeFileSync(
path.join(tmpDirPath, 'ref.yml'),
'date: 2012-10-17\nref: !Ref Topic\n'
);

serverless.utils.writeFileSync(path.join(tmpDirPath, 'test.yml'), {
main: { $ref: './ref.yml' },
});

return expect(serverless.yamlParser.parse(path.join(tmpDirPath, 'test.yml')))
.to.eventually.have.property('main')
.to.deep.equal({ date: '2012-10-17', ref: { Ref: 'Topic' } });
});

it('should leave same-document refs in the root file untouched', async () => {
const tmpFilePath = getTmpFilePath('same-document.yml');

Expand Down
29 changes: 29 additions & 0 deletions test/unit/lib/configuration/read.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,35 @@ describe('test/unit/lib/configuration/read.test.js', () => {
});
});

it('should preserve date-shaped YAML values as strings', async () => {
configurationPath = 'serverless.yml';
await fsp.writeFile(
configurationPath,
[
'service: test-date-strings',
'provider:',
' name: aws',
' unquotedDate: 2020-12-12',
" quotedDate: '2020-12-12'",
' explicitlyTaggedDate: !!str 2020-12-12',
' unquotedDateTime: 2020-12-12T00:00:00Z',
' spacedDateTime: 2020-12-12 00:00:00',
'',
].join('\n')
);
expect(await readConfiguration(configurationPath)).to.deep.equal({
service: 'test-date-strings',
provider: {
name: 'aws',
unquotedDate: '2020-12-12',
quotedDate: '2020-12-12',
explicitlyTaggedDate: '2020-12-12',
unquotedDateTime: '2020-12-12T00:00:00Z',
spacedDateTime: '2020-12-12 00:00:00',
},
});
});

it('should read "serverless.json"', async () => {
configurationPath = 'serverless.json';
const configuration = {
Expand Down
4 changes: 4 additions & 0 deletions test/unit/lib/configuration/variables/sources/file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => {
yaml: '${file(file.yaml)}',
yml: '${file(file.yml)}',
json: '${file(file.json)}',
dateString: '${file(file-date.yml):date}',
tfstate: '${file(file.tfstate)}',
js: '${file(file.js)}',
cjs: '${file(file.cjs)}',
Expand Down Expand Up @@ -87,6 +88,9 @@ describe('test/unit/lib/configuration/variables/sources/file.test.js', () => {
it('should resolve "json" file sources', () =>
expect(configuration.json).to.deep.equal({ result: 'json' }));

it('should resolve date-shaped values as strings', () =>
expect(configuration.dateString).to.equal('2012-10-17'));

it('should resolve "tfstate" file sources', () =>
expect(configuration.tfstate).to.deep.equal({ result: 'tfstate' }));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
date: 2012-10-17
6 changes: 6 additions & 0 deletions test/unit/lib/utils/fs/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ describe('#parse()', () => {
});
});

it('should keep date-shaped values as strings and support explicit timestamp tags', () => {
const obj = parse('anything.yml', 'date: 2012-10-17\ntagged: !!timestamp 2020-12-12');
expect(obj.date).to.equal('2012-10-17');
expect(obj.tagged).to.be.instanceOf(Date);
});

it('should parse YAML without shorthand syntax', () => {
const tmpFilePath = 'anything.yml';
const fileContents = 'Item:\n Fn::Join:\n - ""\n - - "arn:aws:s3::"\n - !Ref MyBucket';
Expand Down
20 changes: 20 additions & 0 deletions test/unit/lib/utils/serverless-utils/cloudformation-schema.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const expect = require('chai').expect;
const yaml = require('js-yaml');
const cloudformationSchema = require('../../../../../lib/utils/serverless-utils/cloudformation-schema');

const load = (input) => yaml.load(input, { schema: cloudformationSchema });

describe('serverless-utils/cloudformation-schema', () => {
it('should keep date-shaped plain scalars and mapping keys as strings', () => {
expect(load('date: 2012-10-17').date).to.equal('2012-10-17');
expect(load('dateTime: 2020-12-12T00:00:00Z').dateTime).to.equal('2020-12-12T00:00:00Z');
expect(load('spaced: 2020-12-12 00:00:00').spaced).to.equal('2020-12-12 00:00:00');
expect(load('map:\n 2012-10-17: value').map).to.deep.equal({ '2012-10-17': 'value' });
});

it('should construct a Date for an explicit timestamp tag', () => {
expect(load('date: !!timestamp 2020-12-12').date).to.be.instanceOf(Date);
});
});