generator-team
Version:
Generates an app with CI/CD in Team Foundation Server or Team Services
1,501 lines (1,272 loc) • 46.4 kB
JavaScript
// Collection of utility functions used by other generators.
const fs = require('fs');
const os = require('os');
const url = require('url');
const cheerio = require('cheerio');
const request = require(`request`);
const package = require('../../package.json');
// The version of the API used to talk to TFS and VSTS. In future
// versions this might be changed depending on the version of TFS/VSTS
// we are talking too. However, the shape of the build and release
// JSON changes from version to version. The versions used below are
// supported in TFS 2017 U3, 2018 U1 and VSTS.
const BUILD_API_VERSION = `2.0`;
const PROJECT_API_VERSION = `1.0`;
// To support PowerShell multi-phased builds we had to use the newer API.
const VSTS_BUILD_API_VERSION = `4.0`;
const RELEASE_API_VERSION = `3.0-preview`;
const DISTRIBUTED_TASK_API_VERSION = `3.0-preview`;
const SERVICE_ENDPOINTS_API_VERSION = `3.0-preview`;
const PACKAGE_FEEDS_API_VERSION = `4.0-preview`;
// This location is the same as the VSTeam PowerShell module. Therefore,
// the profiles can be shared between Yo Team and the VSTeam PowerShell
// module.
const PROFILE_PATH = os.homedir() + '/vsteam_profiles.json';
const EXTENSIONS_SUB_DOMAIN = `extmgmt`;
const RELEASE_MANAGEMENT_SUB_DOMAIN = `vsrm`;
var profile = null;
var logMessage = function (msg) {
if (process.env.LOGYO === `on`) {
console.log(msg);
}
};
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
function isDocker(value) {
return value === `docker` || value === `dockerpaas` || value === `acilinux` || value === `k8s`;
}
function isWindowsAgent(queue) {
return (queue.indexOf(`Linux`) === -1 &&
queue.indexOf(`Ubuntu`) === -1 &&
queue.indexOf(`macOS`) === -1);
}
function getDockerRegistryServer(server) {
let parts = url.parse(server);
return parts.host;
}
// Qualify the image name with the dockerRegistryId for docker hub
// or the server name for other registries.
function getImageNamespace(registryId, url) {
// Default to the username for the registry. This is what we would
// use for DockerHub.
let dockerNamespace = registryId ? registryId.toLowerCase() : null;
// If the endpoint was provided see if this docker hub or not.
if (!isDockerHub(url)) {
dockerNamespace = getDockerRegistryServer(url);
}
return dockerNamespace;
}
function addUserAgent(options) {
options.headers['user-agent'] = getUserAgent();
return options;
}
// Setting the User Agent allows these calls to be identified in the VSTS telemetry.
function getUserAgent() {
return `Yo Team/${package.version} (${process.platform}: ${process.arch}) Node.js/${process.version}`;
}
function reconcileValue(first, second, fallback) {
// The only way I found to get the CLI to work when I need
// to skip values is to enter " ". That space throws off
// other portions of the code that test for the exisitence
// of a value. Triming the strings will only leave non
// whitespace.
return first ? first.trim() : (second ? second.trim() : fallback);
}
// If the user has selected Java and Hosted Linux Preview you
// can't select App Service because you have to use PowerShell
// to convert the war file into a zip file.
function getTargets(answers) {
return new Promise(function (resolve, reject) {
let targets = [];
if (answers.type === `custom`) {
targets = [{
name: `Azure`,
value: `paas`
}, {
name: `Docker`,
value: `docker`
}, {
name: `Both`,
value: `dockerpaas`
}];
} else if (answers.type === `aspFull`) {
targets = [{
name: `Azure App Service`,
value: `paas`
}, {
name: `Azure App Service (Deployment Slots)`,
value: `paasslots`
}];
} else {
targets = [{
name: `Azure App Service`,
value: `paas`
}, {
name: `Azure App Service (Deployment Slots)`,
value: `paasslots`
}, {
name: `Azure Container Instances (Linux)`,
value: `acilinux`
}, {
name: `Azure App Service Docker (Linux)`,
value: `dockerpaas`
}, {
name: `Docker Host`,
value: `docker`
}];
// Only supported on Azure DevOps
if (isVSTS(answers.tfs)) {
targets.push({
name: `Kubernetes`,
value: `k8s`
});
}
// TODO: Investigate if we need to remove these
// options. I think you can offer paas and paasslots
// for this combination.
if (answers.type === `java` &&
answers.queue === `Hosted Linux Preview`) {
// Remove Azure App Service
targets.splice(0, 1);
// Remove Azure App Service (Deployment Slots)
targets.splice(0, 1);
}
}
resolve(targets);
});
}
function getAppTypes(answers) {
// Default to languages that work on all agents
let types = [{
name: `.NET Core`,
value: `asp`
}, {
name: `Node.js`,
value: `node`
}, {
name: `Java`,
value: `java`
}
// , {
// name: `Custom`,
// value: `custom`
// }
];
// Only return PowerShell for VSTS. PowerShell relies
// on the hosted macOS, Linux and Windows build pools.
// Yes people could register macOS, Linux and Windows
// agents to TFS but at this time I don't have time
// to verify and test.
if (isVSTS(answers.tfs)) {
types.push({
name: `PowerShell module`,
value: `powershell`
});
}
// If this is not a Linux or Mac based agent also show
// .NET Full
if (isWindowsAgent(answers.queue)) {
types.splice(1, 0, {
name: `.NET Framework`,
value: `aspFull`
});
}
return types;
}
function getPATPrompt(answers) {
if (!answers.tfs) {
// The user passed in the tfs value on the
// command line but we still need to prompt
// for the pat. answers will not have a tfs
// value so send a generic response.
return `What is your Personal Access Token?`;
}
if (isVSTS(answers.tfs)) {
return `What is your VSTS Personal Access Token?`;
}
return `What is your TFS Personal Access Token?`;
}
function getInstancePrompt() {
return `Enter Service account name\n (dev.azure.com/{account} or \n {account}.visualstudio.com)\n Or full Server URL including collection\n (http://tfs:8080/tfs/DefaultCollection)\n Or name of a stored Profile?`;
}
function getDefaultPortMapping(answers) {
if (answers.target === `docker`) {
if (answers.type === `java`) {
return `8080:8080`;
} else if (answers.type === `node`) {
return `3000:3000`;
} else {
return `80:80`;
}
} else {
if (answers.type === `java`) {
return `8080`;
} else if (answers.type === `node`) {
return `3000`;
} else {
return `80`;
}
}
}
function validateRequired(input, msg) {
return !input ? msg : true;
}
function validatePortMapping(input) {
return validateRequired(input, `You must provide a Port Mapping`);
}
function validateClusterName(input) {
return validateRequired(input, `You must provide a Cluster name`);
}
function validateClusterResourceGroup(input) {
return validateRequired(input, `You must provide a Cluster resource group name`);
}
function validateGroupID(input) {
return validateRequired(input, `You must provide a Group ID`);
}
function validateProfileName(input) {
return validateRequired(input, `You must provide a profile name`);
}
function validateCustomFolder(input) {
return validateRequired(input, `You must provide a custom template path`);
}
function validateApplicationName(input) {
return validateRequired(input, `You must provide a name for your application`);
}
function validateFunctionName(input) {
return validateRequired(input, `You must provide a name for your function`);
}
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 or Team Service account name`);
}
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 Registry username`);
}
function validateDockerHubPassword(input) {
return validateRequired(input, `You must provide a Docker Registry Password`);
}
function validateDockerRegistry(input) {
if (!input) {
return validateRequired(input, `You must provide a Docker Registry URL`);
}
return input.toLowerCase().match(/http/) === null ? `You must provide a Docker Registry URL including http(s)` : true;
}
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 validateApiKey(input) {
return validateRequired(input, `You must provide a apiKey`);
}
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
let b = Buffer.from(`:` + pat);
return b.toString(`base64`);
}
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 = addUserAgent({
"method": `GET`,
"headers": {
"authorization": `Basic ${token}`
},
"url": `${uri}`
});
request(options, function (err, res, body) {
let obj = {};
try {
obj = JSON.parse(body);
} catch (error) {
// This a HTML page with an error message.
err = error;
console.log(body);
}
callback(err, obj);
});
}
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 = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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.toLowerCase() === `dockerregistry`;
});
if (endpoint === undefined) {
callback({
"message": `Could not find Docker Registry Service Endpoint`,
"code": `NotFound`
}, undefined);
} else {
// Down stream we need the full endpoint so call again with the ID. This will return more data
getServiceEndpoint(account, projectId, endpoint.id, token, callback);
}
});
}
function getServiceEndpoint(account, projectId, id, token, callback) {
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(account)}/${projectId}/_apis/distributedtask/serviceendpoints/${id}`,
"qs": {
"api-version": SERVICE_ENDPOINTS_API_VERSION
}
});
request(options, function (error, response, body) {
callback(error, JSON.parse(body));
});
}
function tryFindPackageFeed(account, projectName, token, gen, callback) {
'use strict';
// Will NOT throw an error if the feed is not found. This is used
// by code that will create the feed if it is not found.
findPackageFeed(account, projectName, token, gen, function (e, ep) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, ep);
}
});
}
function findPackageFeed(account, projectName, token, gen, callback) {
'use strict';
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(account, false, 'feeds')}/_apis/packaging/feeds`,
"qs": {
"api-version": PACKAGE_FEEDS_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 package feed: ${response.statusMessage}`);
return;
}
var obj = JSON.parse(body);
var endpoint = obj.value.find(function (i) {
return i.name === projectName;
});
if (endpoint === undefined) {
callback({
"message": `Could not find package feed`,
"code": `NotFound`
}, undefined);
} else {
callback(error, endpoint);
}
});
}
function tryFindNuGetServiceEndpoint(account, projectId, 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.
findNuGetServiceEndpoint(account, projectId, token, gen, function (e, ep) {
if (e && e.code === `NotFound`) {
callback(null, undefined);
} else {
callback(e, ep);
}
});
}
function findNuGetServiceEndpoint(account, projectId, token, gen, callback) {
'use strict';
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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 NuGet 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
var endpoint = obj.value.find(function (i) {
return i.url.toLowerCase().startsWith('https://www.powershellgallery.com/api/v2/package');
});
if (endpoint === undefined) {
callback({
"message": `Could not find NuGet 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
// This will be true when the user selected Linux for the build queue.
// Because we use ARM deployment with the Hosted VS2017 queue even if
// the user selected Linux for build the call to needsDockerHost may
// return true for build. However, if they select Linux for build they
// would not be prompted for a docker host and dockerHost will be empty.
// In that case just return. When user is calling from the command line
// and they pass in Linux they have to leave dockerHost empty. If they
// pass in Linux and a dockerHost they will get an error saying the
// docker service endpoint could not be found. That is because we did
// not create one because the Hosted Linux build has the docker tools
// so we don't need an external host so no service endpoint was created.
if (!dockerHost) {
callback(null, null);
return;
}
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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.toLowerCase().startsWith(dockerHost.toLowerCase());
});
if (endpoint === undefined) {
callback({
"message": `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 = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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 !== undefined && i.data.subscriptionName.toLowerCase() === sub.name.toLowerCase();
});
if (endpoint === undefined) {
callback({
"message": `Could not find Azure Service Endpoint`,
"code": `NotFound`
}, undefined);
} else {
// Down stream we need the full endpoint so call again with the ID. This will return more data
getServiceEndpoint(account, projectId, endpoint.id, token, callback);
}
});
}
function findAzureSub(account, subName, token, gen, callback) {
"use strict";
var options = addUserAgent({
method: 'GET',
headers: {
'cache-control': 'no-cache',
'content-type': 'application/json',
'authorization': `Basic ${token}`
},
url: `${getFullURL(account)}/_apis/distributedtask/serviceendpointproxy/azurermsubscriptions`
});
request(options, function (error, response, body) {
var obj = JSON.parse(body);
var sub = obj.value.find(function (i) {
return i.displayName.toLowerCase() === subName.toLowerCase();
});
callback(error, sub);
});
}
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 = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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(`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": `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 = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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 findAllQueues(account, teamProject, token, callback) {
'use strict';
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(account)}/${teamProject.id}/_apis/distributedtask/queues`,
"qs": {
"api-version": DISTRIBUTED_TASK_API_VERSION
}
});
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);
}
});
}
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 = isDocker(target) ? `${teamProject.name}-Docker-CI` : `${teamProject.name}-CI`;
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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.toLowerCase() === name.toLowerCase();
});
if (!bld) {
callback({
"message": `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 = isDocker(args.target) ? `${args.appName}-Docker-CD` : `${args.appName}-CD`;
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${args.token}`
},
"url": `${getFullURL(args.account, true, RELEASE_MANAGEMENT_SUB_DOMAIN)}/${args.teamProject.name}/_apis/release/definitions`,
"qs": {
"api-version": RELEASE_API_VERSION
}
});
request(options, function (e, response, body) {
if (response.statusCode !== 200) {
// The body is HTML
var dom = cheerio.load(body);
callback({
"message": `Server Error: ${dom(`title`).text()}`,
"code": `ServerError`
}, undefined);
return;
}
var obj = JSON.parse(body);
var rel = obj.value.find(function (i) {
return i.name.toLowerCase() === name.toLowerCase();
});
if (!rel) {
callback({
"message": `Release ${name} not found`,
"code": `NotFound`
}, undefined);
} else {
callback(e, rel);
}
});
}
function getAzureSubs(answers) {
"use strict";
var token = encodePat(answers.pat);
var options = addUserAgent({
method: 'GET',
headers: {
'cache-control': 'no-cache',
'content-type': 'application/json',
'authorization': `Basic ${token}`
},
url: `${getFullURL(answers.tfs)}/_apis/distributedtask/serviceendpointproxy/azurermsubscriptions`
});
return new Promise(function (resolve, reject) {
request(options, function (e, response, body) {
if (e) {
reject(e);
return;
}
var obj = JSON.parse(body);
var result = [];
obj.value.forEach((sub) => {
result.push({
name: sub.displayName
});
});
resolve(result);
});
});
}
function getProfileCommands(answers) {
"use strict";
return [{
name: `Add`,
value: `add`
}, {
name: `List`,
value: `list`
}, {
name: `Delete`,
value: `delete`
}];
}
function getTFSVersion(answers) {
"use strict";
return [{
name: `TFS2017`,
value: `TFS2017`
}, {
name: `TFS2018`,
value: `TFS2018`
}];
}
function getPools(answers) {
"use strict";
var token = encodePat(answers.pat);
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(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;
}
if (response.statusCode === 401) {
reject({
"message": `Check your personal access token: ${response.statusMessage}`,
"code": `Unauthorized`
});
return;
}
var obj = JSON.parse(body);
resolve(obj.value);
});
});
}
function isDockerHub(dockerRegistry) {
return dockerRegistry ? dockerRegistry.toLowerCase().match(/index.docker.io/) !== null : true;
}
function loadProfiles() {
let results = {
error: null,
profiles: null
};
if (fs.existsSync(PROFILE_PATH)) {
try {
results.profiles = JSON.parse(fs.readFileSync(PROFILE_PATH, 'utf8'));
} catch (error) {
// The file is invalid
results.error = `Invalid file.`;
}
} else {
results.error = `No profiles file.`;
}
return results;
}
// Reads profiles created by the VSTeam PowerShell module.
// Search ignores case.
// Yo team stores the full URL of TFS and the account name
// for VSTS. The profile name might not match the account
// name or full URL. So when you run Yo Team again it will
// default to the full URL or account name and fail to find
// the profile and prompt for the PAT. Therefore, here you
// need to search the profile name and URL
function searchProfiles(input) {
let results = loadProfiles();
if (results.profiles !== null) {
var found = results.profiles.filter(function (i) {
return (i.Name.toLowerCase().includes(input.toLowerCase()) ||
i.URL.toLowerCase().includes(input.toLowerCase())) &&
i.Type === `Pat`;
});
if (found.length > 1) {
// Try to find the correct entry
found = results.profiles.filter(function (i) {
return (i.Name.toLowerCase() === input.toLowerCase() ||
i.URL.toLowerCase() === input.toLowerCase());
});
}
if (found.length === 1) {
return found[0];
}
}
return null;
}
// Used to determine if we need to prompt the user for a PAT
// If the PAT was read from the profile this method returns
// false letting the prompt know it does not need to ask for
// a PAT.
function readPatFromProfile(answers, obj) {
if (profile) {
// Profiles are stored 64 bit encoded
let b = Buffer.from(profile.Pat, 'base64');
// Skip the leading :
obj.options.pat = b.toString().substring(1);
}
// If the value was passed on the command line it will
// not be set in answers which other prompts expect.
// So, place it in answers now.
answers.pat = obj.options.pat;
return obj.options.pat === undefined;
}
function extractInstance(input) {
profile = searchProfiles(input);
if (profile !== null) {
input = profile.URL;
}
// When using VSTS we only want the account name but
// people continue to give the entire url which will
// cause issues later. So check to see if the value
// provided contains visualstudio.com or dev.azure.com
// and if so extract the account name and simply return that.
// If you find visualstudio.com or dev.azure.com in the name
// the user most likely entered the entire URL instead of just
// the account name so lets extract it.
if (input.match(/visualstudio.com/i) === null &&
input.match(/dev.azure.com/i) === null) {
return input;
}
var myRegexp = /([^/]+)\.visualstudio.com|dev.azure.com\/([^/]+)/i;
var match = myRegexp.exec(input);
// The match for VSTS is in 1 and the match for Azure DevOps is in
// 2. If we did not match VSTS we must of matched Azure DevOps.
return match[1] ? match[1] : match[2]
}
function needsRegistry(answers, options) {
if (options !== undefined) {
return (answers.target === `docker` ||
options.target === `docker` ||
answers.target === `acilinux` ||
options.target === `acilinux` ||
answers.target === `dockerpaas` ||
options.target === `dockerpaas` ||
answers.target === `k8s` ||
options.target === `k8s`);
} else {
return (answers.target === `docker` ||
answers.target === `acilinux` ||
answers.target === `dockerpaas` ||
answers.target === `k8s`);
}
}
function needsDockerHost(answers, options) {
let isDocker;
let paasRequiresHost;
if (options !== undefined) {
// If you pass in the target on the command line
// answers.target will be undefined so test options
isDocker = (answers.target === `docker` || options.target === `docker`);
// This will be true if the user did not select a Hosted Linux queue
paasRequiresHost = (answers.target === `dockerpaas` ||
options.target === `dockerpaas` ||
answers.target === `acilinux` ||
options.target === `acilinux` ||
answers.target === `k8s` ||
options.target === `k8s`) &&
((hasDockerTools(answers.queue) === false) &&
(hasDockerTools(options.queue) === false));
} else {
// If you pass in the target on the command line
// answers.target will be undefined so test options
isDocker = answers.target === `docker`;
// This will be true the user did not select the Hosted Linux queue
paasRequiresHost = (answers.target === `dockerpaas` || answers.target === `acilinux`) && hasDockerTools(answers.queue) === false;
}
logMessage(`needsDockerHost returning = ${isDocker || paasRequiresHost}`);
return (isDocker || paasRequiresHost);
}
function hasDockerTools(agent) {
if (agent == undefined) {
return false;
}
return agent.indexOf(`Linux`) !== -1 || agent.indexOf(`Ubuntu`) !== -1;
}
function needsApiKey(answers, options) {
if (options !== undefined) {
return (answers.type === `powershell` ||
options.type === `powershell`);
} else {
return (answers.type === `powershell`);
}
}
function isPaaS(answers, cmdLnInput) {
if (cmdLnInput !== undefined) {
return (answers.target === `paas` ||
cmdLnInput.options.target === `paas` ||
answers.target === `paasslots` ||
cmdLnInput.options.target === `paasslots` ||
answers.target === `acilinux` ||
cmdLnInput.options.target === `acilinux` ||
answers.target === `dockerpaas` ||
cmdLnInput.options.target === `dockerpaas` ||
answers.target === `k8s` ||
cmdLnInput.options.target === `k8s`);
} else {
return (answers.target === `paas` ||
answers.target === `paasslots` ||
answers.target === `acilinux` ||
answers.target === `dockerpaas` ||
answers.target === `k8s`);
}
}
function isVSTS(instance) {
if (instance) {
return instance.toLowerCase().match(/http/) === null || instance.toLowerCase().match(/visualstudio\.com/) !== null;
}
return false;
}
function supportsLoadTests(account, token, callback) {
if (isVSTS(account)) {
let pat = encodePat(token);
// We can determine if this is 2017 or not by searching for a
// specific docker task.
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${pat}`
},
"url": `${getFullURL(account)}/_api/_account/GetAccountSettings?__v=5`
});
request(options, function (error, response, body) {
if (error) {
callback(error, undefined);
} else if (response.statusCode >= 200 && response.statusCode < 300) {
let obj = {};
let result = true;
try {
obj = JSON.parse(body);
if (obj.accountRegion === `West Central US`) {
result = false;
}
callback(undefined, result);
} catch (error) {
// This a HTML page with an error message.
err = error;
console.log(body);
callback(err, undefined);
}
} else {
callback(undefined, undefined);
}
});
} else {
callback(undefined, false);
}
}
//
// Returns true if the extension is installed.
// Despite the resource name of installedextensionsbyname you actually have to pass in the ID.
// https://{accountName}.extmgmt.visualstudio.com/_apis/extensionmanagement/installedextensionsbyname/{publisherId}/{extensionId}
//
function isExtensionInstalled(account, token, publisherId, extensionId, callback) {
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(account)}/_apis/extensionmanagement/installedextensionsbyname/${publisherId}/${extensionId}`
});
request(options, function (error, response, body) {
if (error) {
callback(undefined, false);
} else {
callback(undefined, true);
}
});
}
//
// token must be encoded before calling this function.
//
function isTFSGreaterThan2017(account, token, callback) {
if (isVSTS(account)) {
callback(undefined, true);
} else {
// We can determine if this is 2017 or not by reviewing
// the values from teh serviceLevel
var options = addUserAgent({
"method": `GET`,
"headers": {
"cache-control": `no-cache`,
"authorization": `Basic ${token}`
},
"url": `${getFullURL(account)}/_apis/servicelevel`
});
request(options, function (error, response, body) {
if (error) {
callback(error, undefined);
} else if (response.statusCode === 200) {
var obj = JSON.parse(body);
if (obj.configurationDatabaseServiceLevel.startsWith(`Dev15`)) {
callback(undefined, false);
} else {
callback(undefined, true);
}
} else {
callback(undefined, true);
}
});
}
}
function getFullURL(instance, includeCollection, subDomain) {
// The user MUST only enter the VSTS account name and not the full url.
// This is how the system determines which system is being targeted. Some
// URL for VSTS are not the same as they are for TFS for example Release
// Management.
// When used with VSTS all the user provides is the account name. But When
// it is used with TFS they give the full url including the collection. This
// functions makes sure any code needing the full URL gets that they need to
// continue without worrying if it is TFS or VSTS.
if (instance.toLowerCase().match(/http/) !== null) {
return instance;
}
var vstsURL = `https://${instance}.visualstudio.com`;
if (subDomain) {
vstsURL = `https://${instance}.${subDomain}.visualstudio.com`;
}
if (includeCollection) {
vstsURL += `/DefaultCollection`;
}
return vstsURL;
}
function isKubernetes(target) {
return target === 'k8s';
}
module.exports = {
// Exports the portions of the file we want to share with files that require
// it.
PROFILE_PATH: PROFILE_PATH,
BUILD_API_VERSION: BUILD_API_VERSION,
PROJECT_API_VERSION: PROJECT_API_VERSION,
RELEASE_API_VERSION: RELEASE_API_VERSION,
EXTENSIONS_SUB_DOMAIN: EXTENSIONS_SUB_DOMAIN,
VSTS_BUILD_API_VERSION: VSTS_BUILD_API_VERSION,
PACKAGE_FEEDS_API_VERSION: PACKAGE_FEEDS_API_VERSION,
DISTRIBUTED_TASK_API_VERSION: DISTRIBUTED_TASK_API_VERSION,
SERVICE_ENDPOINTS_API_VERSION: SERVICE_ENDPOINTS_API_VERSION,
RELEASE_MANAGEMENT_SUB_DOMAIN: RELEASE_MANAGEMENT_SUB_DOMAIN,
isVSTS: isVSTS,
isPaaS: isPaaS,
getPools: getPools,
tokenize: tokenize,
isDocker: isDocker,
encodePat: encodePat,
findQueue: findQueue,
findBuild: findBuild,
getFullURL: getFullURL,
logMessage: logMessage,
getTargets: getTargets,
getAppTypes: getAppTypes,
needsApiKey: needsApiKey,
checkStatus: checkStatus,
findProject: findProject,
findRelease: findRelease,
validateTFS: validateTFS,
isDockerHub: isDockerHub,
getAzureSubs: getAzureSubs,
findAzureSub: findAzureSub,
loadProfiles: loadProfiles,
getPATPrompt: getPATPrompt,
tryFindBuild: tryFindBuild,
addUserAgent: addUserAgent,
isKubernetes: isKubernetes,
getUserAgent: getUserAgent,
needsRegistry: needsRegistry,
findAllQueues: findAllQueues,
getTFSVersion: getTFSVersion,
tryFindRelease: tryFindRelease,
reconcileValue: reconcileValue,
searchProfiles: searchProfiles,
tryFindProject: tryFindProject,
isWindowsAgent: isWindowsAgent,
validateApiKey: validateApiKey,
validateGroupID: validateGroupID,
extractInstance: extractInstance,
findPackageFeed: findPackageFeed,
needsDockerHost: needsDockerHost,
validateAzureSub: validateAzureSub,
getInstancePrompt: getInstancePrompt,
getImageNamespace: getImageNamespace,
supportsLoadTests: supportsLoadTests,
getProfileCommands: getProfileCommands,
readPatFromProfile: readPatFromProfile,
validateDockerHost: validateDockerHost,
validateAzureSubID: validateAzureSubID,
tryFindPackageFeed: tryFindPackageFeed,
validatePortMapping: validatePortMapping,
validateProfileName: validateProfileName,
validateClusterName: validateClusterName,
validateDockerHubID: validateDockerHubID,
isExtensionInstalled: isExtensionInstalled,
validateFunctionName: validateFunctionName,
isTFSGreaterThan2017: isTFSGreaterThan2017,
validateCustomFolder: validateCustomFolder,
getDefaultPortMapping: getDefaultPortMapping,
validateAzureTenantID: validateAzureTenantID,
validateDockerRegistry: validateDockerRegistry,
getDockerRegistryServer: getDockerRegistryServer,
validateApplicationName: validateApplicationName,
findNuGetServiceEndpoint: findNuGetServiceEndpoint,
findAzureServiceEndpoint: findAzureServiceEndpoint,
findDockerServiceEndpoint: findDockerServiceEndpoint,
validateDockerHubPassword: validateDockerHubPassword,
validateServicePrincipalID: validateServicePrincipalID,
validateServicePrincipalKey: validateServicePrincipalKey,
tryFindAzureServiceEndpoint: tryFindAzureServiceEndpoint,
validatePersonalAccessToken: validatePersonalAccessToken,
tryFindNuGetServiceEndpoint: tryFindNuGetServiceEndpoint,
tryFindDockerServiceEndpoint: tryFindDockerServiceEndpoint,
validateClusterResourceGroup: validateClusterResourceGroup,
validateDockerCertificatePath: validateDockerCertificatePath,
findDockerRegistryServiceEndpoint: findDockerRegistryServiceEndpoint,
tryFindDockerRegistryServiceEndpoint: tryFindDockerRegistryServiceEndpoint
};