diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 5ce20e36..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 120, - "singleQuote": true, - "tabWidth": 4, - "arrowParens": "avoid", - "trailingComma": "none", - "htmlWhitespaceSensitivity": "strict" -} diff --git a/index.js b/index.js index a45c357f..76ab1a29 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,11 @@ const TemplateTagFactory = require('./lib/templateTagFactory'); const TokenFactory = require('./lib/tokenFactory'); const TriggerFactory = require('./lib/triggerFactory'); const UserFactory = require('./lib/userFactory'); +const PipelineTemplateFactory = require('./lib/pipelineTemplateFactory'); +const PipelineTemplateVersionFactory = require('./lib/pipelineTemplateVersionFactory'); +const TemplateMetaFactory = require('./lib/templateMetaFactory'); +const PipelineTemplateVersion = require('./lib/pipelineTemplateVersion'); +const TemplateMeta = require('./lib/templateMeta'); module.exports = { BannerFactory, @@ -35,5 +40,10 @@ module.exports = { TemplateTagFactory, TokenFactory, TriggerFactory, - UserFactory + UserFactory, + PipelineTemplateFactory, + PipelineTemplateVersionFactory, + TemplateMetaFactory, + PipelineTemplateVersion, + TemplateMeta }; diff --git a/lib/baseFactory.js b/lib/baseFactory.js index 84c34033..68caaa29 100644 --- a/lib/baseFactory.js +++ b/lib/baseFactory.js @@ -279,7 +279,7 @@ class BaseFactory { * Cleanup */ cleanUp() { - // no-op when not implemted by extender + // no-op when not implemented by extender } /** diff --git a/lib/jobTemplateTagFactory.js b/lib/jobTemplateTagFactory.js new file mode 100644 index 00000000..dd49adc3 --- /dev/null +++ b/lib/jobTemplateTagFactory.js @@ -0,0 +1,31 @@ +'use strict'; + +const BaseFactory = require('./baseFactory'); +const TemplateTagFactory = require('./templateTagFactory'); + +let instance; + +class JobTemplateTagFactory extends TemplateTagFactory { + /** + * Get the template type + * @returns {string} + */ + _getTemplateType() { + return 'JOB'; + } + + /** + * Get an instance of the JobTemplateTagFactory + * @method getInstance + * @param {Object} config + * @param {Datastore} config.datastore + * @return {JobTemplateTagFactory} + */ + static getInstance(config) { + instance = BaseFactory.getInstance(JobTemplateTagFactory, instance, config); + + return instance; + } +} + +module.exports = JobTemplateTagFactory; diff --git a/lib/pipelineTemplate.js b/lib/pipelineTemplate.js new file mode 100644 index 00000000..cefc6194 --- /dev/null +++ b/lib/pipelineTemplate.js @@ -0,0 +1,7 @@ +'use strict'; + +const TemplateMetaModel = require('./templateMeta'); + +class PipelineTemplate extends TemplateMetaModel {} + +module.exports = PipelineTemplate; diff --git a/lib/pipelineTemplateFactory.js b/lib/pipelineTemplateFactory.js new file mode 100644 index 00000000..158bc2d0 --- /dev/null +++ b/lib/pipelineTemplateFactory.js @@ -0,0 +1,31 @@ +'use strict'; + +const TemplateMetaFactory = require('./templateMetaFactory'); +const BaseFactory = require('./baseFactory'); + +let instance; + +class PipelineTemplateFactory extends TemplateMetaFactory { + /** + * Get the template type + * @returns {string} + */ + _getTemplateType() { + return 'PIPELINE'; + } + + /** + * Get an instance of the PipelineTemplateFactory + * @method getInstance + * @param {Object} config + * @param {Datastore} config.datastore + * @return {PipelineTemplateFactory} + */ + static getInstance(config) { + instance = BaseFactory.getInstance(PipelineTemplateFactory, instance, config); + + return instance; + } +} + +module.exports = PipelineTemplateFactory; diff --git a/lib/pipelineTemplateTagFactory.js b/lib/pipelineTemplateTagFactory.js new file mode 100644 index 00000000..933fa4e1 --- /dev/null +++ b/lib/pipelineTemplateTagFactory.js @@ -0,0 +1,31 @@ +'use strict'; + +const BaseFactory = require('./baseFactory'); +const TemplateTagFactory = require('./templateTagFactory'); + +let instance; + +class PipelineTemplateTagFactory extends TemplateTagFactory { + /** + * Get the template type + * @returns {string} + */ + _getTemplateType() { + return 'PIPELINE'; + } + + /** + * Get an instance of the PipelineTemplateTagFactory + * @method getInstance + * @param {Object} config + * @param {Datastore} config.datastore + * @return {PipelineTemplateTagFactory} + */ + static getInstance(config) { + instance = BaseFactory.getInstance(PipelineTemplateTagFactory, instance, config); + + return instance; + } +} + +module.exports = PipelineTemplateTagFactory; diff --git a/lib/pipelineTemplateVersion.js b/lib/pipelineTemplateVersion.js new file mode 100644 index 00000000..33b229fe --- /dev/null +++ b/lib/pipelineTemplateVersion.js @@ -0,0 +1,24 @@ +'use strict'; + +const BaseModel = require('./base'); + +class PipelineTemplateVersionModel extends BaseModel { + /** + * Construct a PipelineTemplateVersionModel object + * @method constructor + * @param {Object} config + * @param {Datastore} config.datastore Object that will perform operations on the datastore + * @param {String} config.name The template name + * @param {String} config.namespace The template namespace + * @param {String} config.version Version of the template + * @param {String} config.description Description of the template + * @param {String} config.maintainer Maintainer's email + * @param {Object} config.config Config of the screwdriver-template.yaml + * @param {String} config.pipelineId pipelineId of the template + */ + constructor(config) { + super('pipelineTemplateVersions', config); + } +} + +module.exports = PipelineTemplateVersionModel; diff --git a/lib/pipelineTemplateVersionFactory.js b/lib/pipelineTemplateVersionFactory.js new file mode 100644 index 00000000..66c6e640 --- /dev/null +++ b/lib/pipelineTemplateVersionFactory.js @@ -0,0 +1,147 @@ +'use strict'; + +const schema = require('screwdriver-data-schema'); +const BaseFactory = require('./baseFactory'); +const PipelineTemplateVersionModel = require('./pipelineTemplateVersion'); + +const EXACT_VERSION_REGEX = schema.config.regex.EXACT_VERSION; +const VERSION_REGEX = schema.config.regex.VERSION; +let instance; + +class PipelineTemplateVersionFactory extends BaseFactory { + /** + * Construct a TemplateFactory object + * @method constructor + * @param {Object} config + * @param {Datastore} config.datastore Object that will perform operations on the datastore + */ + constructor(config) { + super('pipelineTemplateVersions', config); + } + + /** + * Instantiate a PipelineTemplateVersionModel class + * @method createClass + * @param {Object} config Template data + * @param {Datastore} config.datastore Object that will perform operations on the datastore + * @param {String} config.name The template name + * @param {String} config.namespace The template namespace + * @param {String} config.version Version of the template + * @param {String} config.description Description of the template + * @param {String} config.maintainer Maintainer's email + * @param {Object} config.config Config of the screwdriver-template.yaml + * @param {String} config.pipelineId pipelineId of the template + * @return {PipelineTemplateVersionModel} + */ + createClass(config) { + return new PipelineTemplateVersionModel(config); + } + + /** + * Create a new template of the correct version (See schema definition) + * @method create + * @param {Object} config Config object + * @param templateMetaFactory + * @param {Datastore} config.datastore Object that will perform operations on the datastore + * @param {String} config.name The template name + * @param {String} config.namespace The template namespace + * @param {String} config.version Version of the template + * @param {String} config.description Description of the template + * @param {String} config.maintainer Maintainer's email + * @param {Object} config.config Config of the screwdriver-template.yaml + * @param {String} config.pipelineId pipelineId of the template + * @return {Promise} + */ + async create(config, templateMetaFactory) { + const createTime = new Date().toISOString(); + const [, configMajor, configMinor] = VERSION_REGEX.exec(config.version); + + // get the template meta + let pipelineTemplateMeta = await templateMetaFactory.get({ + name: config.name, + namespace: config.namespace + }); + + // if template meta doesn't exist, create one + if (!pipelineTemplateMeta) { + pipelineTemplateMeta = await templateMetaFactory.create({ + pipelineId: config.pipelineId, + namespace: config.namespace, + name: config.name, + maintainer: config.maintainer, + createTime, + updateTime: createTime + }); + } + + let newVersion = configMinor ? `${configMajor}${configMinor}.0` : `${configMajor}.0.0`; + + if (pipelineTemplateMeta.latestVersion) { + // list all the versions of the template + const pipelineTemplateVersions = await super.list({ + params: { + templateId: pipelineTemplateMeta.id, + sort: 'descending', + sortBy: 'createTime' + } + }); + + if (pipelineTemplateVersions.length > 0) { + // get latest version that have version starting with config.version + const pipelineTemplateVersion = pipelineTemplateVersions.find(template => { + const [, major, minor] = VERSION_REGEX.exec(template.version); + + return major === configMajor && minor === configMinor; + }); + + if (pipelineTemplateVersion) { + const [, targetMajor, targetMinor, targetPatch] = VERSION_REGEX.exec( + pipelineTemplateVersion.version + ); + const patch = parseInt(targetPatch.slice(1), 10) + 1; + + newVersion = `${targetMajor}${targetMinor}.${patch}`; + } + } + } + + const newPipelineTemplateVersion = await super.create({ + templateId: pipelineTemplateMeta.id, + description: config.description, + config: config.config, + createTime, + version: newVersion + }); + + const latestVersion = pipelineTemplateMeta.latestVersion || '0.0.0'; + const [, latestMajor, latestMinor, latestPatch] = EXACT_VERSION_REGEX.exec(latestVersion); + const [, major, minor, patch] = EXACT_VERSION_REGEX.exec(newVersion); + + if ( + major > latestMajor || + (major === latestMajor && minor >= latestMinor) || + (major === latestMajor && minor === latestMinor && patch > latestPatch) + ) { + pipelineTemplateMeta.latestVersion = newPipelineTemplateVersion.version; + pipelineTemplateMeta.updateTime = new Date().toISOString(); + await pipelineTemplateMeta.update(); + } + + return newPipelineTemplateVersion; + } + + /** + * Get an instance of the PipelineTemplateVersionFactory + * @method getInstance + * @param {Object} config + * @param {Datastore} config.datastore + * @return {PipelineTemplateVersionFactory} + */ + static getInstance(config) { + instance = BaseFactory.getInstance(PipelineTemplateVersionFactory, instance, config); + + return instance; + } +} + +module.exports = PipelineTemplateVersionFactory; diff --git a/lib/templateMeta.js b/lib/templateMeta.js new file mode 100644 index 00000000..8446c84f --- /dev/null +++ b/lib/templateMeta.js @@ -0,0 +1,24 @@ +'use strict'; + +const BaseModel = require('./base'); + +class TemplateMeta extends BaseModel { + /** + * Construct a TemplateModel object + * @method constructor + * @param {Object} config + * @param {Datastore} config.datastore Object that will perform operations on the datastore + * @param {String} config.name The template name + * @param {String} config.namespace The template namespace + * @param {String} config.version Version of the template + * @param {String} config.description Description of the template + * @param {String} config.maintainer Maintainer's email + * @param {Object} config.config Config of the screwdriver-template.yaml + * @param {String} config.pipelineId pipelineId of the template + */ + constructor(config) { + super('templateMeta', config); + } +} + +module.exports = TemplateMeta; diff --git a/lib/templateMetaFactory.js b/lib/templateMetaFactory.js new file mode 100644 index 00000000..6c8f3f90 --- /dev/null +++ b/lib/templateMetaFactory.js @@ -0,0 +1,47 @@ +'use strict'; + +const BaseFactory = require('./baseFactory'); +const TemplateMeta = require('./templateMeta'); + +class TemplateMetaFactory extends BaseFactory { + /** + * Construct a TemplateFactory object + * @method constructor + * @param {Object} config + * @param {Datastore} config.datastore Object that will perform operations on the datastore + */ + constructor(config) { + super('templateMeta', config); + } + + /** + * + * @param config + * @returns {TemplateMeta} + */ + createClass(config) { + return new TemplateMeta(config); + } + + create(config) { + config.templateType = this._getTemplateType(); + + return super.create(config); + } + + get(config) { + config.templateType = this.getTemplateType(); + + return super.get(config); + } + + getTemplateType() { + return this._getTemplateType(); + } + + _getTemplateType() { + throw new Error('Not implemented'); + } +} + +module.exports = TemplateMetaFactory; diff --git a/lib/templateTagFactory.js b/lib/templateTagFactory.js index 7fe4fc20..df2c0b70 100644 --- a/lib/templateTagFactory.js +++ b/lib/templateTagFactory.js @@ -129,6 +129,7 @@ class TemplateTagFactory extends BaseFactory { const nameObj = parseTemplateConfigName(config); const result = hoek.applyToDefaults(config, nameObj); + result.templateType = this.getTemplateType(); result.createTime = new Date().toISOString(); return super.create(result); diff --git a/test/lib/jobTemplateTagFactory.test.js b/test/lib/jobTemplateTagFactory.test.js new file mode 100644 index 00000000..39d7de4d --- /dev/null +++ b/test/lib/jobTemplateTagFactory.test.js @@ -0,0 +1,54 @@ +'use strict'; + +const { assert } = require('chai'); +const sinon = require('sinon'); + +sinon.assert.expose(assert, { prefix: '' }); + +describe('TemplateTag Factory', () => { + let JobTemplateTagFactory; + let datastore; + let factory; + + beforeEach(() => { + datastore = { + save: sinon.stub(), + get: sinon.stub(), + scan: sinon.stub() + }; + + // eslint-disable-next-line global-require + JobTemplateTagFactory = require('../../lib/jobTemplateTagFactory'); + + factory = new JobTemplateTagFactory({ datastore }); + }); + + afterEach(() => { + datastore = null; + }); + + describe('getTemplateType', () => { + it('should get an template type', () => { + const type = factory.getTemplateType(); + + assert.equal(type, 'JOB'); + }); + }); + + describe('getInstance', () => { + it('should throw when config not supplied', () => { + assert.throw(JobTemplateTagFactory.getInstance, Error, 'No datastore provided to JobTemplateTagFactory'); + }); + + it('should get an instance', () => { + const config = { datastore }; + const f1 = JobTemplateTagFactory.getInstance(config); + const f2 = JobTemplateTagFactory.getInstance(config); + + assert.instanceOf(f1, JobTemplateTagFactory); + assert.instanceOf(f2, JobTemplateTagFactory); + + assert.equal(f1, f2); + }); + }); +}); diff --git a/test/lib/pipelineTemplateFactory.test.js b/test/lib/pipelineTemplateFactory.test.js new file mode 100644 index 00000000..bfb9cd88 --- /dev/null +++ b/test/lib/pipelineTemplateFactory.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const { assert } = require('chai'); +const sinon = require('sinon'); + +sinon.assert.expose(assert, { prefix: '' }); + +describe('Pipeline Template Factory', () => { + let PipelineTemplateFactory; + let datastore; + let factory; + + beforeEach(() => { + datastore = { + save: sinon.stub(), + get: sinon.stub(), + scan: sinon.stub(), + update: sinon.stub() + }; + + /* eslint-disable global-require */ + PipelineTemplateFactory = require('../../lib/pipelineTemplateFactory'); + + factory = new PipelineTemplateFactory({ datastore }); + }); + + afterEach(() => { + datastore = null; + }); + + describe('getTemplateType', () => { + it('should get an template type', () => { + const type = factory.getTemplateType(); + + assert.equal(type, 'PIPELINE'); + }); + }); + + describe('getInstance', () => { + it('should throw when config not supplied', () => { + assert.throw( + PipelineTemplateFactory.getInstance, + Error, + 'No datastore provided to PipelineTemplateFactory' + ); + }); + it('should get an instance', () => { + const config = { datastore }; + const f1 = PipelineTemplateFactory.getInstance(config); + const f2 = PipelineTemplateFactory.getInstance(config); + + assert.instanceOf(f1, PipelineTemplateFactory); + assert.instanceOf(f2, PipelineTemplateFactory); + + assert.equal(f1, f2); + }); + }); +}); diff --git a/test/lib/pipelineTemplateTagFactory.test.js b/test/lib/pipelineTemplateTagFactory.test.js new file mode 100644 index 00000000..a634c2c9 --- /dev/null +++ b/test/lib/pipelineTemplateTagFactory.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const { assert } = require('chai'); +const sinon = require('sinon'); + +sinon.assert.expose(assert, { prefix: '' }); + +describe('TemplateTag Factory', () => { + let PipelineTemplateTagFactory; + let datastore; + let factory; + + beforeEach(() => { + datastore = { + save: sinon.stub(), + get: sinon.stub(), + scan: sinon.stub() + }; + + // eslint-disable-next-line global-require + PipelineTemplateTagFactory = require('../../lib/pipelineTemplateTagFactory'); + + factory = new PipelineTemplateTagFactory({ datastore }); + }); + + afterEach(() => { + datastore = null; + }); + + describe('getTemplateType', () => { + it('should get an template type', () => { + const type = factory.getTemplateType(); + + assert.equal(type, 'PIPELINE'); + }); + }); + + describe('getInstance', () => { + it('should throw when config not supplied', () => { + assert.throw( + PipelineTemplateTagFactory.getInstance, + Error, + 'No datastore provided to PipelineTemplateTagFactory' + ); + }); + + it('should get an instance', () => { + const config = { datastore }; + const f1 = PipelineTemplateTagFactory.getInstance(config); + const f2 = PipelineTemplateTagFactory.getInstance(config); + + assert.instanceOf(f1, PipelineTemplateTagFactory); + assert.instanceOf(f2, PipelineTemplateTagFactory); + + assert.equal(f1, f2); + }); + }); +}); diff --git a/test/lib/pipelineTemplateVersionFactory.test.js b/test/lib/pipelineTemplateVersionFactory.test.js new file mode 100644 index 00000000..6ecaf702 --- /dev/null +++ b/test/lib/pipelineTemplateVersionFactory.test.js @@ -0,0 +1,238 @@ +'use strict'; + +const { assert } = require('chai'); +const mockery = require('mockery'); +const sinon = require('sinon'); + +sinon.assert.expose(assert, { prefix: '' }); + +describe('PipelineTemplateVersion Factory', () => { + const namespace = 'namespace'; + const name = 'testPipelineTemplateVersion'; + const version = '1.3'; + const tag = 'latest'; + const metaData = { + name, + tag, + version + }; + let PipelineTemplateVersionFactory; + let datastore; + let factory; + let PipelineTemplateVersion; + let templateMetaFactoryMock; + + before(() => { + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + }); + + beforeEach(() => { + datastore = { + save: sinon.stub(), + get: sinon.stub(), + scan: sinon.stub() + }; + + templateMetaFactoryMock = { + get: sinon.stub(), + create: sinon.stub() + }; + + // eslint-disable-next-line global-require + PipelineTemplateVersion = require('../../lib/pipelineTemplateVersion'); + // eslint-disable-next-line global-require + PipelineTemplateVersionFactory = require('../../lib/pipelineTemplateVersionFactory'); + + factory = new PipelineTemplateVersionFactory({ datastore }); + }); + + afterEach(() => { + datastore = null; + mockery.deregisterAll(); + mockery.resetCache(); + }); + + after(() => { + mockery.disable(); + }); + + describe('createClass', () => { + it('should return a PipelineTemplateVersion model', () => { + const model = factory.createClass(metaData); + + assert.instanceOf(model, PipelineTemplateVersion); + }); + }); + + describe('create', async () => { + const generatedId = 1234135; + const generatedVersionId = 2341351; + let expected; + let returnValue; + + beforeEach(() => { + expected = { + id: generatedVersionId, + name, + version + }; + returnValue = [ + { + id: generatedId + 3, + name, + version: '2.1.2' + }, + { + id: generatedId + 2, + name, + version: '1.3.5' + }, + { + id: generatedId + 1, + name, + version: '1.3.1' + } + ]; + }); + + it('creates a pipeline template version given name, version and namespace', async () => { + expected.namespace = namespace; + const pipelineTemplateMetaMock = { + latestVersion: '2.1.2', + name: 'testPipelineTemplateVersion', + namespace, + update: sinon.stub().resolves() + }; + + templateMetaFactoryMock.get.resolves(pipelineTemplateMetaMock); + + datastore.scan.resolves(returnValue); + datastore.save.resolves(expected); + + const model = await factory.create( + { + name, + namespace, + version + }, + templateMetaFactoryMock + ); + + assert.calledWith(templateMetaFactoryMock.get, { + name, + namespace + }); + assert.calledOnce(datastore.scan); + assert.calledOnce(datastore.save); + assert.notCalled(templateMetaFactoryMock.create); + assert.notCalled(pipelineTemplateMetaMock.update); + assert.instanceOf(model, PipelineTemplateVersion); + assert.equal(model.id, generatedVersionId); + assert.equal(model.version, '1.3.6'); + }); + + it('creates a pipeline template meta and version when name and namespace does not exist', async () => { + templateMetaFactoryMock.get.resolves(null); + const pipelineTemplateMetaMock = { + pipelineId: 123, + name: 'testPipelineTemplateVersion', + namespace: 'example', + maintainer: 'abc', + latestVersion: null, + update: sinon.stub().resolves() + }; + + templateMetaFactoryMock.create.resolves(pipelineTemplateMetaMock); + datastore.scan.resolves([]); + datastore.save.resolves(expected); + + const model = await factory.create( + { + name, + namespace: 'example', + version + }, + templateMetaFactoryMock + ); + + assert.calledWith(templateMetaFactoryMock.get, { + name, + namespace: 'example' + }); + assert.calledOnce(templateMetaFactoryMock.create); + assert.notCalled(datastore.scan); + assert.calledOnce(datastore.save); + assert.calledOnce(pipelineTemplateMetaMock.update); + assert.instanceOf(model, PipelineTemplateVersion); + assert.equal(model.id, generatedVersionId); + assert.equal(model.version, '1.3.0'); + }); + + it('creates a pipeline template version given name with namespace exists but version does not exit', async () => { + const pipelineTemplateMetaMock = { + latestVersion: '2.1.2', + name, + namespace, + update: sinon.stub().resolves() + }; + + templateMetaFactoryMock.get.resolves(pipelineTemplateMetaMock); + + datastore.save.resolves(expected); + datastore.scan.resolves(returnValue); + expected.name = name; + expected.namespace = namespace; + + const model = await factory.create( + { + name, + namespace, + version: '3.1' + }, + templateMetaFactoryMock + ); + + assert.calledWith(templateMetaFactoryMock.get, { + name, + namespace + }); + assert.notCalled(templateMetaFactoryMock.create); + assert.calledOnce(datastore.scan); + assert.calledOnce(datastore.save); + assert.calledOnce(pipelineTemplateMetaMock.update); + assert.instanceOf(model, PipelineTemplateVersion); + assert.equal(model.id, generatedVersionId); + assert.equal(model.version, '3.1.0'); + assert.equal(pipelineTemplateMetaMock.latestVersion, '3.1.0'); + }); + }); + + describe('getInstance', () => { + let config; + + beforeEach(() => { + config = { datastore }; + }); + + it('should get an instance', () => { + const f1 = PipelineTemplateVersionFactory.getInstance(config); + const f2 = PipelineTemplateVersionFactory.getInstance(config); + + assert.instanceOf(f1, PipelineTemplateVersionFactory); + assert.instanceOf(f2, PipelineTemplateVersionFactory); + + assert.equal(f1, f2); + }); + + it('should throw when config not supplied', () => { + assert.throw( + PipelineTemplateVersionFactory.getInstance, + Error, + 'No datastore provided to PipelineTemplateVersionFactory' + ); + }); + }); +});