clever-tools
Version:
Command Line Interface for Clever Cloud.
367 lines (307 loc) • 11.8 kB
JavaScript
import {
addDependency,
create as createApplication,
getAll as getAllApplications,
getAllDependencies,
get as getApplication,
redeploy as redeployApplication,
remove as removeApplication,
removeDependency,
update as updateApplication,
} from '@clevercloud/client/esm/api/v2/application.js';
import { getAvailableInstances } from '@clevercloud/client/esm/api/v2/product.js';
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
import cliparse from 'cliparse';
import _ from 'lodash';
import { confirmAnswer } from '../lib/prompts.js';
import { styleText } from '../lib/style-text.js';
import { Logger } from '../logger.js';
import * as AppConfiguration from './app_configuration.js';
import { resolveOwnerId } from './ids-resolver.js';
import * as Organisation from './organisation.js';
import { sendToApi } from './send-to-api.js';
import * as User from './user.js';
export function listAvailableTypes() {
return cliparse.autocomplete.words([
'docker',
'elixir',
'frankenphp',
'go',
'gradle',
'haskell',
'jar',
'linux',
'maven',
'meteor',
'node',
'php',
'play1',
'play2',
'python',
'ruby',
'rust',
'sbt',
'static',
'static-apache',
'v',
'war',
]);
}
export const AVAILABLE_ZONES = ['par', 'parhds', 'grahds', 'rbx', 'rbxhds', 'scw', 'ldn', 'mtl', 'sgp', 'syd', 'wsw'];
export function listAvailableZones() {
return cliparse.autocomplete.words(AVAILABLE_ZONES);
}
export function listAvailableAliases() {
return AppConfiguration.loadApplicationConf().then(({ apps }) => cliparse.autocomplete.words(_.map(apps, 'alias')));
}
export function listAvailableFlavors() {
return ['pico', 'nano', 'XS', 'S', 'M', 'L', 'XL', '2XL', '3XL'];
}
async function getId(ownerId, dependency) {
if (dependency.app_id) {
return dependency.app_id;
}
const app = await getByName(ownerId, dependency.app_name);
return app.id;
}
async function getInstanceType(type) {
// TODO: We should be able to use it without {}
const types = await getAvailableInstances({}).then(sendToApi);
const enabledTypes = types.filter((t) => t.enabled);
const matchingVariants = enabledTypes.filter((t) => t.variant != null && t.variant.slug === type);
const instanceVariant = _.sortBy(matchingVariants, 'version').reverse()[0];
if (instanceVariant == null) {
throw new Error(type + ' type does not exist.');
}
return instanceVariant;
}
export async function create(name, typeName, region, orgaIdOrName, github, isTask, envVars) {
Logger.debug('Create the application…');
const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
const instanceType = await getInstanceType(typeName);
const newApp = {
deploy: 'git',
description: name,
instanceType: instanceType.type,
instanceVersion: instanceType.version,
instanceVariant: instanceType.variant.id,
maxFlavor: instanceType.defaultFlavor.name,
maxInstances: 1,
minFlavor: instanceType.defaultFlavor.name,
minInstances: 1,
name: name,
zone: region,
instanceLifetime: isTask ? 'TASK' : 'REGULAR',
env: envVars,
};
if (github != null) {
newApp.oauthService = 'github';
newApp.oauthApp = github;
}
return createApplication({ id: ownerId }, newApp).then(sendToApi);
}
export async function deleteApp(app, skipConfirmation) {
Logger.debug('Deleting app: ' + app.name + ' (' + app.id + ')');
if (!skipConfirmation) {
await confirmAnswer(
`Deleting an application can't be undone, please type ${styleText('green', app.name)} to confirm:`,
'No confirmation, aborting application deletion',
app.name,
);
}
return removeApplication({ id: app.ownerId, appId: app.id }).then(sendToApi);
}
export async function getAllApps(ownerId) {
const summary = await getSummary().then(sendToApi);
const orgaWithApps = await Promise.all(
summary.organisations
// If owner ID is present, only keep the matching org
.filter((org) => ownerId == null || org.id === ownerId)
.sort((a, b) => a.name.localeCompare(b.name))
.map(async (org) => {
const applications = await getApplicationsForOwner(org.id);
return {
id: org.id,
name: org.name,
applications: applications.sort((a, b) => a.name.localeCompare(b.name)),
};
}),
);
return orgaWithApps;
}
async function getApplicationsForOwner(ownerId) {
const rawApplications = await getAllApplications({ id: ownerId }).then(sendToApi);
return rawApplications.map((app) => {
return {
app_id: app.id,
org_id: ownerId,
name: app.name,
zone: app.zone,
type: app.instance.variant.slug,
createdAt: new Date(app.creationDate).toISOString(),
deploy_url: app.deployment.httpUrl,
git_ssh_url: app.deployment.url,
};
});
}
function getApplicationByName(apps, name) {
const filteredApps = apps.filter((app) => app.name === name);
if (filteredApps.length === 1) {
return filteredApps[0];
} else if (filteredApps.length === 0) {
throw new Error('Application not found');
}
throw new Error('Ambiguous application name');
}
async function getByName(ownerId, name) {
const apps = await getAllApplications({ id: ownerId }).then(sendToApi);
return getApplicationByName(apps, name);
}
function addInstanceLifetime(app) {
// Patch to help config commands
app.instanceLifetime = app.instance.lifetime;
return app;
}
export function get(ownerId, appId) {
Logger.debug(`Get information for the app: ${appId}`);
return getApplication({ id: ownerId, appId }).then(sendToApi).then(addInstanceLifetime);
}
export function updateOptions(ownerId, appId, options) {
Logger.debug(`Update app: ${appId}`);
return updateApplication({ id: ownerId, appId }, options).then(sendToApi).then(addInstanceLifetime);
}
function getFromSelf(appId) {
Logger.debug(`Get information for the app: ${appId}`);
// /self differs from /organisations only for this one:
// it fallbacks to the organisations of which the user
// is a member, if it doesn't belong to Personal Space.
return getApplication({ appId }).then(sendToApi);
}
/**
* @param {{app_id: string}|{app_name: string}} appIdOrName
* @param {string} alias
* @return {Promise<{appId: string, ownerId: string}>}
*/
export async function resolveId(appIdOrName, alias) {
if (appIdOrName != null && alias != null) {
throw new Error('Only one of the `--app` or `--alias` options can be set at a time');
}
// -- resolve by linked app
if (appIdOrName == null) {
const appDetails = await AppConfiguration.getAppDetails({ alias });
return { appId: appDetails.appId, ownerId: appDetails.ownerId };
}
// -- resolve by app id
if (appIdOrName.app_id != null) {
const ownerId = await resolveOwnerId(appIdOrName.app_id);
if (ownerId != null) {
return {
appId: appIdOrName.app_id,
ownerId,
};
}
throw new Error('Application not found');
}
// -- resolve by app name
const summary = await getSummary({}).then(sendToApi);
const candidates = [summary.user, ...summary.organisations]
.flatMap((owner) => owner.applications.map((app) => ({ app, owner })))
.filter((candidate) => candidate.app.name === appIdOrName.app_name);
if (candidates.length === 0) {
throw new Error('Application not found');
}
if (candidates.length === 1) {
return {
appId: candidates[0].app.id,
ownerId: candidates[0].owner.id,
};
}
Logger.printErrorLine(`The name '${appIdOrName.app_name}' refers to multiple applications:`);
candidates.forEach((candidate) => {
Logger.printErrorLine(`- ${candidate.owner.name}: ${candidate.app.id} (${candidate.app.variantSlug})`);
});
throw new Error('Ambiguous application name, use the `--app` option with one of the IDs above');
}
export async function linkRepo(app, orgaIdOrName, alias, ignoreParentConfig) {
Logger.debug(`Linking current repository to the app: ${app.app_id || app.app_name}`);
const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
const appData = app.app_id != null ? await getFromSelf(app.app_id) : await getByName(ownerId, app.app_name);
return AppConfiguration.addLinkedApplication(appData, alias, ignoreParentConfig);
}
export function unlinkRepo(alias) {
Logger.debug(`Unlinking current repository from the app: ${alias}`);
return AppConfiguration.removeLinkedApplication({ alias });
}
export function redeploy(ownerId, appId, commit, withoutCache) {
Logger.debug(`Redeploying the app: ${appId}`);
const useCache = withoutCache ? 'no' : null;
return redeployApplication({ id: ownerId, appId, commit, useCache }).then(sendToApi);
}
export function mergeScalabilityParameters(scalabilityParameters, instance) {
const flavors = listAvailableFlavors();
if (scalabilityParameters.minFlavor) {
instance.minFlavor = scalabilityParameters.minFlavor;
if (flavors.indexOf(instance.minFlavor) > flavors.indexOf(instance.maxFlavor)) {
instance.maxFlavor = instance.minFlavor;
}
}
if (scalabilityParameters.maxFlavor) {
instance.maxFlavor = scalabilityParameters.maxFlavor;
if (
flavors.indexOf(instance.minFlavor) > flavors.indexOf(instance.maxFlavor) &&
scalabilityParameters.minFlavor == null
) {
instance.minFlavor = instance.maxFlavor;
}
}
if (scalabilityParameters.minInstances) {
instance.minInstances = scalabilityParameters.minInstances;
if (instance.minInstances > instance.maxInstances) {
instance.maxInstances = instance.minInstances;
}
}
if (scalabilityParameters.maxInstances) {
instance.maxInstances = scalabilityParameters.maxInstances;
if (instance.minInstances > instance.maxInstances && scalabilityParameters.minInstances == null) {
instance.minInstances = instance.maxInstances;
}
}
return instance;
}
export async function setScalability(appId, ownerId, scalabilityParameters, buildFlavor) {
Logger.info('Scaling the app: ' + appId);
const app = await getApplication({ id: ownerId, appId }).then(sendToApi);
const instance = _.cloneDeep(app.instance);
instance.minFlavor = instance.minFlavor.name;
instance.maxFlavor = instance.maxFlavor.name;
const newConfig = mergeScalabilityParameters(scalabilityParameters, instance);
if (buildFlavor != null) {
newConfig.separateBuild = buildFlavor !== 'disabled';
if (buildFlavor !== 'disabled') {
newConfig.buildFlavor = buildFlavor;
} else {
Logger.info('No build size given, disabling dedicated build instance');
}
}
return updateApplication({ id: ownerId, appId }, newConfig).then(sendToApi);
}
export async function listDependencies(ownerId, appId, showAll) {
const applicationDeps = await getAllDependencies({ id: ownerId, appId }).then(sendToApi);
if (!showAll) {
return applicationDeps.map((app) => ({ ...app, isLinked: true }));
}
const allApps = await getAllApplications({ id: ownerId }).then(sendToApi);
const applicationDepsIds = applicationDeps.map((app) => app.id);
return allApps.map((app) => {
const isLinked = applicationDepsIds.includes(app.id);
return { ...app, isLinked };
});
}
export async function link(ownerId, appId, dependency) {
const dependencyId = await getId(ownerId, dependency);
return addDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
}
export async function unlink(ownerId, appId, dependency) {
const dependencyId = await getId(ownerId, dependency);
return removeDependency({ id: ownerId, appId, dependencyId }).then(sendToApi);
}