Skip to content

Commit

Permalink
Merge pull request #828 from CleverCloud/domain-list
Browse files Browse the repository at this point in the history
feat(domain): introduce domain overview command
  • Loading branch information
hsablonniere authored Oct 23, 2024
2 parents f320571 + d408e00 commit 7107ee4
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 14 deletions.
27 changes: 17 additions & 10 deletions bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { getPackageJson } from '../src/load-package-json.cjs';
import * as git from '../src/models/git.js';
import * as Parsers from '../src/parsers.js';
import { handleCommandPromise } from '../src/command-promise-handler.js';
import * as Application from '../src/models/application.js';
import { AVAILABLE_ZONES } from '../src/models/application.js';
import { getOutputFormatOption, getSameCommitPolicyOption, getExitOnOption } from '../src/command-options.js';
import { getExitOnOption, getOutputFormatOption, getSameCommitPolicyOption } from '../src/command-options.js';

import * as Addon from '../src/models/addon.js';
import * as Application from '../src/models/application.js';
import * as ApplicationConfiguration from '../src/models/application_configuration.js';
import * as Drain from '../src/models/drain.js';
import * as Notification from '../src/models/notification.js';
Expand Down Expand Up @@ -130,10 +130,6 @@ function run () {
logsFormat: getOutputFormatOption(['json-stream']),
activityFormat: getOutputFormatOption(['json-stream']),
envFormat: getOutputFormatOption(['shell']),
accesslogsFollow: cliparse.flag('follow', {
aliases: ['f'],
description: 'Display access logs continuously (ignores before/until, after/since)',
}),
importAsJson: cliparse.flag('json', {
description: 'Import variables as JSON (an array of { "name": "THE_NAME", "value": "THE_VALUE" } objects)',
}),
Expand All @@ -156,11 +152,15 @@ function run () {
complete: Application.listAvailableAliases,
}),
domain: cliparse.option('filter', {
aliases: ['f'],
default: '',
metavar: 'TEXT',
description: 'Check only domains containing the provided text',
}),
domainOverviewFilter: cliparse.option('filter', {
default: '',
metavar: 'TEXT',
description: 'Get only domains containing the provided text',
}),
naturalName: cliparse.flag('natural-name', {
aliases: ['n'],
description: 'Show the application names or aliases if possible',
Expand Down Expand Up @@ -583,10 +583,12 @@ function run () {
const domainCreateCommand = cliparse.command('add', {
description: 'Add a domain name to an application',
args: [args.fqdn],
options: [opts.alias, opts.appIdOrName],
}, domain.add);
const domainRemoveCommand = cliparse.command('rm', {
description: 'Remove a domain name from an application',
args: [args.fqdn],
options: [opts.alias, opts.appIdOrName],
}, domain.rm);
const domainSetFavouriteCommand = cliparse.command('set', {
description: 'Set the favourite domain for an application',
Expand All @@ -597,16 +599,21 @@ function run () {
}, domain.unsetFavourite);
const domainFavouriteCommands = cliparse.command('favourite', {
description: 'Manage the favourite domain name for an application',
options: [opts.alias, opts.appIdOrName],
commands: [domainSetFavouriteCommand, domainUnsetFavouriteCommand],
}, domain.getFavourite);
const domainDiagApplicationCommand = cliparse.command('diag', {
description: 'Check if domains associated to a specific app are properly configured',
options: [opts.humanJsonOutputFormat, opts.domain],
options: [opts.alias, opts.appIdOrName, opts.humanJsonOutputFormat, opts.domain],
}, domain.diagApplication);
const domainOverviewCommand = cliparse.command('overview', {
description: 'Get an overview of all your domains (all orgas, all apps)',
options: [opts.humanJsonOutputFormat, opts.domainOverviewFilter],
}, domain.overview);
const domainCommands = cliparse.command('domain', {
description: 'Manage domain names for an application',
options: [opts.alias, opts.appIdOrName],
commands: [domainCreateCommand, domainFavouriteCommands, domainRemoveCommand, domainDiagApplicationCommand],
privateOptions: [opts.alias, opts.appIdOrName],
commands: [domainCreateCommand, domainFavouriteCommands, domainRemoveCommand, domainDiagApplicationCommand, domainOverviewCommand],
}, domain.list);

// DRAIN COMMANDS
Expand Down
14 changes: 12 additions & 2 deletions docs/applications-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,21 @@ By default, a Clever Cloud application gets `app_id.cleverapps.io` as fully qual
clever domain
```

To add/remove domains through these commands, use:
To get an overview of domains linked to any of your applications and organizations, use:

```
clever domain overview
clever domain overview --filter domain.tld
clever domain overview --filter .tld --format json
```

> [!TIP]
> The JSON output of this command plays nicely with tools such as [jless](https://jless.io/) to navigate through your domains.
To add/remove a domain to an application, use:

```
add Add a domain name to a Clever Cloud application
favourite Manage Clever Cloud application favourite domain name
rm Remove a domain name from a Clever Cloud application
```

Expand Down
138 changes: 136 additions & 2 deletions src/commands/domain.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import * as Application from '../models/application.js';
import { Logger } from '../logger.js';
import { get as getApp, addDomain, getFavouriteDomain as getFavouriteDomainWithError, markFavouriteDomain, unmarkFavouriteDomain, removeDomain } from '@clevercloud/client/esm/api/v2/application.js';
import {
addDomain,
get as getApp,
getAllDomains,
getFavouriteDomain as getFavouriteDomainWithError,
markFavouriteDomain,
removeDomain,
unmarkFavouriteDomain,
} from '@clevercloud/client/esm/api/v2/application.js';
import { getSummary } from '@clevercloud/client/cjs/api/v2/user.js';
import { sendToApi } from '../models/send-to-api.js';
import colors from 'colors/safe.js';
import { parse as parseDomain } from 'tldts';
import { diagDomainConfig } from '@clevercloud/client/esm/utils/diag-domain-config.js';
import { sortDomains } from '@clevercloud/client/esm/utils/domains.js';
import { DnsResolver } from '../models/node-dns-resolver.js';
import _ from 'lodash';

/**
* @typedef {import('@clevercloud/client/esm/utils/diag-domain-config.types.js').DomainInfo} DomainInfo
Expand Down Expand Up @@ -117,7 +127,11 @@ export async function diagApplication (params) {
cnameRecords: await dnsResolver.resolveCname(hostname) ?? [],
};

const diagnosticResults = diagDomainConfig({ hostname, pathPrefix, isApex }, resolvedDnsConfig, loadBalancerDnsConfig);
const diagnosticResults = diagDomainConfig({
hostname,
pathPrefix,
isApex,
}, resolvedDnsConfig, loadBalancerDnsConfig);
allDomainDiagnostics.push({ ...diagnosticResults, resolvedDnsConfig });

if (diagnosticResults.diagSummary === 'invalid' || diagnosticResults.diagSummary === 'no-config') {
Expand Down Expand Up @@ -147,6 +161,92 @@ export async function diagApplication (params) {
}
}

export async function overview (params) {

const { format, filter } = params.options;

const summary = await getSummary().then(sendToApi);
const consoleUrl = summary.user.partnerConsoleUrl;

const applications = [
...summary.user.applications.map((app) => {
return { ownerName: summary.user.name, ownerId: summary.user.id, ...app };
}),
...summary.organisations.flatMap((o) => {
return o.applications.map((app) => {
return { ownerName: o.name, ownerId: o.id, ...app };
});
}),
];

const applicationsWithDomains = await Promise.all(
applications.map(async (app) => {
const domains = await getAllDomains({ id: app.ownerId, appId: app.id }).then(sendToApi);
return { app, domains };
}),
);

const applicationsWithParsedDomain = applicationsWithDomains
.flatMap(({ app, domains }) => {
return domains
.filter((domain) => filter == null || domain.fqdn.includes(filter))
.map((domain) => {

const parsedDomain = parseDomain(domain.fqdn);
const pathname = new URL('https://' + domain.fqdn).pathname;
const subdomains = parsedDomain.subdomain !== '' ? parsedDomain.subdomain.split('.') : [];

// We're trying to create a propertyPath for lodash to create a tree structure object,
// the propertyPath for `aaa.bbb.ccc.example.com/the-path` would be:
// ["example.com", "example.com.ccc", "example.com.ccc.bbb", "example.com.ccc.bbb.aaa", "/path-aaa"]",

const sortSegments = [parsedDomain.domain, ...subdomains.reverse()];
const propertyPath = sortSegments.map((item, i, all) => {
return all.slice(0, i + 1).reverse().join('.');
});
propertyPath.push(pathname);

return {
ownerId: app.ownerId,
ownerName: app.ownerName,
appId: app.id,
appName: app.name,
appConsoleUrl: `${consoleUrl}/goto/${app.id}`,
appVariantSlug: app.variantSlug,
domain: domain.fqdn,
propetyPath: propertyPath,
};
});
});

const applicationsWithParsedDomainAsTree = {};
for (const { propetyPath, ...appWithDomain } of applicationsWithParsedDomain) {
_.set(applicationsWithParsedDomainAsTree, propetyPath, appWithDomain);
}

const applicationsWithParsedDomainAsSortedTree = recursiveSort(applicationsWithParsedDomainAsTree);

switch (format) {
case 'json':
Logger.printJson(applicationsWithParsedDomainAsSortedTree);
break;
case 'human':
default:
if (Object.keys(applicationsWithParsedDomainAsSortedTree).length === 0) {
if (filter?.length > 0) {
Logger.println(`No matches for filter "${filter}"`);
}
else {
Logger.println('No domains');
}
}
else {
recursiveDisplay(applicationsWithParsedDomainAsSortedTree);
}
break;
}
}

/** @param {DomainDiag & { resolvedDnsConfig: ResolveDnsResult }} domainDiag */
function reportDomainDiagnostics ({ hostname, pathPrefix, resolvedDnsConfig, diagDetails, diagSummary }) {

Expand Down Expand Up @@ -287,3 +387,37 @@ function getParsedDomains (vhosts) {
function printlnWithIndent (text, indentLevel) {
Logger.println(' '.repeat(indentLevel) + text);
}

function recursiveSort (obj) {

if (typeof obj === 'object' && obj.appId != null) {
return obj;
}

const sortedObj = {};
Object.keys(obj).sort((a, b) => a.localeCompare(b)).forEach((key) => {
sortedObj[key] = recursiveSort(obj[key]);
});

return sortedObj;
}

function recursiveDisplay (obj, indentLevel = 0) {

if (typeof obj === 'object' && obj.appId != null) {
printlnWithIndent(`${obj.ownerName} | ${obj.appName} (${obj.appVariantSlug})`, indentLevel);
printlnWithIndent(colors.blue(obj.appConsoleUrl), indentLevel);
return;
}

for (const [propertyPath, subObj] of Object.entries(obj)) {
if (propertyPath !== '/') {
Logger.println('');
printlnWithIndent(colors.yellow(propertyPath), indentLevel);
recursiveDisplay(subObj, indentLevel + 2);
}
else {
recursiveDisplay(subObj, indentLevel);
}
}
}

0 comments on commit 7107ee4

Please sign in to comment.