├── .eslintrc.cjs
├── .github
└── workflows
│ ├── deploy.yml
│ ├── lint.yml
│ └── security.yml
├── .gitignore
├── .prettierrc
├── README.md
├── astro.config.mjs
├── dev
├── Makefile
├── dockerfile.dev
├── pull
└── upload.sh
├── package-lock.json
├── package.json
├── src
├── env.d.ts
├── layouts
│ └── Layout.astro
├── lib
│ └── server
│ │ └── miniflare.ts
├── middleware.ts
├── pages
│ ├── index.astro
│ ├── v2
│ │ ├── [...name]
│ │ │ ├── blobs
│ │ │ │ └── [reference]
│ │ │ │ │ └── index.ts
│ │ │ └── manifests
│ │ │ │ └── [reference]
│ │ │ │ └── index.ts
│ │ └── seed.ts
│ └── version.ts
├── public
│ └── favicon.svg
├── types
│ └── bindings.d.ts
└── utils
│ ├── response.ts
│ └── url.ts
└── tsconfig.json
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "standard-with-typescript",
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:@typescript-eslint/eslint-recommended",
7 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
8 | "plugin:@typescript-eslint/strict",
9 | "prettier",
10 | "plugin:astro/recommended",
11 | "plugin:astro/jsx-a11y-strict",
12 | "plugin:tailwindcss/recommended",
13 | "plugin:regexp/recommended",
14 | "plugin:mocha/recommended",
15 | ],
16 | parser: "@typescript-eslint/parser",
17 | parserOptions: {
18 | tsconfigRootDir: __dirname,
19 | project: ["./tsconfig.json"],
20 | },
21 | plugins: ["@typescript-eslint", "prettier", "sonarjs", "regexp", "mocha"],
22 | overrides: [
23 | {
24 | files: ["*.astro"],
25 | parser: "astro-eslint-parser",
26 | parserOptions: {
27 | extraFileExtensions: [".astro"],
28 | },
29 | },
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | env:
10 | env_name: ${{ github.ref != 'refs/heads/master' && format('pr-{0}', github.event.number) || 'prod' }}
11 | cf_name: ${{ github.ref != 'refs/heads/master' && format('pr-{0}', github.event.number) || 'main' }}
12 | env_url: ${{ github.ref != 'refs/heads/master' && format('https://pr-{0}.{1}.pages.dev', github.event.number, 'containerflare') || 'https://cfcr.dev' }}
13 |
14 | jobs:
15 | publish:
16 | runs-on: ubuntu-latest
17 | permissions:
18 | contents: read
19 | deployments: write
20 | pull-requests: write
21 | environment:
22 | name: ${{ github.ref != 'refs/heads/master' && format('pr-{0}', github.event.number) || 'prod' }}
23 | url: ${{ github.ref != 'refs/heads/master' && format('https://pr-{0}.{1}.pages.dev', github.event.number, 'containerflare') || 'https://cfcr.dev' }}
24 | if: github.event.pull_request.merged != true && github.event.pull_request.closed != true
25 | name: Deploy to Cloudflare Pages
26 | steps:
27 | - name: Checkout code
28 | uses: actions/checkout@v3
29 |
30 | - name: Setup Node
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: 18
34 | cache: "npm"
35 |
36 | - name: Initialize npm
37 | run: npm ci
38 |
39 | - name: Initialize npm
40 | run: npm run build
41 |
42 | - name: Publish to Cloudflare Pages
43 | uses: cloudflare/pages-action@v1
44 | with:
45 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
46 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
47 | projectName: containerflare
48 | directory: ./dist
49 | branch: ${{ env.cf_name }}
50 | id: publish
51 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | permissions:
10 | contents: read
11 | pull-requests: read
12 |
13 | jobs:
14 | eslint:
15 | name: ESLint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v3
20 |
21 | - name: Setup Node
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: 18
25 | cache: "npm"
26 |
27 | - name: Initialize npm
28 | run: npm ci
29 |
30 | - name: Run ESLint
31 | run: npm run lint
32 |
33 | prettier:
34 | name: Prettier
35 | runs-on: ubuntu-latest
36 | steps:
37 | - name: Checkout code
38 | uses: actions/checkout@v3
39 |
40 | - name: Setup Node
41 | uses: actions/setup-node@v3
42 | with:
43 | node-version: 18
44 | cache: "npm"
45 |
46 | - name: Initialize npm
47 | run: npm ci
48 |
49 | - name: Run Prettier
50 | run: npm run prettier:ci
51 |
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Security
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | TruffleHog:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 | - name: TruffleHog Scan
18 | uses: trufflesecurity/trufflehog@v3.24.0
19 | with:
20 | path: ./
21 | base: ${{ github.event.repository.default_branch }}
22 | head: HEAD
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Env config
2 | .env
3 | .env.production
4 | .secret
5 |
6 | # build output
7 | dist/
8 | .output/
9 | .mf
10 | .wrangler
11 | functions/\[\[path\]\].js
12 |
13 |
14 | # dependencies
15 | node_modules/
16 |
17 | # logs
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 | pnpm-debug.log*
22 |
23 | # macOS-specific files
24 | .DS_Store
25 |
26 | # Misc
27 | NOTES.md
28 | data
29 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "importOrder": ["@types", "@utils", "^[./]"],
4 | "importOrderSeparation": true,
5 | "importOrderSortSpecifiers": true
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ContainerFlare
8 |
9 |
10 |
11 | Cloudflare container image repository
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 | ---
24 |
25 |
26 |
27 | ## About
28 |
29 | A globally distributed, edge container image registry that leverages the power of Cloudflare's global network. Providing `docker pull`'s 3x faster than DockerHub, Containerflare offers a unique solution for developers looking to distribute their container images worldwide efficiently.
30 |
31 | ## Features
32 |
33 | - **Global Distribution**: Utilize Cloudflare's vast network to pull container images from the edge, closest to where they are needed
34 | - **High Performance**: Experience speeds up to 3x faster than DockerHub, reducing the time it takes to download and deploy container images
35 | - **Built with Astro**: Containerflare is developed using the Astro web framework, ensuring a modern and efficient web application
36 | - **Cloudflare Pages Hosting**: By being hosted on Cloudflare Pages, Containerflare benefits from the additional speed and reliability inherent to the platform
37 |
38 | ## Demo
39 |
40 | Use the publicly hosted instance at [cfcr.dev](https://cfcr.dev):
41 |
42 | ```console
43 | docker pull cfcr.dev/hello-world
44 | ```
45 |
46 | ## Development
47 |
48 | Containerflare can be run locally for either further development or customization.
49 |
50 | > [!NOTE]
51 | > **BEFORE** you run the following steps make sure:
52 | > - You have Node installed locally on you machine
53 | > - You have `docker` & `docker-compose` installed and running
54 |
55 | ```shell
56 | # Clone the repository
57 | git clone https://github.com/MNThomson/containerflare.git && cd containerflare
58 |
59 | # To start developing, run containerflare
60 | npm start
61 | ```
62 |
63 | The development environment is now running and accesible at https://localhost:4321/
64 |
65 | To benchmark the implementation:
66 |
67 | ```shell
68 | # Build the empty container (so there is no docker cache effecting results)
69 | cd dev && make build
70 |
71 | # Run the empty container
72 | make run
73 |
74 | # Within the container shell run the command to pull from the locally running containerflare instance
75 | pull
76 | ```
77 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import cloudflare from "@astrojs/cloudflare";
2 | import { defineConfig } from "astro/config";
3 |
4 | // https://astro.build/config
5 | export default defineConfig({
6 | publicDir: "./src/public",
7 | site: `https://cfcr.dev`,
8 | output: "server",
9 | adapter: cloudflare({
10 | mode: "directory",
11 | runtime: { mode: "local", persistTo: ".wrangler/state/v3" },
12 | }),
13 | });
14 |
--------------------------------------------------------------------------------
/dev/Makefile:
--------------------------------------------------------------------------------
1 | run:
2 | docker run -d --privileged --name containerflare-dev mnthomson/containerflare:dev
3 | docker exec -it containerflare-dev /bin/sh
4 | make kill
5 |
6 | kill:
7 | docker kill containerflare-dev
8 | docker rm containerflare-dev
9 |
10 | build:
11 | docker build -t mnthomson/containerflare:dev -f dockerfile.dev .
12 |
--------------------------------------------------------------------------------
/dev/dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM docker:20.10-dind
2 |
3 | RUN mkdir -p /etc/docker && echo '{"insecure-registries" : ["172.17.0.1:8788"]}' > /etc/docker/daemon.json && \
4 | mkdir -p /etc/default && echo 'DOCKER_OPTS="--config-file=/etc/docker/daemon.json"' > /etc/default/docker
5 |
6 | ADD pull /usr/local/bin
7 |
8 | CMD ["dockerd-entrypoint.sh"]
9 |
--------------------------------------------------------------------------------
/dev/pull:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker run 172.17.0.1:8788/hello-world:latest
4 |
--------------------------------------------------------------------------------
/dev/upload.sh:
--------------------------------------------------------------------------------
1 | rm -f upload.kv
2 | touch upload.kv
3 | echo "[" >> upload.kv
4 |
5 | echo " {" >> upload.kv
6 | echo " \"key\": \"hello-world:latest\"," >> upload.kv
7 |
8 | echo " \"value\": \"$(cat ./.mf/kv/hello-world/latest)\"" >> upload.kv
9 | echo " }," >> upload.kv
10 |
11 | # https://developers.cloudflare.com/workers/wrangler/commands/#kvbulk
12 | for file in ./.mf/kv/sha256/*; do
13 | if [[ -f "$file" && ! "$(basename $file)" == *.* ]]; then
14 | file=$(basename $file)
15 | echo "KV: $file"
16 | echo " {" >> upload.kv
17 | echo " \"key\": \"sha256:$file\"," >> upload.kv
18 | contents=$(jq -Rs . < ./.mf/kv/sha256/$file)
19 | echo " \"value\": \"${contents}\"" >> upload.kv
20 | echo " }," >> upload.kv
21 | fi
22 | done
23 |
24 | echo "]" >> upload.kv
25 |
26 | npx wrangler kv:bulk put upload.kv --namespace-id=$INSERT_NAMESPACE_HERE
27 |
28 | for file in ./.mf/r2/sha256/*; do
29 | if [[ -f "$file" && ! "$(basename $file)" == *.* ]]; then
30 | file=$(basename $file)
31 | echo "R2: $file"
32 | npx wrangler r2 object put "containerflarer2/sha256:$file" --file "./.mf/r2/sha256/$file"
33 | fi
34 | done
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "containerflare",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "start": "astro dev",
8 | "dev:cf": "( sleep 10 && curl http://localhost:8788/v2/seed ) & wrangler pages dev --compatibility-date=2023-09-01 --kv kv --r2 r2 --binding $(cat ./.env) -- npm run dev:a",
9 | "preview": "npm run build && wrangler pages dev ./dist --compatibility-date=2023-09-01 --kv kv --r2 r2 --binding $(cat ./.env)",
10 | "build": "astro build",
11 | "publish": "npm run build && wrangler pages publish ./dist #--branch main",
12 | "--Oberservability": "---------------------------------------------------------",
13 | "logs:preview": "wrangler pages deployment tail --project-name containerflare --environment preview",
14 | "logs:prod": "wrangler pages deployment tail --project-name containerflare",
15 | "--Lint-----------": "---------------------------------------------------------",
16 | "format": "prettier -w .",
17 | "lint": "eslint .",
18 | "lint:dashboard": "eslint-dashboard",
19 | "prettier:ci": "prettier --check .",
20 | "--Test-----------": "---------------------------------------------------------",
21 | "pull:local": "docker pull localhost:4321/hello-world:latest && docker image rm -f localhost:4321/hello-world:latest",
22 | "pull:remote": "docker pull hello-world:latest && docker image rm -f hello-world:latest",
23 | "burp": "sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf && sudo systemctl daemon-reload && sudo systemctl restart docker"
24 | },
25 | "dependencies": {
26 | "@astrojs/cloudflare": "^7.7.1",
27 | "astro": "^3.5.5"
28 | },
29 | "devDependencies": {
30 | "@cloudflare/workers-types": "^4.20231025.0",
31 | "@miniflare/cache": "^2.14.1",
32 | "@miniflare/d1": "^2.14.1",
33 | "@miniflare/durable-objects": "^2.14.1",
34 | "@miniflare/kv": "^2.14.1",
35 | "@miniflare/r2": "^2.14.1",
36 | "@miniflare/shared": "^2.14.1",
37 | "@miniflare/storage-file": "^2.14.1",
38 | "@miniflare/storage-memory": "^2.14.1",
39 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
40 | "@typescript-eslint/eslint-plugin": "^6.0.0",
41 | "@typescript-eslint/parser": "^6.0.0",
42 | "eslint": "^8.53.0",
43 | "eslint-config-prettier": "^9.0.0",
44 | "eslint-config-standard-with-typescript": "^39.1.1",
45 | "eslint-dashboard": "^0.1.1",
46 | "eslint-plugin-astro": "^0.29.1",
47 | "eslint-plugin-jsx-a11y": "^6.8.0",
48 | "eslint-plugin-mocha": "^10.2.0",
49 | "eslint-plugin-prettier": "^5.0.1",
50 | "eslint-plugin-regexp": "^2.1.1",
51 | "eslint-plugin-sonarjs": "^0.23.0",
52 | "eslint-plugin-tailwindcss": "^3.13.0",
53 | "prettier": "^3.1.0",
54 | "prettier-plugin-astro": "^0.12.2",
55 | "typescript": "^5.2.2",
56 | "wrangler": "^3.16.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import type { KVNamespace, R2Bucket } from "@cloudflare/workers-types";
3 | import type { DirectoryRuntime } from "@astrojs/cloudflare";
4 |
5 | type ENV = {
6 | kv: KVNamespace;
7 | r2: R2Bucket;
8 | };
9 |
10 | declare namespace App {
11 | interface Locals extends DirectoryRuntime {
12 | runtime: DirectoryRuntime & {
13 | env: {
14 | kv: KVNamespace;
15 | r2: R2Bucket;
16 | };
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export interface Props {
3 | title: string;
4 | }
5 |
6 | const { title } = Astro.props;
7 | ---
8 |
9 |
10 |
11 |
12 | {title}
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
48 |
--------------------------------------------------------------------------------
/src/lib/server/miniflare.ts:
--------------------------------------------------------------------------------
1 | // ABSOLUTE LEGEND: https://github.com/sveltejs/kit/issues/4292#issuecomment-1550596497
2 | type StorageOptionsMemory = {
3 | type: "memory";
4 | };
5 |
6 | type StorageOptionsFile = {
7 | type: "file";
8 | path: string;
9 | };
10 |
11 | export type StorageOptions = StorageOptionsMemory | StorageOptionsFile;
12 |
13 | export const createCache = async (storageOptions: StorageOptions) => {
14 | const { Cache } = await import("@miniflare/cache");
15 |
16 | if (storageOptions.type === "memory") {
17 | const { MemoryStorage } = await import("@miniflare/storage-memory");
18 | return new Cache(new MemoryStorage());
19 | } else if (storageOptions.type === "file") {
20 | const { FileStorage } = await import("@miniflare/storage-file");
21 | return new Cache(new FileStorage(storageOptions.path));
22 | }
23 |
24 | throw new Error("StorageType not found");
25 | };
26 |
27 | export const createD1 = async (storageOptions: StorageOptions) => {
28 | const { createSQLiteDB } = await import("@miniflare/shared");
29 | const { D1Database, D1DatabaseAPI } = await import("@miniflare/d1");
30 |
31 | if (storageOptions.type === "memory") {
32 | const sqliteDb = await createSQLiteDB(":memory:");
33 | return new D1Database(new D1DatabaseAPI(sqliteDb));
34 | } else if (storageOptions.type === "file") {
35 | const sqliteDb = await createSQLiteDB(storageOptions.path);
36 | return new D1Database(new D1DatabaseAPI(sqliteDb));
37 | }
38 |
39 | throw new Error("StorageType not found");
40 | };
41 |
42 | export const createR2 = async (storageOptions: StorageOptions) => {
43 | const { R2Bucket } = await import("@miniflare/r2");
44 |
45 | if (storageOptions.type === "memory") {
46 | const { MemoryStorage } = await import("@miniflare/storage-memory");
47 | return new R2Bucket(new MemoryStorage());
48 | } else if (storageOptions.type === "file") {
49 | const { FileStorage } = await import("@miniflare/storage-file");
50 | return new R2Bucket(new FileStorage(storageOptions.path));
51 | }
52 |
53 | throw new Error("StorageType not found");
54 | };
55 |
56 | export const createKV = async (storageOptions: StorageOptions) => {
57 | const { KVNamespace } = await import("@miniflare/kv");
58 |
59 | if (storageOptions.type === "memory") {
60 | const { MemoryStorage } = await import("@miniflare/storage-memory");
61 | return new KVNamespace(new MemoryStorage());
62 | } else if (storageOptions.type === "file") {
63 | const { FileStorage } = await import("@miniflare/storage-file");
64 | return new KVNamespace(new FileStorage(storageOptions.path));
65 | }
66 |
67 | throw new Error("StorageType not found");
68 | };
69 |
70 | export const createDOStorage = async (storageOptions: StorageOptions) => {
71 | const { DurableObjectStorage } = await import("@miniflare/durable-objects");
72 |
73 | if (storageOptions.type === "memory") {
74 | const { MemoryStorage } = await import("@miniflare/storage-memory");
75 | return new DurableObjectStorage(new MemoryStorage());
76 | } else if (storageOptions.type === "file") {
77 | const { FileStorage } = await import("@miniflare/storage-file");
78 | return new DurableObjectStorage(new FileStorage(storageOptions.path));
79 | }
80 |
81 | throw new Error("StorageType not found");
82 | };
83 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createKV, createR2 } from "./lib/server/miniflare";
2 | import { defineMiddleware } from "astro:middleware";
3 |
4 | export const onRequest = defineMiddleware(async (context, next) => {
5 | if (import.meta.env.DEV) {
6 | context.locals.runtime = {
7 | env: {
8 | kv: await createKV({ type: "file", path: ".mf/kv" }),
9 | r2: await createR2({ type: "file", path: ".mf/r2" }),
10 | },
11 | };
12 | }
13 |
14 | return next();
15 | });
16 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "../layouts/Layout.astro";
3 |
4 | export const prerender = true;
5 | ---
6 |
7 |
8 |
9 | Containerflare
10 |
11 | The Globally Distributed, Edge Container Registry
12 |
13 |
14 | Containerflare is 3x faster than DockerHub!
15 | docker run cfcr.dev/hello-world
16 |
17 |
18 |
19 |
20 |
65 |
--------------------------------------------------------------------------------
/src/pages/v2/[...name]/blobs/[reference]/index.ts:
--------------------------------------------------------------------------------
1 | import { errorNoData } from "@utils/response";
2 | import { parseParams } from "@utils/url";
3 |
4 | import type { APIContext } from "astro";
5 |
6 | export async function GET(context: APIContext) {
7 | const { reference, error } = parseParams(context.params);
8 | if (error != null) {
9 | return error;
10 | }
11 |
12 | let response: typeof Response;
13 |
14 | // DB Query
15 | const data = await context.locals?.runtime?.env.r2.get(reference);
16 | if (data == null) {
17 | response = errorNoData();
18 | } else {
19 | response = new Response(data.body);
20 | }
21 |
22 | return response;
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/v2/[...name]/manifests/[reference]/index.ts:
--------------------------------------------------------------------------------
1 | import { errorNoData } from "@utils/response";
2 | import { parseParams } from "@utils/url";
3 | import type { APIContext } from "astro";
4 |
5 | export async function GET(context: APIContext) {
6 | const { name, reference } = parseParams(context.params);
7 |
8 | // DB Query
9 | let dbKey: string;
10 | if (reference.includes("sha256")) {
11 | dbKey = reference;
12 | } else {
13 | dbKey = name + "/" + reference;
14 | }
15 |
16 | // Potential for readable stream and no waiting
17 | const data = await context.locals?.runtime?.env.kv.get(dbKey);
18 | if (!data) {
19 | return errorNoData();
20 | }
21 |
22 | // Set body of response
23 | let body = "";
24 | if (context.request.method !== "HEAD") {
25 | body = data;
26 | }
27 |
28 | const resp = new Response(body);
29 |
30 | // Set docker-content-digest header
31 | let shaTag = reference;
32 | if (!reference.includes("sha256")) {
33 | shaTag = data;
34 | }
35 | resp.headers.set("docker-content-digest", shaTag);
36 |
37 | // Set Content-Type header
38 | let contentType = "";
39 | if (data.startsWith("{")) {
40 | contentType = JSON.parse(data)?.mediaType;
41 | }
42 | if (!contentType) {
43 | contentType = "application/vnd.docker.distribution.manifest.list.v2+json";
44 | }
45 | resp.headers.set("Content-Type", contentType);
46 | resp.headers.set("Content-Length", data.length.toString());
47 |
48 | return resp;
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/v2/seed.ts:
--------------------------------------------------------------------------------
1 | // import type { Env} from "../../../env.d.ts"
2 |
3 | let authHeader: string;
4 |
5 | async function getAuth() {
6 | const resp = await fetch(
7 | "https://auth.docker.io/token?scope=repository%3Alibrary%2Fhello-world%3Apull&service=registry.docker.io",
8 | );
9 | return "Bearer " + ((await resp.json()) as Record).token;
10 | }
11 |
12 | async function seedTag(env: Env, tag: string) {
13 | // Get hello-world:latest
14 | const label_req = await fetch(
15 | "https://registry-1.docker.io/v2/library/hello-world/manifests/latest",
16 | {
17 | method: "HEAD",
18 | headers: {
19 | Authorization: authHeader,
20 | Accept: "application/vnd.docker.distribution.manifest.list.v2+json",
21 | },
22 | },
23 | );
24 |
25 | if (label_req.status != 200) {
26 | console.error("Tag does not exist", label_req.statusText);
27 | return;
28 | }
29 |
30 | console.log(await label_req.text());
31 |
32 | const sha_hash = await label_req.headers.get("docker-content-digest")!;
33 | await env.kv.put(tag, sha_hash);
34 | console.log(sha_hash);
35 | seedOS(env, sha_hash);
36 | }
37 |
38 | async function seedOS(env: Env, hash: string) {
39 | // Get sha256:
40 | const label_req = await fetch(
41 | `https://registry-1.docker.io/v2/library/hello-world/manifests/${hash}`,
42 | {
43 | method: "GET",
44 | headers: {
45 | Authorization: authHeader,
46 | Accept: "application/vnd.docker.distribution.manifest.list.v2+json",
47 | },
48 | },
49 | );
50 |
51 | if (label_req.status != 200) {
52 | console.error("OS list does not exist", label_req.statusText);
53 | return;
54 | }
55 |
56 | const os_list = await label_req.text();
57 | await env.kv.put(hash, os_list);
58 | const os_json = JSON.parse(os_list) as Record;
59 |
60 | for (var key in os_json.manifests) {
61 | seedImage(env, os_json.manifests[key].digest);
62 | }
63 | }
64 |
65 | async function seedImage(env: Env, hash: string) {
66 | // Get sha256:
67 | const label_req = await fetch(
68 | `https://registry-1.docker.io/v2/library/hello-world/manifests/${hash}`,
69 | {
70 | method: "GET",
71 | headers: {
72 | Authorization: authHeader,
73 | Accept: "application/vnd.docker.distribution.manifest.v2+json",
74 | },
75 | },
76 | );
77 |
78 | if (label_req.status != 200) {
79 | console.error("OS list does not exist", label_req.statusText);
80 | return;
81 | }
82 |
83 | const os_list = await label_req.text();
84 | await env.kv.put(hash, os_list);
85 | const image_json = JSON.parse(os_list) as Record;
86 |
87 | seedBlob(env, image_json.config.digest);
88 | for (var key in image_json.layers) {
89 | seedBlob(env, image_json.layers[key].digest);
90 | }
91 | }
92 |
93 | async function seedBlob(env: Env, hash: string) {
94 | const req = await fetch(
95 | `https://registry-1.docker.io/v2/library/hello-world/blobs/${hash}`,
96 | {
97 | headers: {
98 | Authorization: authHeader,
99 | },
100 | },
101 | );
102 |
103 | if (req.status != 200) {
104 | console.error("Seed not 200", req.statusText);
105 | }
106 |
107 | await env.r2.put(hash, await req.arrayBuffer());
108 | }
109 |
110 | export const GET: APIRoute = async (context) => {
111 | // if (!import.meta.env.DEV) {
112 | // return new Response("", { status: 404 });
113 | // }
114 |
115 | console.log("Seeding...");
116 | authHeader = await getAuth();
117 | await seedTag(context.locals?.runtime?.env, "hello-world:latest");
118 | console.log("DONE Seeding");
119 |
120 | return new Response("DONE");
121 | };
122 |
--------------------------------------------------------------------------------
/src/pages/version.ts:
--------------------------------------------------------------------------------
1 | export const prerender = true;
2 |
3 | export const GET: APIRoute = async () => {
4 | // return new Response(import.meta.env.PUBLIC_GIT_SHA);
5 | return new Response("todo!()");
6 | };
7 |
--------------------------------------------------------------------------------
/src/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/types/bindings.d.ts:
--------------------------------------------------------------------------------
1 | import type { KVNamespace, R2Bucket } from "@cloudflare/workers-types";
2 |
3 | export interface Env {
4 | ENVIRONMENT: "production" | "preview" | "development";
5 |
6 | containerFlareKV: KVNamespace;
7 | containerFlareR2: R2Bucket;
8 | }
9 |
10 | export type RequestParams = "name" | "reference";
11 |
--------------------------------------------------------------------------------
/src/utils/response.ts:
--------------------------------------------------------------------------------
1 | const errorNoDataJSON = {
2 | errors: [
3 | {
4 | code: "404",
5 | message: "No Data",
6 | detail: "No Data",
7 | },
8 | ],
9 | };
10 |
11 | function errorNoData(): Response {
12 | return new Response(JSON.stringify(errorNoDataJSON), {
13 | status: 404,
14 | });
15 | }
16 |
17 | export { errorNoData };
18 |
--------------------------------------------------------------------------------
/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | // Regex from https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
2 | const nameRegex = new RegExp(
3 | "^[a-z\\d]+([._-][a-z\\d]+)*(/[a-z\\d]+([._-][a-z\\d]+)*)*$",
4 | );
5 | const referenceRegex = new RegExp("^(sha256:)?(\\w[\\w.-]{0,127})$");
6 |
7 | function parseParams(params: Record): {
8 | name: string;
9 | reference: string;
10 | error: Response | null;
11 | } {
12 | try {
13 | if (!params) throw new Error();
14 | const name = params.name;
15 | const reference = params.reference as string;
16 |
17 | let error = "";
18 | if (!nameRegex.test(name)) {
19 | error += "Name incorrect ";
20 | }
21 | if (!referenceRegex.test(reference)) {
22 | error += "Reference incorrect";
23 | }
24 |
25 | return {
26 | name,
27 | reference,
28 | error:
29 | error.length == 0
30 | ? null
31 | : new Response(
32 | `{"errors": [{
33 | "code": "123",
34 | "message": "Bad Params",
35 | "detail": "${error}"
36 | }]}`,
37 | { status: 404 },
38 | ),
39 | };
40 | } catch (error) {
41 | console.log(error);
42 | return {
43 | name: "",
44 | reference: "",
45 | error: new Response(
46 | `{"errors": [{
47 | "code": "123",
48 | "message": "Bad Params",
49 | "detail": "Bad Params"
50 | }]}`,
51 | { status: 404 },
52 | ),
53 | };
54 | }
55 | }
56 |
57 | export { parseParams };
58 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@utils/*": ["src/utils/*"],
7 | "@types/*": ["src/types/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------