Skip to content

Commit

Permalink
feat: setpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
hugojosefson committed May 13, 2024
1 parent f751064 commit 0511cd3
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .idea/watcherTasks.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { assertEquals } from "https://deno.land/std@0.224.0/assert/assert_equals.ts";
import { calculateAppDirSegments } from "./calculate-app-dir-segments.ts";
import { s } from "../../../deps.ts";
import { AbsolutePath } from "../../absolute-path.ts";

const cases = [
["/mnt/apps", "/mnt/apps/app1", ["app1"]],
["/mnt/apps", "/mnt/apps/app1/app2", ["app1", "app2"]],
["/mnt/apps", "/mnt/apps/app1/app2/app3", ["app1", "app2", "app3"]],
] as [AbsolutePath, AbsolutePath, string[]][];

for (const [appsDir, appDir, expected] of cases) {
Deno.test(`calculateAppDirSegments(${appsDir}, ${appDir}) => ${s(expected)}`, () =>
assertEquals(
calculateAppDirSegments(appsDir)(appDir),
expected,
));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AbsolutePath } from "../../absolute-path.ts";
import { s } from "../../../deps.ts";

export function calculateAppDirSegments<AppsDir extends AbsolutePath>(
appsDir: AppsDir,
): (appDir: AbsolutePath) => string[] {
return function (appDir: AbsolutePath): string[] {
if (appDir === appsDir) {
return [];
}
if (!appDir.startsWith(appsDir)) {
throw new Error(
`Expected appDir to be a subdirectory of appsDir, but got ${
s({ appDir, appsDir })
}`,
);
}
const relativeAppDir = appDir.slice(appsDir.length + 1);
return relativeAppDir.split("/");
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AbsolutePath } from "../../absolute-path.ts";
import { CreateAppContainerOptions } from "../create-app-container/options.ts";
import { Setpoint } from "./setpoint.ts";
import {
DEFAULT_LOAD_CONFIG_FILES_OPTIONS,
loadConfig,
} from "../../../deps.ts";
import { findAppDirs } from "./find-app-dirs.ts";
import { calculateAppDirSegments } from "./calculate-app-dir-segments.ts";

/**
* Builds a list of apps, with {@link findAppDirs}.
* Populates each app in the list, with config loaded by https://deno.land/x/load_config_files. { commonNames: ["index", "common", "incus-app-container"]}
* Returns a setpoint, which is a list of apps, with their configs.
* @param appsDir
*/
export async function calculateSetpoint<AppsDir extends AbsolutePath>(
appsDir: AppsDir,
): Promise<Setpoint<AppsDir>> {
const appDirs: AbsolutePath[] = await findAppDirs(appsDir);
const apps: Setpoint<AppsDir>["apps"] = Object.fromEntries(
await Promise.all(appDirs.map(async (appDir) => [
appDir,
await loadConfig(
new URL(`file://${appsDir}`),
calculateAppDirSegments(appsDir)(appDir),
{
commonNames: [
...DEFAULT_LOAD_CONFIG_FILES_OPTIONS.commonNames,
"incus-app-container",
],
configTransformers: [
(config) => config as CreateAppContainerOptions<AppsDir>,
],
},
) as CreateAppContainerOptions<AppsDir>,
])),
);
return {
appsDir,
apps,
};
}
110 changes: 110 additions & 0 deletions src/incus-app-container-files/cli/commands/setpoint/find-app-dirs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Getter } from "https://deno.land/x/fns@1.1.1/fn/getter.ts";
import {
DEFAULT_CONFIG_FILE_LOADERS,
exists,
pFilter,
pMap,
prop,
} from "../../../deps.ts";
import { AbsolutePath, isAbsolutePath } from "../../absolute-path.ts";

const CONFIG_FILE_EXTENSIONS = Object.keys(DEFAULT_CONFIG_FILE_LOADERS);

function getPossibleConfigFilePaths(
directory: AbsolutePath,
): AbsolutePath[] {
return CONFIG_FILE_EXTENSIONS.map((extension) =>
`${directory}/incus-app-container.${extension}` as AbsolutePath
);
}

function getPossibleDockerComposeFilePaths(
directory: AbsolutePath,
): AbsolutePath[] {
return [
"yml",
"yaml",
"json",
].map((extension) =>
`${directory}/docker-compose.${extension}` as AbsolutePath
);
}

async function directoryHasAnyConfigFile(
directory: AbsolutePath,
): Promise<boolean> {
if (!(await exists(directory, { isDirectory: true }))) {
return false;
}
const possibleConfigFilePaths = getPossibleConfigFilePaths(directory);
return (await pFilter(possibleConfigFilePaths, (path) => exists(path)))
.length > 0;
}

async function directoryHasAnyDockerComposeFile(
directory: AbsolutePath,
): Promise<boolean> {
if (!(await exists(directory, { isDirectory: true }))) {
return false;
}
const possibleDockerComposeFilePaths = getPossibleDockerComposeFilePaths(
directory,
);
return (await pFilter(possibleDockerComposeFilePaths, (path) => exists(path)))
.length > 0;
}

/**
* Builds a list of apps, by scanning the appsDirectory recursively.
* When it encounters an incus-app-container.${CONFIG_FILE_EXTENSIONS} file in a directory, it considers that directory an app, and doesn't traverse it any deeper.
* When it encounters a directory named "appdata", or a file named "docker-compose.{yml,yaml,json}", it considers its parent directory a likely disabled app, and doesn't traverse it any deeper.
* This function is recursive, and will call itself on subdirectories.
*
* Does NOT read the config files, just returns the directory names.
*
* @param appsDir the directory where app directories are stored
* @returns a list of directories that are apps (have an `incus-app-container.${CONFIG_FILE_EXTENSIONS}` file)
*/
export async function findAppDirs<
AppsDir extends AbsolutePath,
>(appsDir: AppsDir): Promise<AbsolutePath[]> {
if (await directoryHasAnyConfigFile(appsDir)) {
return [appsDir as AbsolutePath];
}
const dirEntryAsyncIterable: AsyncIterable<Deno.DirEntry> = Deno.readDir(
appsDir,
);
const dirEntryAsyncIterator: AsyncIterator<Deno.DirEntry> =
dirEntryAsyncIterable[Symbol.asyncIterator]();
const isDirectoryGetter = prop("isDirectory") as Getter<boolean>;
const directories = await pFilter(
dirEntryAsyncIterator,
isDirectoryGetter,
);
return (await pMap(
directories,
async (entry: Deno.DirEntry & { isDirectory: true }) => {
const potentialAppDir = `${appsDir}/${entry.name}` as AbsolutePath;
if (await directoryHasAnyConfigFile(potentialAppDir)) {
return [potentialAppDir];
}
if (entry.name === "appdata") {
return [];
}
if (await directoryHasAnyDockerComposeFile(potentialAppDir)) {
return [];
}
return await findAppDirs(potentialAppDir);
},
)).flat();
}

if (import.meta.main) {
const appsDir = new URL(Deno.args[0], import.meta.url).pathname;
if (!isAbsolutePath(appsDir)) {
throw new Error(
`Expected absolute path for apps-dir, but got ${s(appsDir)}`,
);
}
console.dir(await findAppDirs(appsDir));
}
19 changes: 19 additions & 0 deletions src/incus-app-container-files/cli/commands/setpoint/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AbsolutePath } from "../../absolute-path.ts";
import { calculateSetpoint } from "./calculate-setpoint.ts";
import { SetpointInputOptions } from "../../config.ts";

/**
* Prints the current setpoint; the containers we want, according to configuration files.
* @param options
*/
export async function setpoint<AppsDir extends AbsolutePath>(
options: SetpointInputOptions<AppsDir>,
): Promise<void> {
console.log(
JSON.stringify(
await calculateSetpoint(options.appsDir),
null,
2,
),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AbsolutePath } from "../../absolute-path.ts";
import { CreateAppContainerOptions } from "../create-app-container/options.ts";

export type Setpoint<AppsDir extends AbsolutePath> = {
appsDir: AppsDir;
apps: Record<string, CreateAppContainerOptions<AppsDir>>;
};
6 changes: 6 additions & 0 deletions src/incus-app-container-files/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export type Config<AppsDir extends AbsolutePath = AbsolutePath> =
& Partial<InputOptions<"create", AppsDir>>
& Partial<InputOptions<"list", AppsDir>>
& Partial<InputOptions<"setup-incus", AppsDir>>
& Partial<InputOptions<"setpoint", AppsDir>>
& Partial<InputOptionsPerCommand<AppsDir>>;

export type InputOptions<C extends CommandName, AppsDir extends AbsolutePath> =
C extends "create" ? CreateInputOptions<AppsDir>
: C extends "list" ? ListInputOptions
: C extends "setup-incus" ? SetupIncusOptions
: C extends "setpoint" ? SetpointInputOptions<AppsDir>
: never;

export type InputOptionsPerCommand<AppsDir extends AbsolutePath> = {
Expand All @@ -36,6 +38,10 @@ export type ListInputOptions = {
format: OutputFormat;
};

export type SetpointInputOptions<AppsDir extends AbsolutePath> = {
appsDir: AppsDir;
};

export async function getConfig<
AppsDir extends AbsolutePath = AbsolutePath,
>(): Promise<
Expand Down
52 changes: 38 additions & 14 deletions src/incus-app-container-files/cli/create-cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { breadc, isString, optionalTypeGuard, run } from "../deps.ts";
import {
breadc,
BreadcCommand,
isString,
optionalTypeGuard,
run,
} from "../deps.ts";
import { enforceType } from "../type-guard.ts";
import { AbsolutePath, isAbsolutePath } from "./absolute-path.ts";
import { DEFAULT_BRIDGE, isBridgeName } from "./bridge-name.ts";
Expand All @@ -8,6 +14,7 @@ import { isSize } from "./commands/create-app-container/size.ts";
import { isSshKey } from "./commands/create-app-container/ssh-key.ts";
import { listAppContainers } from "./commands/list-app-containers.ts";
import { setupIncus } from "./commands/setup-incus/mod.ts";
import { setpoint } from "./commands/setpoint/mod.ts";
import { Config } from "./config.ts";
import {
INCUS_CONTAINER_STATUS_CODES,
Expand All @@ -25,6 +32,7 @@ export const COMMAND_NAMES = [
"create",
"list",
"setup-incus",
"setpoint",
] as const;
export type CommandName = typeof COMMAND_NAMES[number];

Expand All @@ -42,6 +50,26 @@ export async function createCli<
version: "0.0.0",
});

const appsDirOptionCast = await enforceType(
isAbsolutePath,
"an absolute path, for example /mnt/apps",
"apps-dir",
);
const createAppsDirOption = (
commandName: CommandName,
): ReturnType<BreadcCommand.option> => {
return [
"--apps-dir <apps-dir>",
{
default: defaults?.[commandName]?.appsDir ?? defaults.appsDir ??
"/mnt/apps",
description:
"Base directory for where all app containers' metadata and appdata are (to be) stored.",
cast: appsDirOptionCast,
},
];
};

cli
.command("create <container_name>", "Create a new Incus app container.")
.option(
Expand Down Expand Up @@ -107,19 +135,7 @@ export async function createCli<
default: defaults?.create?.diskSize ?? defaults.diskSize ?? "10GiB",
},
)
.option(
"--apps-dir <apps-dir>",
{
default: defaults?.create?.appsDir ?? defaults.appsDir ?? "/mnt/apps",
description:
"Base directory for where all app containers' metadata and appdata are (to be) stored.",
cast: await enforceType(
isAbsolutePath,
"an absolute path, for example /mnt/apps",
"apps-dir",
),
},
)
.option(...createAppsDirOption("create"))
.action(
async (name: string, inputOptions) => {
const options = await resolveCreateAppContainerOptions(inputOptions);
Expand Down Expand Up @@ -191,5 +207,13 @@ export async function createCli<
)
.action(setupIncus);

cli
.command(
"setpoint",
"Print the current setpoint; the containers we want, according to configuration files.",
)
.option(...createAppsDirOption("setpoint"))
.action(setpoint);

return cli;
}
17 changes: 16 additions & 1 deletion src/incus-app-container-files/deps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// std
export { basename } from "https://deno.land/std@0.222.1/path/basename.ts";
export { dirname } from "https://deno.land/std@0.222.1/path/dirname.ts";
export { exists } from "https://deno.land/std@0.224.0/fs/exists.ts";
export { parse as parseToml } from "https://deno.land/std@0.222.1/toml/parse.ts";
export {
parse as parseCsv,
Expand All @@ -13,6 +14,11 @@ export { mapValues } from "https://deno.land/std@0.222.1/collections/map_values.
// x
export { fetch as fetchFile } from "https://deno.land/x/file_fetch@0.2.0/mod.ts";
export { default as camelCase } from "https://deno.land/x/case@2.2.0/camelCase.ts";
export {
DEFAULT_FILE_LOADERS as DEFAULT_CONFIG_FILE_LOADERS,
DEFAULT_OPTIONS as DEFAULT_LOAD_CONFIG_FILES_OPTIONS,
loadConfig,
} from "https://deno.land/x/load_config_files@0.3.0/mod.ts";

// x/cliffy
export { Select } from "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/select.ts";
Expand All @@ -37,6 +43,7 @@ export {
startWith,
unicode,
} from "https://deno.land/x/fns@1.1.1/string/regex.ts";
export { prop } from "https://deno.land/x/fns@1.1.1/object/prop.ts";

// x/fns@unstable
export { optional as optionalTypeGuard } from "https://raw.githubusercontent.com/hugojosefson/fns/unstable/type-guard/optional.ts";
Expand All @@ -45,7 +52,15 @@ export { createDeepMapKeys } from "https://raw.githubusercontent.com/hugojosefso

// npm
export { default as split } from "npm:argv-split@2.0.1";
export { breadc, ParseError } from "npm:breadc@0.9.7";
export {
breadc,
type Command as BreadcCommand,
ParseError,
} from "npm:breadc@0.9.7";

// npm sindresorhus/p-*
export { default as pMap } from "npm:p-map@7.0.2";
export { default as pFilter } from "npm:p-filter@4.1.0";

// npm ip-cidr
export { type Address } from "npm:ip-cidr@4.0.0";
Expand Down

0 comments on commit 0511cd3

Please sign in to comment.