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): Merge job template steps for pipeline template validation [1] #163

Merged
merged 1 commit into from
Jun 11, 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
24 changes: 22 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const phaseMerge = require('./lib/phase/merge');
const {
flattenSharedIntoJobs,
handleMergeSharedStepsAnnotation,
flattenPhase: phaseFlatten
flattenPhase: phaseFlatten,
flattenTemplates
} = require('./lib/phase/flatten');
const phaseValidateFunctionality = require('./lib/phase/functional');
const phaseGeneratePermutations = require('./lib/phase/permutation');
Expand Down Expand Up @@ -314,7 +315,26 @@ async function parsePipelineTemplate({ yaml }) {
return pipelineTemplate;
}

/**
* Generates pipeline template configuration for the validator
* @method validatePipelineTemplate
* @param {Object} config
* @param {String} config.yaml Pipeline Template
* @param {TemplateFactory} config.templateFactory Template Factory to get templates
* @return {Object} Pipeline Template
*/
async function validatePipelineTemplate({ yaml, templateFactory }) {
const pipelineTemplate = await parsePipelineTemplate({ yaml });
// Merge template steps for validator
const { newJobs } = await flattenTemplates(pipelineTemplate.config, templateFactory, true);

pipelineTemplate.config.jobs = newJobs;

return pipelineTemplate;
}

module.exports = {
parsePipelineYaml,
parsePipelineTemplate
parsePipelineTemplate,
validatePipelineTemplate
};
30 changes: 22 additions & 8 deletions lib/phase/flatten.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,18 @@ function handleMergeSharedStepsAnnotation({ sharedConfig, jobConfig, template })
* @param {TemplateFactory} templateFactory Template Factory to get templates
* @param {Object} sharedConfig Shared configuration
* @param {Object} pipelineParameters Pipeline level parameters
* @param {Boolean} [isPipelineTemplate] If the current template is pipeline template or not
* @return {Promise}
*/
function mergeTemplateIntoJob({ jobName, jobConfig, newJobs, templateFactory, sharedConfig, pipelineParameters }) {
async function mergeTemplateIntoJob({
jobName,
jobConfig,
newJobs,
templateFactory,
sharedConfig,
pipelineParameters,
isPipelineTemplate
}) {
let oldJob = jobConfig;

// Try to get the template
Expand Down Expand Up @@ -390,7 +399,9 @@ function mergeTemplateIntoJob({ jobName, jobConfig, newJobs, templateFactory, sh
let warnings = [];

// merge shared steps into oldJob
oldJob = handleMergeSharedStepsAnnotation({ sharedConfig, jobConfig: oldJob, template });
if (!isPipelineTemplate) {
oldJob = handleMergeSharedStepsAnnotation({ sharedConfig, jobConfig: oldJob, template });
}

// Include parameters from the template only if it not overwritten either in pipeline or job parameters
if (newJob.parameters !== undefined) {
Expand All @@ -409,7 +420,7 @@ function mergeTemplateIntoJob({ jobName, jobConfig, newJobs, templateFactory, sh
} else {
delete newJob.parameters;
}
} else {
} else if (oldJob.parameters !== undefined) {
newJob.parameters = oldJob.parameters;
}

Expand All @@ -435,11 +446,12 @@ function mergeTemplateIntoJob({ jobName, jobConfig, newJobs, templateFactory, sh
* Goes through each job and if template is specified, then merge into job config
*
* @method flattenTemplates
* @param {Object} doc Document that went through structural parsing
* @param {TemplateFactory} templateFactory Template Factory to get templates
* @param {Object} doc Document that went through structural parsing
* @param {TemplateFactory} templateFactory Template Factory to get templates
* @param {Boolean} [isPipelineTemplate] If the current template is pipeline template or not
* @return {Promise} Resolves to new object with jobs after merging templates
*/
function flattenTemplates(doc, templateFactory) {
async function flattenTemplates(doc, templateFactory, isPipelineTemplate) {
const newJobs = {};
const templates = [];
const { jobs, shared, parameters } = doc;
Expand All @@ -466,7 +478,8 @@ function flattenTemplates(doc, templateFactory) {
newJobs,
templateFactory,
sharedConfig: shared,
pipelineParameters: parameters
pipelineParameters: parameters,
isPipelineTemplate
})
);
} else {
Expand Down Expand Up @@ -754,5 +767,6 @@ function flattenPhase(parsedDoc, templateFactory) {
module.exports = {
flattenPhase,
flattenSharedIntoJobs,
handleMergeSharedStepsAnnotation
handleMergeSharedStepsAnnotation,
flattenTemplates
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"joi": "^17.7.0",
"js-yaml": "^4.1.0",
"keymbinatorial": "^2.0.0",
"screwdriver-data-schema": "^23.3.1",
"screwdriver-data-schema": "^23.3.2",
"screwdriver-notifications-email": "^3.0.1",
"screwdriver-notifications-slack": "^5.0.0",
"screwdriver-workflow-parser": "^4.3.0",
Expand Down
3 changes: 1 addition & 2 deletions test/data/pipeline-template-invalid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ jobs:
- install: npm install
requires:
- ~pr
- ~commit

- ~commit
14 changes: 14 additions & 0 deletions test/data/validate-pipeline-template-invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace: template_namespace
name: template_name
version: 1.2.3
description: template description
maintainer: [email protected]
config:
shared:
image: node:20
environment:
FOO: foo
parameters:
user:
value: sd-bot
description: User running build
112 changes: 112 additions & 0 deletions test/data/validate-pipeline-template-with-job-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"config": {
"annotations": {
"screwdriver.cd/chainPR": false,
"screwdriver.cd/restrictPR": "none"
},
"jobs": {
"extra": {
"annotations": {},
"environment": {
"FOO": "BAR"
},
"image": "node:20",
"requires": [
"main"
],
"secrets": [],
"settings": {},
"sourcePaths": [],
"steps": [
{
"name": "echo \"pipeline template test\""
}
]
},
"main": {
"annotations": {},
"environment": {
"BAR": "foo",
"FOO": "BAR",
"SD_TEMPLATE_FULLNAME": "mytemplate",
"SD_TEMPLATE_NAME": "mytemplate",
"SD_TEMPLATE_NAMESPACE": "",
"SD_TEMPLATE_VERSION": "1.2.3"
},
"image": "golang",
"requires": [
"main"
],
"secrets": [
"GIT_KEY"
],
"settings": {
"email": "[email protected]"
},
"sourcePaths": [],
"steps": [
{
"install": "npm install"
},
{
"test": "npm test"
}
],
"templateId": 7754
},
"other": {
"annotations": {},
"environment": {
"BAR": "foo",
"FOO": "BAR",
"SD_TEMPLATE_FULLNAME": "mytemplate",
"SD_TEMPLATE_NAME": "mytemplate",
"SD_TEMPLATE_NAMESPACE": "",
"SD_TEMPLATE_VERSION": "1.2.3"
},
"image": "golang",
"requires": [
"main"
],
"secrets": [
"GIT_KEY"
],
"settings": {
"email": "[email protected]"
},
"sourcePaths": [],
"steps": [
{
"install": "npm install"
},
{
"test": "npm test"
}
],
"templateId": 7754
}
},
"parameters": {
"nameA": "value1"
},
"subscribe": {
"scmUrls": [
{
"https://github.com/VonnyJap/python-zero-to-hero.git": [
"~pr"
]
},
{
"https://github.com/VonnyJap/sshca.git": [
"~pr"
]
}
]
}
},
"description": "An example pipeline template for testing golang files",
"maintainer": "[email protected]",
"name": "example-template",
"namespace": "sd-test",
"version": "1.0.0"
}
31 changes: 31 additions & 0 deletions test/data/validate-pipeline-template-with-job-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace: sd-test
name: example-template
version: '1.0.0'
description: An example pipeline template for testing golang files
maintainer: [email protected]
config:
parameters:
nameA: "value1"
annotations:
screwdriver.cd/restrictPR: none
screwdriver.cd/chainPR: false
subscribe:
scmUrls:
- https://github.com/VonnyJap/python-zero-to-hero.git: ['~pr']
- https://github.com/VonnyJap/sshca.git: ['~pr']
shared:
image: golang
environment:
FOO: "BAR"
requires: [main]
steps:
- name: echo "bang"
jobs:
main:
template: sd/[email protected]
extra:
image: node:20
steps:
- name: echo "pipeline template test"
other:
template: sd/[email protected]
23 changes: 19 additions & 4 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { assert } = require('chai');
const fs = require('fs');
const path = require('path');
const sinon = require('sinon');
const { parsePipelineTemplate, parsePipelineYaml: parser } = require('..');
const { parsePipelineTemplate, parsePipelineYaml: parser, validatePipelineTemplate } = require('..');
const pipelineId = 111;

sinon.assert.expose(assert, { prefix: '' });
Expand Down Expand Up @@ -260,8 +260,6 @@ describe('config parser', () => {
triggerFactory,
pipelineId
}).then(data => {
console.log(data);
console.log(data.errors);
assert.match(
data.errors[0],
/Error: main job has invalid requires: baz. Triggers must be jobs from canary stage./
Expand Down Expand Up @@ -736,7 +734,7 @@ describe('config parser', () => {
pipelineTemplateTagFactory: pipelineTemplateTagFactoryMock,
pipelineTemplateVersionFactory: pipelineTemplateVersionFactoryMock
}).then(data => {
assert.deepEqual(data.errors[0], 'ValidationError: "jobs" is not allowed');
assert.deepEqual(data.errors[0], 'ValidationError: "jobs.main.steps" is not allowed');
}));

it('returns error if pipeline template does not exist', () => {
Expand Down Expand Up @@ -1146,4 +1144,21 @@ describe('config parser', () => {
assert.match(err.toString(), /[ValidationError]: "config.jobs" is required/);
}));
});

describe('validate pipeline template', () => {
it('flattens pipeline template for the validator and pulls in job template steps', () =>
validatePipelineTemplate({
yaml: loadData('validate-pipeline-template-with-job-template.yaml'),
templateFactory: templateFactoryMock
}).then(data => {
assert.deepEqual(data, JSON.parse(loadData('validate-pipeline-template-with-job-template.json')));
}));
it('throws error if pipeline template is invalid', () =>
validatePipelineTemplate({
yaml: loadData('validate-pipeline-template-invalid.yaml'),
templateFactory: templateFactoryMock
}).then(assert.fail, err => {
assert.match(err.toString(), /[ValidationError]: "config.jobs" is required/);
}));
});
});