Skip to content

Commit

Permalink
feat!: support readonly fs (#3036)
Browse files Browse the repository at this point in the history
- Closes #2078

BREAKING CHANGE: User files and folders are now symlinked from `/tmp/containerbase/cache` to `/home/ubuntu`.
  • Loading branch information
viceice committed Oct 1, 2024
1 parent 563c0d4 commit df0306f
Show file tree
Hide file tree
Showing 55 changed files with 949 additions and 375 deletions.
2 changes: 2 additions & 0 deletions src/cli/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Cli } from 'clipanion';
import type { CliMode } from '../utils';
import { logger } from '../utils/logger';
import { DownloadFileCommand } from './download-file';
import { InitToolCommand } from './init-tool';
import { InstallGemCommand, InstallGemShortCommand } from './install-gem';
import { InstallNpmCommand, InstallNpmShortCommand } from './install-npm';
import { InstallPipCommand, InstallPipShortCommand } from './install-pip';
Expand Down Expand Up @@ -40,4 +41,5 @@ export function prepareCommands(cli: Cli, mode: CliMode | null): void {
cli.register(InstallPipCommand);
cli.register(InstallToolCommand);
cli.register(PrepareToolCommand);
cli.register(InitToolCommand);
}
26 changes: 26 additions & 0 deletions src/cli/command/init-tool.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Cli } from 'clipanion';
import { describe, expect, test, vi } from 'vitest';
import { prepareCommands } from '.';

const mocks = vi.hoisted(() => ({
installTool: vi.fn(),
prepareTools: vi.fn(),
initializeTools: vi.fn(),
}));

vi.mock('../install-tool', () => mocks);
vi.mock('../prepare-tool', () => mocks);

describe('index', () => {
test('init-tool', async () => {
const cli = new Cli({ binaryName: 'cli' });
prepareCommands(cli, null);

expect(await cli.run(['init', 'tool', 'node'])).toBe(0);
expect(mocks.initializeTools).toHaveBeenCalledOnce();
expect(mocks.initializeTools).toHaveBeenCalledWith(['node'], false);

mocks.initializeTools.mockRejectedValueOnce(new Error('test'));
expect(await cli.run(['init', 'tool', 'node'])).toBe(1);
});
});
47 changes: 47 additions & 0 deletions src/cli/command/init-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Command, Option } from 'clipanion';
import prettyMilliseconds from 'pretty-ms';
import { initializeTools } from '../prepare-tool';
import { logger } from '../utils';

export class InitToolCommand extends Command {
static override paths = [['init', 'tool']];

static override usage = Command.Usage({
description:
'Initialize a tool into the container. This creates missing files and directories.',
examples: [
['Initialize node', '$0 init tool node'],
['Initialize all tools', '$0 init tool all'],
],
});

tools = Option.Rest({ required: 1 });

dryRun = Option.Boolean('-d,--dry-run', false);

async execute(): Promise<number | void> {
const start = Date.now();
let error = false;
logger.info(`Initializing tools ${this.tools.join(', ')}...`);
try {
return await initializeTools(this.tools, this.dryRun);
} catch (err) {
error = true;
logger.debug(err);
if (err instanceof Error) {
logger.fatal(err.message);
}
return 1;
} finally {
if (error) {
logger.fatal(
`Initialize tools ${this.tools.join(', ')} failed in ${prettyMilliseconds(Date.now() - start)}.`,
);
} else {
logger.info(
`Initialize tools ${this.tools.join(', ')} succeded in ${prettyMilliseconds(Date.now() - start)}.`,
);
}
}
}
}
21 changes: 17 additions & 4 deletions src/cli/install-tool/base-install.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { join } from 'node:path';
import { codeBlock } from 'common-tags';
import { injectable } from 'inversify';
import type { EnvService, PathService } from '../services';
import { NoPrepareTools } from '../tools';
import { isValid } from '../utils';
import { NoInitTools, NoPrepareTools } from '../tools';
import { isValid, tool2path } from '../utils';

export interface ShellWrapperConfig {
name?: string;
Expand Down Expand Up @@ -35,12 +35,20 @@ export abstract class BaseInstallService {
return !!(await this.pathSvc.findVersionedToolPath(this.name, version));
}

async isInitialized(): Promise<boolean> {
return await this.pathSvc.isInitialized(this.name);
}

async isPrepared(): Promise<boolean> {
return null !== (await this.pathSvc.findToolPath(this.name));
return await this.pathSvc.isPrepared(this.name);
}

abstract link(version: string): Promise<void>;

needsInitialize(): boolean {
return !NoInitTools.includes(this.name);
}

needsPrepare(): boolean {
return !NoPrepareTools.includes(this.name);
}
Expand Down Expand Up @@ -70,14 +78,19 @@ export abstract class BaseInstallService {
}: ShellWrapperConfig): Promise<void> {
const tgt = join(this.pathSvc.binDir, name ?? this.name);

const envs = [...(extraToolEnvs ?? []), this.name];
const envs = [...(extraToolEnvs ?? []), this.name].map(tool2path);
let content = codeBlock`
#!/bin/bash
if [[ -z "\${CONTAINERBASE_ENV+x}" ]]; then
. ${this.pathSvc.envFile}
fi
if [[ ! -f "${this.pathSvc.toolInitPath(this.name)}" ]]; then
# set logging to only warn and above to not interfere with tool output
CONTAINERBASE_LOG_LEVEL=warn containerbase-cli init tool "${this.name}"
fi
# load tool envs
for n in ${envs.join(' ')}; do
if [[ -f "${this.pathSvc.toolsPath}/\${n}/env.sh" ]]; then
Expand Down
16 changes: 15 additions & 1 deletion src/cli/install-tool/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import fs from 'node:fs/promises';
import { beforeAll, describe, expect, test, vi } from 'vitest';
import { rootPath } from '../../../test/path';
import { installTool, resolveVersion } from '.';

vi.mock('del');
Expand All @@ -7,6 +9,18 @@ vi.mock('../tools/bun');
vi.mock('../tools/php/composer');

describe('index', () => {
beforeAll(async () => {
for (const p of [
'var/lib/containerbase/tool.prep.d',
'tmp/containerbase/tool.init.d',
]) {
const prepDir = rootPath(p);
await fs.mkdir(prepDir, {
recursive: true,
});
}
});

test('installTool', async () => {
expect(await installTool('bun', '1.0.0')).toBeUndefined();
expect(await installTool('dummy', '1.0.0')).toBeUndefined();
Expand Down
19 changes: 14 additions & 5 deletions src/cli/install-tool/install-tool.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deleteAsync } from 'del';
import { inject, injectable, multiInject, optional } from 'inversify';
import { prepareTools } from '../prepare-tool';
import { initializeTools, prepareTools } from '../prepare-tool';
import { EnvService, PathService, VersionService } from '../services';
import { cleanAptFiles, cleanTmpFiles, isDockerBuild, logger } from '../utils';
import type { BaseInstallService } from './base-install.service';
Expand Down Expand Up @@ -31,6 +31,8 @@ export class InstallToolService {
'supported tools',
);

await this.pathSvc.ensureBasePaths();

try {
const toolSvc = this.toolSvcs.find((t) => t.name === tool);
if (toolSvc) {
Expand All @@ -48,6 +50,14 @@ export class InstallToolService {
}
}

if (toolSvc.needsInitialize() && !(await toolSvc.isInitialized())) {
logger.debug({ tool }, 'tool not initialized');
const res = await initializeTools([tool], dryRun);
if (res) {
return res;
}
}

logger.debug({ tool }, 'validate tool');
if (!(await toolSvc.validate(version))) {
logger.fatal({ tool, version }, 'tool version not supported');
Expand Down Expand Up @@ -83,7 +93,7 @@ export class InstallToolService {

if (await isDockerBuild()) {
logger.debug('cleaning tmp files');
await cleanTmpFiles(this.pathSvc.tmpDir, dryRun);
await cleanTmpFiles(this.envSvc.tmpDir, dryRun);

if (this.envSvc.isRoot) {
logger.debug('cleaning root caches');
Expand All @@ -96,9 +106,8 @@ export class InstallToolService {
logger.debug('cleaning user caches');
await deleteAsync(
[
`${this.envSvc.userHome}/.cache`,
`${this.envSvc.userHome}/.local/share/virtualenv`,
`${this.pathSvc.cachePath}/**`,
`${this.pathSvc.cachePath}/.cache/**`,
`${this.pathSvc.cachePath}/.local/share/virtualenv/**`,
],
{
force: true,
Expand Down
9 changes: 5 additions & 4 deletions src/cli/prepare-tool/base-prepare.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ export abstract class BasePrepareService {
@inject(EnvService) protected readonly envSvc: EnvService,
) {}

abstract execute(): Promise<void> | void;

async isPrepared(): Promise<boolean> {
return null !== (await this.pathSvc.findToolPath(this.name));
prepare(): Promise<void> | void {
// noting to do;
}
initialize(): Promise<void> | void {
// noting to do;
}

toString(): string {
Expand Down
30 changes: 28 additions & 2 deletions src/cli/prepare-tool/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, expect, test, vi } from 'vitest';
import { prepareTools } from '.';
import fs from 'node:fs/promises';
import { beforeAll, describe, expect, test, vi } from 'vitest';
import { rootPath } from '../../../test/path';
import { initializeTools, prepareTools } from '.';

vi.mock('del');
vi.mock('execa');
Expand All @@ -12,7 +14,31 @@ vi.mock('node:process', async (importOriginal) => ({
}));

describe('index', () => {
beforeAll(async () => {
for (const p of [
'var/lib/containerbase/tool.prep.d',
'tmp/containerbase/tool.init.d',
'usr/local/containerbase/tools/v2',
]) {
const prepDir = rootPath(p);
await fs.mkdir(prepDir, {
recursive: true,
});
}

await fs.writeFile(
rootPath('usr/local/containerbase/tools/v2/dummy.sh'),
'',
);
});

test('prepareTools', async () => {
expect(await prepareTools(['bun', 'dummy'])).toBeUndefined();
expect(await prepareTools(['not-exist'])).toBe(1);
});

test('initializeTools', async () => {
expect(await initializeTools(['bun', 'dummy'])).toBeUndefined();
expect(await initializeTools(['not-exist'])).toBeUndefined();
});
});
10 changes: 9 additions & 1 deletion src/cli/prepare-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,13 @@ export function prepareTools(
dryRun = false,
): Promise<number | void> {
const container = prepareContainer();
return container.get(PrepareToolService).execute(tools, dryRun);
return container.get(PrepareToolService).prepare(tools, dryRun);
}

export function initializeTools(
tools: string[],
dryRun = false,
): Promise<number | void> {
const container = prepareContainer();
return container.get(PrepareToolService).initialize(tools, dryRun);
}
13 changes: 10 additions & 3 deletions src/cli/prepare-tool/prepare-legacy-tools.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { logger } from '../utils';

@injectable()
export class PrepareLegacyToolsService {
async execute(tools: string[]): Promise<void> {
logger.debug(`Preparing legacy tools ${tools.join(', ')} ...`);
await execa('/usr/local/containerbase/bin/prepare-tool.sh', tools, {
async prepare(tool: string): Promise<void> {
logger.debug(`Preparing legacy tool ${tool} ...`);
await execa('/usr/local/containerbase/bin/prepare-tool.sh', [tool], {
stdio: ['inherit', 'inherit', 1],
});
}

async initialize(tool: string): Promise<void> {
logger.debug(`Initializing legacy tool ${tool} ...`);
await execa('/usr/local/containerbase/bin/init-tool.sh', [tool], {
stdio: ['inherit', 'inherit', 1],
});
}
Expand Down
Loading

0 comments on commit df0306f

Please sign in to comment.