Skip to content

Commit

Permalink
feat: implement tenant list command using foundation config
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannesRudolph committed May 19, 2022
1 parent be9b82b commit c83556b
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 45 deletions.
40 changes: 25 additions & 15 deletions src/api/CliApiFacadeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { Logger } from "../cli/Logger.ts";
import { CmdGlobalOptions } from "../commands/cmd-options.ts";
import { isatty, TTY } from "../commands/tty.ts";
import { MeshError } from "../errors.ts";
import { AwsCliEnv, AzCliEnv, GcloudCliEnv } from "../model/CliToolEnv.ts";
import { isWindows } from "../os.ts";
import { DefaultEnvShellRunner } from "../process/DefaultEnvShellRunner.ts";
import { LoaderShellRunner } from "../process/loader-shell-runner.ts";
import { VerboseShellRunner } from "../process/verbose-shell-runner.ts";
import { AwsCliFacade } from "./aws/aws-cli-facade.ts";
import { AwsShellRunner } from "./aws/aws-shell-runner.ts";
import { AutoInstallAzureCliModuleDecorator } from "./az/auto-install-azure-cli-module-decorator.ts";
import { AzureCliFacade } from "./az/azure-cli-facade.ts";
import { BasicAzureCliFacade } from "./az/basic-azure-cli-facade.ts";
Expand All @@ -21,22 +22,22 @@ export class CliApiFacadeFactory {

constructor(
private readonly logger: Logger,
private readonly options: CmdGlobalOptions
private readonly options: CmdGlobalOptions,
) {}

async buildAws() {
const shellRunner = this.buildShellRunner(this.options);
const awsShellRunner = new AwsShellRunner(shellRunner, "default");
async buildAws(env?: AwsCliEnv) {
const shellRunner = this.buildShellRunner(this.options, env);
// const awsShellRunner = new AwsShellRunner(shellRunner, "default");

const facade = new AwsCliFacade(awsShellRunner);
const facade = new AwsCliFacade(shellRunner);

await this.verifyInstallationStatus(facade);

return facade;
}

async buildGcloud() {
const shellRunner = this.buildShellRunner(this.options);
async buildGcloud(env?: GcloudCliEnv) {
const shellRunner = this.buildShellRunner(this.options, env);

const facade = new GcpCliFacade(shellRunner);

Expand All @@ -45,8 +46,8 @@ export class CliApiFacadeFactory {
return facade;
}

async buildAz() {
const shellRunner = this.buildShellRunner(this.options);
async buildAz(env?: AzCliEnv) {
const shellRunner = this.buildShellRunner(this.options, env);

let azure: AzureCliFacade = new BasicAzureCliFacade(shellRunner);

Expand All @@ -64,12 +65,13 @@ export class CliApiFacadeFactory {

private async verifyInstallationStatus(facade: CliFacade) {
// maintain a cache of installation status so we don't run any checks again unnecessarily
const key = Object.getPrototypeOf(facade).name;
const key = facade.constructor.name;
const cachedStatus = this.installationStatusCache.get(key);

this.logger.debug(
() =>
`requested ${key} has a cached installation status of ${cachedStatus?.status}`
`requested ${key} has a cached installation status of ${cachedStatus
?.status}`,
);

const status = cachedStatus || (await facade.verifyCliInstalled());
Expand All @@ -84,25 +86,33 @@ export class CliApiFacadeFactory {
switch (status.status) {
case PlatformCommandInstallationStatus.NotInstalled:
throw new MeshError(
`"${cmd}" cli is not installed. Please review https://github.com/meshcloud/collie-cli/#prerequisites for installation instructions".`
`"${cmd}" cli is not installed. Please review https://github.com/meshcloud/collie-cli/#prerequisites for installation instructions".`,
);
case PlatformCommandInstallationStatus.UnsupportedVersion:
throw new MeshError(
`"${cmd}" cli is not installed in a supported version. Please review https://github.com/meshcloud/collie-cli/#prerequisites for installation instructions".`
`"${cmd}" cli is not installed in a supported version. Please review https://github.com/meshcloud/collie-cli/#prerequisites for installation instructions".`,
);
case PlatformCommandInstallationStatus.Installed:
break;
}
}

private buildShellRunner(options: CmdGlobalOptions) {
private buildShellRunner(
options: CmdGlobalOptions,
env?: Record<string, string>,
) {
let shellRunner = new ShellRunner();

if (options.verbose) {
shellRunner = new VerboseShellRunner(shellRunner);
} else if (isatty && !isWindows) {
shellRunner = new LoaderShellRunner(shellRunner, new TTY());
}

if (env) {
shellRunner = new DefaultEnvShellRunner(shellRunner, env);
}

return shellRunner;
}
}
21 changes: 12 additions & 9 deletions src/commands/foundation/new.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export function registerNewCmd(program: Command) {

const foundationPath = repo.resolvePath("foundations", foundation);

// todo: this is stupidly hardcoded for now, would need some more dynamic detection of platforms and also
// possibly prompt the user for "do you want to add A? cool, missing a param there, what's your X?" etc.
const factory = new CliApiFacadeFactory(logger, opts);

const platformEntries = await promptPlatformEntries(logger, factory);
Expand All @@ -55,7 +53,7 @@ export function registerNewCmd(program: Command) {
await dir.write(d, "");

logger.progress(
"generated new foundation " + repo.relativePath(foundationPath)
"generated new foundation " + repo.relativePath(foundationPath),
);
});
}
Expand All @@ -74,11 +72,11 @@ Welcome to your cloud foundation.
async function detectPlatform(
logger: Logger,
platform: string,
builder: () => Promise<Dir>
builder: () => Promise<Dir>,
): Promise<Dir | undefined> {
try {
console.log(
`searching for a valid ${platform} platform in your current environment...`
`searching for a valid ${platform} platform in your current environment...`,
);

const dir = await builder();
Expand All @@ -97,8 +95,11 @@ async function detectPlatform(

async function promptPlatformEntries(
logger: Logger,
factory: CliApiFacadeFactory
factory: CliApiFacadeFactory,
) {
// todo: this is stupidly hardcoded for now, would need some more dynamic detection of platforms and also
// possibly prompt the user for "do you want to add A? cool, missing a param there, what's your X?" etc.

const entries: Dir[] = [
await detectPlatform(logger, "Azure", () => setupAzurePlatform(factory)),
await detectPlatform(logger, "AWS", () => setupAwsPlatform(factory)),
Expand Down Expand Up @@ -126,7 +127,7 @@ function generateAwsReadmeMd(identity: CallerIdentity): string {
name: "aws",
aws: {
accountId: identity.Account,
accountAccessRole: "OrganizationAccountAccessRole" // todo: be more smart about this default
accountAccessRole: "OrganizationAccountAccessRole", // todo: be more smart about this default
},
cli: {
aws: {
Expand All @@ -153,7 +154,7 @@ async function setupGcpPlatform(factory: CliApiFacadeFactory): Promise<Dir> {

if (!project) {
throw new MeshError(
"'gcloud config list' does not have a configured project"
"'gcloud config list' does not have a configured project",
);
}

Expand Down Expand Up @@ -198,6 +199,8 @@ async function setupAzurePlatform(factory: CliApiFacadeFactory): Promise<Dir> {
}

function generateAzureReadmeMd(account: Account): string {
const configDir = Deno.env.get("AZURE_CONFIG_DIR");

const frontmatter: PlatformConfigAzure = {
name: "azure",
azure: {
Expand All @@ -206,7 +209,7 @@ function generateAzureReadmeMd(account: Account): string {
},
cli: {
az: {
AZURE_CONFIG_DIR: Deno.env.get("AZURE_CONFIG_DIR") || "",
...(configDir && { AZURE_CONFIG_DIR: configDir }),
},
},
};
Expand Down
36 changes: 25 additions & 11 deletions src/commands/tenant/list.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,56 @@ import { CmdGlobalOptions, OutputFormatType } from "../cmd-options.ts";
import { isatty } from "../tty.ts";
import { TenantListPresenterFactory } from "../../presentation/tenant-list-presenter-factory.ts";
import { CollieRepository } from "/model/CollieRepository.ts";
import { FoundationRepository } from "../../model/FoundationRepository.ts";
import { MeshFoundationAdapterFactory } from "../../mesh/MeshFoundationAdapterFactory.ts";
import { Logger } from "../../cli/Logger.ts";
import { CliApiFacadeFactory } from "../../api/CliApiFacadeFactory.ts";

export function registerListCommand(program: Command) {
const listTenants = new Command()
program
.command("list <foundation>")
// type must be added on every level that uses this type. Maybe bug in Cliffy?
.type("output", OutputFormatType)
.description(
"Returns a list of tenants with their name, id, tags and platform.",
)
.action(listTenantAction);

program.command("list", listTenants);
}

export async function listTenantAction(options: CmdGlobalOptions) {
const repo = await CollieRepository.load("./");
export async function listTenantAction(options: CmdGlobalOptions, foundation: string) {
const collieRepo = await CollieRepository.load("./");

const logger = new Logger(collieRepo, options);
await setupLogger(options);
await verifyCliAvailability();

const config = loadConfig();
const meshAdapterFactory = new MeshAdapterFactory(config);
// todo: unify logging infra

const foundationRepo = await FoundationRepository.load(
collieRepo,
foundation,
);

const facadeFactory = new CliApiFacadeFactory(logger, options);
const meshAdapterFactory = new MeshFoundationAdapterFactory(
collieRepo,
foundationRepo,
facadeFactory,
);

const queryStatistics = new QueryStatistics();
const meshAdapter = meshAdapterFactory.buildMeshAdapter(
options,
const meshAdapter = await meshAdapterFactory.buildMeshAdapter(
queryStatistics,
);

const allTenants = await meshAdapter.getMeshTenants();

const tableFactory = new MeshTableFactory(isatty);

const presenterFactory = new TenantListPresenterFactory(tableFactory);
const presenter = presenterFactory.buildPresenter(
options.output,
allTenants,
queryStatistics,
);

presenter.present();
}
121 changes: 121 additions & 0 deletions src/mesh/MeshFoundationAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { AwsMeshAdapter } from "/api/aws/aws-mesh-adapter.ts";
import { AzureMeshAdapter } from "/api/az/azure-mesh-adapter.ts";
import { MultiMeshAdapter } from "./multi-mesh-adapter.ts";
import { GcpMeshAdapter } from "/api/gcloud/gcp-mesh-adapter.ts";
import { MeshAdapter } from "./mesh-adapter.ts";
import { TimeWindowCalculator } from "./time-window-calculator.ts";
import { newMeshTenantRepository } from "../db/mesh-tenant-repository.ts";
import { CachingMeshAdapterDecorator } from "./caching-mesh-adapter-decorator.ts";
import { StatsMeshAdapterDecorator } from "./stats-mesh-adapter-decorator.ts";
import { MeshPlatform } from "./mesh-tenant.model.ts";
import {
QueryStatistics,
STATS_LAYER_CACHE,
STATS_LAYER_PLATFORM,
} from "./query-statistics.ts";
import { MeshError } from "../errors.ts";
import { MeshTenantChangeDetector } from "./mesh-tenant-change-detector.ts";
import { FoundationRepository } from "../model/FoundationRepository.ts";
import { PlatformConfig } from "../model/PlatformConfig.ts";
import { CollieRepository } from "../model/CollieRepository.ts";
import { CliApiFacadeFactory } from "../api/CliApiFacadeFactory.ts";

/**
* Should consume the cli configuration in order to build the
* proper adapter.
*/
export class MeshFoundationAdapterFactory {
private readonly timeWindowCalc = new TimeWindowCalculator();
private readonly tenantChangeDetector = new MeshTenantChangeDetector();

constructor(
private readonly collie: CollieRepository,
private readonly foundation: FoundationRepository,
private readonly facadeFactory: CliApiFacadeFactory,
) {}

async buildMeshAdapter(queryStats: QueryStatistics): Promise<MeshAdapter> {
const buildAdapterTasks = this.foundation.platforms.map((x) =>
this.buildPlatformAdapter(x, queryStats)
);

const adapters = await Promise.all(buildAdapterTasks);

// TODO: change this to cache per platform!

// There are multiple ways of doing it. Currently we only fetch everything or nothing, which is easier I guess.
// we could also split every platform into an individual repository folder and perform fetching and caching on a per-platform
// basis. For now we go with the easier part.
const tenantRepository = newMeshTenantRepository();

const cachingMeshAdapter = new CachingMeshAdapterDecorator(
tenantRepository,
new MultiMeshAdapter(adapters),
);

if (queryStats) {
return new StatsMeshAdapterDecorator(
cachingMeshAdapter,
"cache",
STATS_LAYER_CACHE,
queryStats,
);
}

return cachingMeshAdapter;
}

// TODO: caching per platform, optimize stats decorator building
async buildPlatformAdapter(
config: PlatformConfig,
queryStats: QueryStatistics,
): Promise<MeshAdapter> {
if ("aws" in config) {
const aws = await this.facadeFactory.buildAws(config.cli.aws);
const awsAdapter = new AwsMeshAdapter(
aws,
config.aws.accountAccessRole,
this.tenantChangeDetector,
);

return new StatsMeshAdapterDecorator(
awsAdapter,
MeshPlatform.AWS, // todo: needs to be per platform instance, not per platform type
STATS_LAYER_PLATFORM,
queryStats,
);
} else if ("azure" in config) {
const az = await this.facadeFactory.buildAz(config.cli.az);
const azureAdapter = new AzureMeshAdapter(az, this.tenantChangeDetector);

return new StatsMeshAdapterDecorator(
azureAdapter,
MeshPlatform.Azure,
STATS_LAYER_PLATFORM,
queryStats,
);
} else if ("gcp" in config) {
const gcloud = await this.facadeFactory.buildGcloud(config.cli.gcloud);
const gcpAdapter = new GcpMeshAdapter(
gcloud,
this.timeWindowCalc,
this.tenantChangeDetector,
);

return new StatsMeshAdapterDecorator(
gcpAdapter,
MeshPlatform.GCP,
STATS_LAYER_PLATFORM,
queryStats,
);
} else {
throw new MeshError(
`Invalid platform definition with unknown platform type at ${
this.collie.relativePath(
this.foundation.resolvePlatformPath(config),
)
}`,
);
}
}
}
3 changes: 3 additions & 0 deletions src/mesh/query-statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { MeshPlatform } from "./mesh-tenant.model.ts";

type DurationContainer = { [P in MeshPlatform | "cache"]?: number };

export const STATS_LAYER_CACHE = 0;
export const STATS_LAYER_PLATFORM = 1;

export class QueryStatistics {
duration: DurationContainer = {};

Expand Down
Loading

0 comments on commit c83556b

Please sign in to comment.