generator-tfs
Version:
Generates an app with CI/CD in Team Foundation Server
512 lines (416 loc) • 15.5 kB
JavaScript
const request = require(`request`);
const BUILD_API_VERSION = `2.0`;
const PROJECT_API_VERSION = `1.0`;
const RELEASE_API_VERSION = `3.0-preview.3`;
const DISTRIBUTED_TASK_API_VERSION = `3.0-preview.1`;
const SERVICE_ENDPOINTS_API_VERSION = `3.0-preview.1`;
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
function reconcileValue(first, second, fallback) {
return first ? first : (second ? second : fallback);
}
function getDefaultPortMapping(answers) {
if (answers.type === `java`) {
return `8080:8080`;
} else if (answers.type === `node`) {
return `3000:3000`;
} else {
return `80:80`;
}
}
function validateRequired(input, msg) {
return !input ? msg : true;
}
function validatePortMapping(input) {
return validateRequired(input, `You must provide a Port Mapping`);
}
function validateGroupID(input) {
return validateRequired(input, `You must provide a Group ID`);
}
function validateApplicationName(input) {
return validateRequired(input, `You must provide a name for your application`);
}
function validatePersonalAccessToken(input) {
return validateRequired(input, `You must provide a Personal Access Token`);
}
function validateTFS(input) {
return validateRequired(input, `You must provide your TFS URL including collection`);
}
function validateAzureSub(input) {
return validateRequired(input, `You must provide an Azure Subscription Name`);
}
function validateDockerHost(input) {
return validateRequired(input, `You must provide a Docker Host URL`);
}
function validateDockerCertificatePath(input) {
return validateRequired(input, `You must provide a Docker Certificate Path`);
}
function validateDockerHubID(input) {
return validateRequired(input, `You must provide a Docker Hub ID`);
}
function validateDockerHubPassword(input) {
return validateRequired(input, `You must provide a Docker Hub Password`);
}
function validateDockerHubEmail(input) {
return validateRequired(input, `You must provide a Docker Hub Email`);
}
function validateAzureSubID(input) {
return validateRequired(input, `You must provide an Azure Subscription ID`);
}
function validateAzureTenantID(input) {
return validateRequired(input, `You must provide an Azure Tenant ID`);
}
function validateServicePrincipalID(input) {
return validateRequired(input, `You must provide a Service Principal ID`);
}
function validateServicePrincipalKey(input) {
return validateRequired(input, `You must provide a Service Principal Key`);
}
function tokenize(input, nvp) {
for (var key in nvp) {
input = input.replaceAll(key, nvp[key]);
}
return input;
}
function encodePat(pat) {
'use strict';
// The personal access token must be 64 bit encoded to be used
// with the REST API
var b = new Buffer(`:` + pat);
var s = b.toString(`base64`);
return s;
}
function checkStatus(uri, token, gen, callback) {
'use strict';
// Simply issues a get to the provided URI and returns
// the body as JSON. Call this when the action taken
// requires time to process.
var options = {
"method": `GET`,
"headers": { "authorization": `Basic ${token}` },
"url": `${uri}`
};
request(options, function (err, res, body) {
callback(err, JSON.parse(body));
});
}
function tryFindDockerRegistryServiceEndpoint(account, projectId, dockerRegistry, token, callback) {
'use strict';
// Will NOT throw an error if the endpoint is not found. This is used
// by code that will create the endpoint if it is not found.
findDockerRegistryServiceEndpoint(account, projectId, dockerRegistry, token, function (e, ep) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, ep);
}
});
}
function findDockerRegistryServiceEndpoint(account, projectId, dockerRegistry, token, callback) {
'use strict';
// There is nothing to do
if (!dockerRegistry) {
callback(null, null);
return;
}
var options = {
"method": `GET`,
"headers": { "cache-control": `no-cache`, "authorization": `Basic ${token}` },
"url": `${account}/${projectId}/_apis/distributedtask/serviceendpoints`,
"qs": { "api-version": SERVICE_ENDPOINTS_API_VERSION }
};
request(options, function (error, response, body) {
var obj = JSON.parse(body);
// TODO: Test that authorization.parameters.registry === dockerHost. But that requires
// a second REST call once you know the ID of the dockerregistry type service endpoint.
// For now assume any dockerregistry service endpoint is safe to use.
var endpoint = obj.value.find(function (i) { return i.type === `dockerregistry`; });
if (endpoint === undefined) {
callback({ "message": `x Could not find Docker Registry Service Endpoint`, "code": `NotFound` }, undefined);
} else {
callback(error, endpoint);
}
});
}
function tryFindDockerServiceEndpoint(account, projectId, dockerHost, token, gen, callback) {
'use strict';
// Will NOT throw an error if the endpoint is not found. This is used
// by code that will create the endpoint if it is not found.
findDockerServiceEndpoint(account, projectId, dockerHost, token, gen, function (e, ep) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, ep);
}
});
}
function findDockerServiceEndpoint(account, projectId, dockerHost, token, gen, callback) {
'use strict';
// There is nothing to do
if (!dockerHost) {
callback(null, null);
return;
}
var options = {
"method": `GET`,
"headers": { "cache-control": `no-cache`, "authorization": `Basic ${token}` },
"url": `${account}/${projectId}/_apis/distributedtask/serviceendpoints`,
"qs": { "api-version": SERVICE_ENDPOINTS_API_VERSION }
};
request(options, function (error, response, body) {
// Check the response statusCode first. If it is not a 200
// the body will be html and not JSON
if (response.statusCode >= 400) {
callback(`Error trying to find Docker Service Endpoint: ${response.statusMessage}`);
return;
}
var obj = JSON.parse(body);
// The i.url is returned with a trailing / so just use starts with just in case
// the dockerHost is passed in without it
var endpoint = obj.value.find(function (i) { return i.url.startsWith(dockerHost); });
if (endpoint === undefined) {
callback({ "message": `x Could not find Docker Service Endpoint`, "code": `NotFound` }, undefined);
} else {
callback(error, endpoint);
}
});
}
function tryFindAzureServiceEndpoint(account, projectId, sub, token, gen, callback) {
'use strict';
// Will NOT throw an error if the endpoint is not found. This is used
// by code that will create the endpoint if it is not found.
findAzureServiceEndpoint(account, projectId, sub, token, gen, function (err, ep) {
if (err && err.code === `NotFound`) {
callback(null, undefined);
} else {
callback(err, ep);
}
});
}
function findAzureServiceEndpoint(account, projectId, sub, token, gen, callback) {
'use strict';
// There is nothing to do
if (!sub.name) {
callback(null, null);
return;
}
var options = {
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${account}/${projectId}/_apis/distributedtask/serviceendpoints`,
"qs": {
"api-version": SERVICE_ENDPOINTS_API_VERSION
}
};
request(options, function (error, response, body) {
var obj = JSON.parse(body);
var endpoint = obj.value.find(function (i) { return i.data.subscriptionName === sub.name; });
if (endpoint === undefined) {
callback({ "message": `x Could not find Azure Service Endpoint`, "code": `NotFound` }, undefined);
} else {
callback(error, endpoint);
}
});
}
function tryFindProject(account, project, token, gen, callback) {
'use strict';
// Will NOT throw an error if the project is not found. This is used
// by code that will create the project if it is not found.
findProject(account, project, token, gen, function (err, obj) {
if (err && err.code === `NotFound`) {
callback(null, undefined);
} else {
callback(err, obj);
}
});
}
function findProject(account, project, token, gen, callback) {
'use strict';
// Will throw an error if the project is not found. This is used
// by code that requires a project and will stop execution if the
// project is not found.
var options = {
"method": `GET`,
"headers": { "cache-control": `no-cache`, "authorization": `Basic ${token}` },
"url": `${account}/_apis/projects/${project}`,
"qs": { "api-version": PROJECT_API_VERSION }
};
request(options, function (err, res, body) {
if (err) {
callback(err, null);
return;
}
// Test for this before you try and parse the body.
// When a 203 is returned the body is HTML instead of
// JSON and will throw an exception if you try and parse.
// I only test this here because the project is required for
// all other items.
if (res.statusCode === 203) {
// You get this when the site tries to send you to the
// login page.
gen.log.error(`x Unable to authenticate with Team Services. Check account name and personal access token.`);
callback({ "message": `Unable to authenticate with Team Services. Check account name and personal access token.` });
return;
}
if (res.statusCode === 404) {
// Returning a undefined project indicates it was not found
callback({ "message": `x Project ${project} not found`, "code": `NotFound` }, undefined);
} else {
var obj = JSON.parse(body);
// Return the team project we just found.
callback(err, obj);
}
});
}
function findQueue(name, account, teamProject, token, callback) {
'use strict';
var options = {
"method": `GET`,
"headers": {
"cache-control": `no-cache`, "authorization": `Basic ${token}`
},
"url": `${account}/${teamProject.id}/_apis/distributedtask/queues`,
"qs": { "api-version": DISTRIBUTED_TASK_API_VERSION, "queueName": name }
};
request(options, function (err, res, body) {
var obj = JSON.parse(body);
if (res.statusCode >= 400) {
callback(new Error(res.statusMessage), null);
} else if (res.statusCode >= 300) {
// When it is a 300 the obj is a error
// object from the server
callback(obj);
} else {
// Setting to null is the all clear signal to the async
// series to continue
callback(null, obj.value[0].id);
}
});
}
function tryFindBuild(account, teamProject, token, target, callback) {
'use strict';
findBuild(account, teamProject, token, target, function (e, bld) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, bld);
}
});
}
function findBuild(account, teamProject, token, target, callback) {
'use strict';
var name = target === `docker` ? `${teamProject.name}-Docker-CI` : `${teamProject.name}-CI`;
var options = {
"method": `GET`,
"headers": {
"cache-control": `no-cache`, "authorization": `Basic ${token}`
},
"url": `${account}/${teamProject.id}/_apis/build/definitions`,
"qs": { "api-version": BUILD_API_VERSION }
};
request(options, function (e, response, body) {
var obj = JSON.parse(body);
var bld = obj.value.find(function (i) { return i.name === name; });
if (!bld) {
callback({ "message": `x Build ${name} not found`, "code": `NotFound` }, undefined);
} else {
callback(e, bld);
}
});
}
function tryFindRelease(args, callback) {
'use strict';
findRelease(args, function (e, rel) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, rel);
}
});
}
function findRelease(args, callback) {
"use strict";
var name = args.target === `docker` ? `${args.appName}-Docker-CD` : `${args.appName}-CD`;
var options = {
"method": `GET`,
"headers": {
"cache-control": `no-cache`, "authorization": `Basic ${args.token}`
},
"url": `${args.account}/${args.teamProject.name}/_apis/release/definitions`,
"qs": { "api-version": RELEASE_API_VERSION }
};
request(options, function (e, response, body) {
var obj = JSON.parse(body);
var rel = obj.value.find(function (i) { return i.name === name; });
if (!rel) {
callback({ "message": `x Release ${name} not found`, "code": `NotFound` }, undefined);
} else {
callback(e, rel);
}
});
}
function getPools(answers) {
"use strict";
var token = encodePat(answers.pat);
var options = {
"method": `GET`,
"headers": {
"cache-control": `no-cache`, "authorization": `Basic ${token}`
},
"url": `${answers.tfs}/_apis/distributedtask/pools`,
"qs": { "api-version": DISTRIBUTED_TASK_API_VERSION }
};
return new Promise(function (resolve, reject) {
request(options, function (e, response, body) {
if (e) {
reject(e);
return;
}
var obj = JSON.parse(body);
resolve(obj.value);
});
});
}
module.exports = {
// Exports the portions of the file we want to share with files that require
// it.
getPools: getPools,
tokenize: tokenize,
encodePat: encodePat,
findQueue: findQueue,
findBuild: findBuild,
checkStatus: checkStatus,
findProject: findProject,
findRelease: findRelease,
validateTFS: validateTFS,
tryFindBuild: tryFindBuild,
tryFindRelease: tryFindRelease,
reconcileValue: reconcileValue,
tryFindProject: tryFindProject,
validateGroupID: validateGroupID,
validateAzureSub: validateAzureSub,
validateDockerHost: validateDockerHost,
validateAzureSubID: validateAzureSubID,
validatePortMapping: validatePortMapping,
validateDockerHubID: validateDockerHubID,
getDefaultPortMapping: getDefaultPortMapping,
validateAzureTenantID: validateAzureTenantID,
validateDockerHubEmail: validateDockerHubEmail,
validateApplicationName: validateApplicationName,
findAzureServiceEndpoint: findAzureServiceEndpoint,
findDockerServiceEndpoint: findDockerServiceEndpoint,
validateDockerHubPassword: validateDockerHubPassword,
validateServicePrincipalID: validateServicePrincipalID,
validateServicePrincipalKey: validateServicePrincipalKey,
tryFindAzureServiceEndpoint: tryFindAzureServiceEndpoint,
validatePersonalAccessToken: validatePersonalAccessToken,
tryFindDockerServiceEndpoint: tryFindDockerServiceEndpoint,
validateDockerCertificatePath: validateDockerCertificatePath,
findDockerRegistryServiceEndpoint: findDockerRegistryServiceEndpoint,
tryFindDockerRegistryServiceEndpoint: tryFindDockerRegistryServiceEndpoint
};