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

feat(3124): Add validatePipelineTemplate function [2] #40

Merged
merged 1 commit into from
Jun 13, 2024
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
46 changes: 42 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

const SCHEMA_CONFIG = require('screwdriver-data-schema').config.template.template;
const parser = require('screwdriver-config-parser').parsePipelineTemplate;
const {
parsePipelineTemplate: parseTemplate,
validatePipelineTemplate: validateTemplate
} = require('screwdriver-config-parser');
const Yaml = require('js-yaml');
const helper = require('./lib/helper');

Expand Down Expand Up @@ -80,7 +83,7 @@ async function flattenTemplate(templateObj, templateFactory) {

/**
* Parses the job configuration from a screwdriver-template.yaml
* @method parseTemplate
* @method parseJobTemplate
* @param {String} yamlString Contents of screwdriver-template.yaml
* @param {TemplateFactory} templateFactory Template Factory to get template from
* @return {Promise} Promise that rejects if the configuration cannot be parsed
Expand Down Expand Up @@ -135,7 +138,41 @@ async function parsePipelineTemplate(yamlString) {
const configToValidate = await loadTemplate(yamlString);

try {
const config = await parser({ yaml: yamlString });
const config = await parseTemplate({ yaml: yamlString });

return {
errors: [],
template: config
};
} catch (err) {
if (!err.details) {
throw err;
}

return {
errors: err.details,
template: configToValidate
};
}
}

/**
* Validates the pipeline configuration from a screwdriver-template.yaml
* @method validatePipelineTemplate
* @param {String} yamlString Contents of screwdriver-template.yaml
* @param {TemplateFactory} templateFactory Template Factory to get template from
* @return {Promise} Promise that rejects if the configuration cannot be validated
* The promise will eventually resolve into:
* {Object} result
* {Object} result.template The validated template that was validated
* {Object[]} result.errors An array of objects related to validating
* the given template
*/
async function validatePipelineTemplate(yamlString, templateFactory) {
const configToValidate = await loadTemplate(yamlString);

try {
const config = await validateTemplate({ yaml: yamlString, templateFactory });

return {
errors: [],
Expand All @@ -155,5 +192,6 @@ async function parsePipelineTemplate(yamlString) {

module.exports = {
parseJobTemplate,
parsePipelineTemplate
parsePipelineTemplate,
validatePipelineTemplate
};
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "screwdriver-template-validator",
"version": "7.0.0",
"version": "8.0.0",
"description": "A module for validating a Screwdriver Template file",
"main": "index.js",
"scripts": {
Expand Down Expand Up @@ -43,8 +43,8 @@
"@hapi/hoek": "^10.0.1",
"joi": "^17.7.0",
"js-yaml": "^4.1.0",
"screwdriver-data-schema": "^23.0.4",
"screwdriver-config-parser": "^10.0.0"
"screwdriver-data-schema": "^23.3.2",
"screwdriver-config-parser": "^10.2.0"
},
"release": {
"debug": false
Expand Down
178 changes: 178 additions & 0 deletions test/data/valid_full_pipeline_template_validated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"errors": [],
"template": {
"namespace": "template_namespace",
"name": "template_name",
"version": "1.2.3",
"description": "template description",
"maintainer": "[email protected]",
"config": {
"jobs": {
"main": {
"image": "node:18",
"environment": {
"BAR": "foo",
"FOO": "foo",
"SD_TEMPLATE_FULLNAME": "template_namespace/parent",
"SD_TEMPLATE_NAME": "parent",
"SD_TEMPLATE_NAMESPACE": "template_namespace",
"SD_TEMPLATE_VERSION": "1.2.3"
},
"settings": {
"email": "[email protected]"
},
"blockedBy": [
"~main"
],
"cache": true,
"description": "This is a description!",
"annotations": {
"foo": "a",
"bar": "b"
},
"freezeWindows": [
"* * ? * 1",
"0-59 0-23 * 1 ?"
],
"parameters": {
"color": [
"red",
"blue"
],
"node-version": {
"value": "18"
}
},
"provider": {
"accountId": 111111111111,
"buildRegion": "",
"clusterName": "sd-build-eks",
"computeType": "BUILD_GENERAL1_SMALL",
"debugSession": false,
"environmentType": "LINUX_CONTAINER",
"executor": "eks",
"executorLogs": false,
"name": "aws",
"privilegedMode": false,
"region": "us-west-2",
"role": "arn:aws:iam::111111111111:role/role"
},
"requires": [
"~commit"
],
"secrets": [
"GIT_KEY",
"NPM_TOKEN"
],
"sourcePaths": [
"src/A",
"src/AConfig"
],
"templateId": 7754,
"order": [
"install",
"test",
"other",
"echo"
],
"steps": [
{
"install": "npm install"
},
{
"test": "npm test"
},
{
"echo": "echo $FOO"
}
]
},
"test": {
"image": "node:18",
"environment": {
"BAR": "foo",
"FOO": "foo",
"SD_TEMPLATE_FULLNAME": "template_namespace/parent",
"SD_TEMPLATE_NAME": "parent",
"SD_TEMPLATE_NAMESPACE": "template_namespace",
"SD_TEMPLATE_VERSION": "1.2.3"
},
"settings": {
"email": "[email protected]"
},
"blockedBy": [
"~main"
],
"cache": true,
"description": "This is a description!",
"annotations": {
"foo": "a",
"bar": "b"
},
"freezeWindows": [
"* * ? * 1",
"0-59 0-23 * 1 ?"
],
"parameters": {
"color": [
"red",
"blue"
],
"node-version": {
"value": "18"
}
},
"provider": {
"accountId": 111111111111,
"buildRegion": "",
"clusterName": "sd-build-eks",
"computeType": "BUILD_GENERAL1_SMALL",
"debugSession": false,
"environmentType": "LINUX_CONTAINER",
"executor": "eks",
"executorLogs": false,
"name": "aws",
"privilegedMode": false,
"region": "us-west-2",
"role": "arn:aws:iam::111111111111:role/role"
},
"requires": [
"~commit"
],
"secrets": [
"GIT_KEY",
"NPM_TOKEN"
],
"sourcePaths": [
"src/A",
"src/AConfig"
],
"templateId": 7754,
"order": [
"install",
"test",
"other",
"echo"
],
"steps": [
{
"install": "npm install"
},
{
"test": "npm test"
},
{
"echo": "echo $FOO"
}
]
}
},
"parameters": {
"user": {
"value": "sd-bot",
"description": "User running build"
}
}
}
}
}
50 changes: 48 additions & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ describe('index test', () => {
it('parses a valid yaml', () =>
validator(loadData(VALID_FULL_PIPELINE_TEMPLATE_PATH)).then(config => {
assert.isObject(config);
assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template.json')));
assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template_parsed.json')));
}));

it('validates a poorly structured template', () =>
it('parses a poorly structured template', () =>
validator(loadData(BAD_STRUCTURE_PIPELINE_TEMPLATE_PATH)).then(result => {
assert.deepEqual(result.template, JSON.parse(loadData('bad_structure_pipeline_template.json')));
assert.strictEqual(result.errors.length, 2);
Expand All @@ -203,4 +203,50 @@ describe('index test', () => {
assert.match(err, /YAMLException/);
}));
});

describe('validate pipeline template', () => {
const templateFactoryMock = {
getTemplate: sinon.stub(),
getFullNameAndVersion: sinon.stub()
};

beforeEach(() => {
template = JSON.parse(loadData('template.json'));
templateLockedStep = JSON.parse(loadData('template_locked_step.json'), templateFactoryMock);

templateFactoryMock.getTemplate.resolves(template);
// eslint-disable-next-line global-require
validator = require('../index').validatePipelineTemplate;
Copy link
Contributor

Choose a reason for hiding this comment

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

validatePipelineTemplate: validateTemplate
I think the variable is renamed to validateTemplate.

});

it('validates a valid yaml', () =>
validator(loadData(VALID_FULL_PIPELINE_TEMPLATE_PATH), templateFactoryMock).then(config => {
assert.isObject(config);
assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template_validated.json')));
}));

it('validates a poorly structured template', () =>
validator(loadData(BAD_STRUCTURE_PIPELINE_TEMPLATE_PATH), templateFactoryMock).then(result => {
assert.deepEqual(result.template, JSON.parse(loadData('bad_structure_pipeline_template.json')));
assert.strictEqual(result.errors.length, 2);

// check required description
const missingField = hoek.reach(result.template, result.errors[0].path[0]);

assert.strictEqual(result.errors[0].message, '"description" is required');
assert.isUndefined(missingField);

// check incorrect type
const chain = `${result.errors[1].path[0]}.${result.errors[1].path[1]}`;
const incorrectType = hoek.reach(result.template, chain).image;

assert.strictEqual(result.errors[1].message, '"config.shared.image" must be a string');
assert.isNumber(incorrectType);
}, assert.fail));

it('throws when validating incorrectly formatted yaml', () =>
validator('main: :').then(assert.fail, err => {
assert.match(err, /YAMLException/);
}));
});
});