├── .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 | Containerflare Logo 5 | 6 |
7 | ContainerFlare 8 |
9 |

10 |

11 | Cloudflare container image repository 12 |

13 |

14 | 15 | Last GitHub Commit 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 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------