Skip to content

Commit

Permalink
feat: support arbitrary mount options for each cache
Browse files Browse the repository at this point in the history
Signed-off-by: Amin Yahyaabadi <[email protected]>
  • Loading branch information
aminya committed Apr 2, 2024
1 parent 0fc239d commit 0391354
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 26 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ Real-world examples:
- <https://github.com/rootless-containers/slirp4netns/blob/v1.2.2/.github/workflows/release.yaml#L18-L36>
- <https://github.com/containers/fuse-overlayfs/blob/40e0f3c/.github/workflows/release.yaml#L17-L36>

## CacheMap Options

Optionally, instead of a single string for the `target`, you can provide an object with additional options that should be passed to `--mount=type=cache` in the values `cache-map` JSON. The `target` path must be present in the object as a property.

```json
{
"var-cache-apt": {
"target": "/var/cache/apt",
"sharing": "locked",
"id": "1"
},
"var-lib-apt": "/var/lib/apt"
}
```

## CLI Usage

In other CI systems, you can run the script directly via `node`:
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: "Injects the cached data into the docker build(x|kit) process"
inputs:
cache-map:
required: true
description: "The map of actions source to container destination paths for the cache paths"
description: "The map of actions source paths to container destination paths or mount arguments"
cache-source:
deprecationMessage: "Use `cache-map` instead"
description: "Where the cache is stored in the calling workspace. Default: `cache`"
Expand Down
41 changes: 32 additions & 9 deletions dist/index.js

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

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions src/extract-cache.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import fs from 'fs/promises';
import path from 'path';
import { Opts, getCacheMap } from './opts.js';
import { CacheOptions, Opts, getCacheMap, getMountArgsString, getTargetPath } from './opts.js';
import { run, runPiped } from './run.js';
import { spawn } from 'child_process';

async function extractCache(cacheSource: string, cacheTarget: string, scratchDir: string) {
async function extractCache(cacheSource: string, cacheOptions: CacheOptions, scratchDir: string) {
// Prepare Timestamp for Layer Cache Busting
const date = new Date().toISOString();
await fs.writeFile(path.join(scratchDir, 'buildstamp'), date);

// Prepare Dancefile to Access Caches
const targetPath = getTargetPath(cacheOptions);
const mountArgs = getMountArgsString(cacheOptions);

const dancefileContent = `
FROM busybox:1
COPY buildstamp buildstamp
RUN --mount=type=cache,target=${cacheTarget} \
RUN --mount=${mountArgs} \
mkdir -p /var/dance-cache/ \
&& cp -p -R ${cacheTarget}/. /var/dance-cache/ || true
&& cp -p -R ${targetPath}/. /var/dance-cache/ || true
`;
await fs.writeFile(path.join(scratchDir, 'Dancefile.extract'), dancefileContent);
console.log(dancefileContent);
Expand Down Expand Up @@ -52,7 +55,7 @@ export async function extractCaches(opts: Opts) {
const scratchDir = opts['scratch-dir'];

// Extract Caches for each source-target pair
for (const [cacheSource, cacheTarget] of Object.entries(cacheMap)) {
await extractCache(cacheSource, cacheTarget, scratchDir);
for (const [cacheSource, cacheOptions] of Object.entries(cacheMap)) {
await extractCache(cacheSource, cacheOptions, scratchDir);
}
}
15 changes: 9 additions & 6 deletions src/inject-cache.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fs from 'fs/promises';
import path from 'path';
import { Opts, getCacheMap } from './opts.js';
import { CacheOptions, Opts, getCacheMap, getMountArgsString, getTargetPath } from './opts.js';
import { run } from './run.js';

async function injectCache(cacheSource: string, cacheTarget: string, scratchDir: string) {
async function injectCache(cacheSource: string, cacheOptions: CacheOptions, scratchDir: string) {
// Clean Scratch Directory
await fs.rm(scratchDir, { recursive: true, force: true });
await fs.mkdir(scratchDir, { recursive: true });
Expand All @@ -15,13 +15,16 @@ async function injectCache(cacheSource: string, cacheTarget: string, scratchDir:
const date = new Date().toISOString();
await fs.writeFile(path.join(cacheSource, 'buildstamp'), date);

const targetPath = getTargetPath(cacheOptions);
const mountArgs = getMountArgsString(cacheOptions);

// Prepare Dancefile to Access Caches
const dancefileContent = `
FROM busybox:1
COPY buildstamp buildstamp
RUN --mount=type=cache,target=${cacheTarget} \
RUN --mount=${mountArgs} \
--mount=type=bind,source=.,target=/var/dance-cache \
cp -p -R /var/dance-cache/. ${cacheTarget} || true
cp -p -R /var/dance-cache/. ${targetPath} || true
`;
await fs.writeFile(path.join(scratchDir, 'Dancefile.inject'), dancefileContent);
console.log(dancefileContent);
Expand All @@ -39,7 +42,7 @@ export async function injectCaches(opts: Opts) {
const scratchDir = opts['scratch-dir'];

// Inject Caches for each source-target pair
for (const [cacheSource, cacheTarget] of Object.entries(cacheMap)) {
await injectCache(cacheSource, cacheTarget, scratchDir);
for (const [cacheSource, cacheOptions] of Object.entries(cacheMap)) {
await injectCache(cacheSource, cacheOptions, scratchDir);
}
}
43 changes: 40 additions & 3 deletions src/opts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,54 @@ Save 'RUN --mount=type=cache' caches on GitHub Actions or other CI platforms
Options:
--extract Extract the cache from the docker container (extract step). Otherwise, inject the cache (main step)
--cache-map The map of actions source to container destination paths for the cache paths
--cache-map The map of actions source paths to container destination paths or mount arguments
--scratch-dir Where the action is stores some temporary files for its processing. Default: 'scratch'
--skip-extraction Skip the extraction of the cache from the docker container
--help Show this help
`);
}

export function getCacheMap(opts: Opts): Record<string, string> {
export type SourcePath = string
export type TargetPath = string
export type ToStringable = {
toString(): string;
}
export type CacheOptions = TargetPath | { target: TargetPath } & Record<string, ToStringable>
export type CacheMap = Record<SourcePath, CacheOptions>

export function getCacheMap(opts: Opts): CacheMap {
try {
return JSON.parse(opts["cache-map"]) as Record<string, string>;
return JSON.parse(opts["cache-map"]) as CacheMap;
} catch (e) {
throw new Error(`Failed to parse cache map. Expected JSON, got:\n${opts["cache-map"]}\n${e}`);
}
}

export function getTargetPath(cacheOptions: CacheOptions): TargetPath {
if (typeof cacheOptions === "string") {
// only the target path is provided
return cacheOptions;
} else {
// object is provided
try {
return cacheOptions.target;
} catch (e) {
throw new Error(`Expected the 'target' key in the cache options, got:\n${cacheOptions}\n${e}`);
}
}
}

/**
* Convert a cache options to a string that is passed to --mount=
* @param CacheOptions The cache options to convert to a string
*/
export function getMountArgsString(cacheOptions: CacheOptions): string {
if (typeof cacheOptions === "string") {
// only the target path is provided
return `type=cache,target=${cacheOptions}`;
} else {
// other options are provided
const otherOptions = Object.entries(cacheOptions).map(([key, value]) => `${key}=${value}`).join(",");
return `type=cache,${otherOptions}`;
}
}

0 comments on commit 0391354

Please sign in to comment.