Add source for cloudflare workers

This commit is contained in:
Marc Campbell
2019-05-19 09:31:20 +00:00
parent 92fe08aef1
commit effb57ba72
16 changed files with 3295 additions and 12 deletions

1
,gitignore Normal file
View File

@@ -0,0 +1 @@
deploy

2
cloudflare/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dist/
node_modules

22
cloudflare/Makefile Normal file
View File

@@ -0,0 +1,22 @@
.PHONY: deps
deps:
yarn global add node-gyp
yarn --silent --frozen-lockfile
.PHONY: prebuild
prebuild:
rm -rf build
mkdir -p build
.PHONY: lint
lint:
npx tslint --project ./tsconfig.json --fix
.PHONY: build
build: prebuild
`yarn bin`/webpack
.PHONY: preview
preview: build
`yarn bin`/workers-preview < dist/bundle.js

30
cloudflare/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "replreg-cloudflare-worker",
"version": "0.0.1",
"description": "Cloudflare Workers for replreg.is",
"main": "index.js",
"scripts": {
"test": "make test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/replicatedhq/replreg.git"
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/replicatedhq/replreg/issues"
},
"homepage": "https://github.com/replicatedhq/replreg#readme",
"devDependencies": {
"awesome-typescript-loader": "^5.2.1",
"typescript": "^3.4.5",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2",
"workers-preview": "^1.0.6"
},
"dependencies": {
"moment": "^2.24.0",
"parse-duration": "^0.1.1"
}
}

46
cloudflare/src/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import { tryParsePutTagRequest, handleTagManifestRequest } from "./tag_manifest";
if (typeof addEventListener === 'function') {
addEventListener('fetch', (e: Event): void => {
// work around as strict typescript check doesn't allow e to be of type FetchEvent
const fe = e as FetchEvent
fe.respondWith(proxyRequest(fe.request))
});
}
async function proxyRequest(r: Request): Promise<Response> {
const url = new URL(r.url);
if (!isRegistryRequest(r)) {
return fetch(`https://friendly-goldstine-7897fc.netlify.com/${url.pathname}`);
}
if (isCatalogRequest(r)) {
return new Response(null, {
status: 401,
});
}
const tagManifestParams = await tryParsePutTagRequest(r.method, url);
if (tagManifestParams) {
return handleTagManifestRequest(r, tagManifestParams);
}
return fetch(r);
}
function isRegistryRequest(r: Request): boolean {
const url = new URL(r.url);
return url.pathname.startsWith(`/v2`);
}
function isCatalogRequest(r: Request): boolean {
const url = new URL(r.url);
return url.pathname.endsWith(`v2/_catalog`);
}
interface FetchEvent extends Event {
request: Request;
respondWith(r: Promise<Response> | Response): Promise<Response>;
}

View File

@@ -0,0 +1,73 @@
import { TagManifestParams } from "./";
import * as parseDuration from "parse-duration";
import * as moment from "moment";
export function tryParsePutTagRequest(method: string, url: URL): TagManifestParams | void {
const split = url.pathname.split("/");
// v2/ns/image/manifests/tag <- namespaced images
// v2image/manifests/tag <- library images
if (method !== "PUT") {
return;
}
if (split.length === 6 && split[4] === "manifests") {
const tagManifestParams: TagManifestParams = {
namespace: split[2],
name: split[3],
tag: split[5],
};
return tagManifestParams;
}
if (split.length === 5 && split[3] === "manifests") {
const tagManifestParams: TagManifestParams = {
namespace: "",
name: split[2],
tag: split[4],
};
return tagManifestParams;
}
return;
}
export async function handleTagManifestRequest(r: Request, params: TagManifestParams): Promise<Response> {
r.headers["Accept"] = "application/vnd.docker.distribution.manifest.v2+json";
const upstreamResponse = await fetch(r);
if (upstreamResponse.status != 201) {
return upstreamResponse;
}
let imageName;
if (params.namespace === "") {
imageName = params.name;
} else {
imageName = `${params.namespace}/${params.name}`;
}
const teapotResponse = {
"errors": [
{
"message": `\r \nimage replreg.is/${imageName} is available now and will be automatically deleted ${expirationFromTag(params.tag)}\n\nreplreg is contributed by Replicated (www.replicated.com)`
}
]
};
return new Response(JSON.stringify(teapotResponse), {
status: 418, // This is what works /shrug
headers: {
"Content-Type": "application/json",
},
});
}
function expirationFromTag(tag: string): string {
const parsed = parseDuration(tag);
const now = new Date();
const then = moment(now.getTime() + parsed);
return then.fromNow();
}

View File

@@ -0,0 +1,2 @@
export * from "./tag_manifest";
export * from "./handler";

View File

@@ -0,0 +1,5 @@
export interface TagManifestParams {
namespace: string;
name: string;
tag: string;
}

View File

@@ -0,0 +1,18 @@
import { tryParsePutTagRequest } from "./handler";
import * as chai from "chai";
const testTryParsePutRequest = async () => {
let result = await tryParsePutTagRequest("PUT", new URL("v2/random/more-random/manifests/2h"));
expect(result).to.be.defined();
expect(result.namespace).to.equal("random");
expect(result.name).to.equal("more-random");
expect(result.tag).to.equal("2h");
result = await tryParsePutTagRequest("PUT", new URL("v2/image-name/manifests/2h"));
expect(result).to.be.defined();
expect(result.namespace).to.equal("");
expect(result.name).to.equal("image-name");
expect(result.tag).to.equal("2h");
}

27
cloudflare/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es6", "dom"],
"noImplicitAny": false,
"noEmitOnError": true,
"sourceMap": true,
"preserveConstEnums": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "build",
"pretty": true
},
"include": [
"src/**/*.js",
"src/**/*.ts"
],
"exclude": [
"node_modules",
"src/**/*_test.ts",
"src/test/**/*.ts"
]
}

View File

@@ -0,0 +1,40 @@
const path = require("path")
const webpack = require('webpack');
module.exports = {
entry: {
bundle: path.join(__dirname, "./src/index.ts"),
},
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
mode: process.env.NODE_ENV || "development",
plugins: [
// Ignore all locale files of moment.js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
watchOptions: {
ignored: /node_modules|dist|\.js/g,
},
devtool: "cheap-module-source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
plugins: [],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "awesome-typescript-loader",
},
],
},
}

3013
cloudflare/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,9 @@
FROM node:10 as deps
ADD ./package.json /src/package.json
ADD ./Makefile /src/Makefile
WORKDIR /src
RUN make deps
FROM node:10
ADD . /src
WORKDIR /src
COPY --from=0 /src .
RUN make test
RUN make deps test
ENTRYPOINT ["node"]
CMD ["--no-deprecation", "build/server.js", "hooks"]

View File

@@ -1,13 +1,9 @@
FROM node:10 as deps
ADD ./package.json /src/package.json
ADD ./Makefile /src/Makefile
WORKDIR /src
RUN make deps
FROM node:10
ADD . /src
WORKDIR /src
COPY --from=0 /src .
RUN make test
RUN make deps test
ENTRYPOINT ["node"]
CMD ["--no-deprecation", "build/server.js", "reap"]

View File

@@ -58,9 +58,16 @@ async function main(argv): Promise<any> {
uri: `https://replreg.is/v2/${imageAndTag[0]}/manifests/${imageAndTag[1]}`,
headers,
resolveWithFullResponse: true,
simple: false,
}
const getResponse = await rp(getOptions);
if (getResponse.statusCode == 404) {
await sremAsync("current.images", image);
await delAsync(image);
continue;
}
const deleteURI = `https://replreg.is/v2/${imageAndTag[0]}/manifests/${getResponse.headers.etag.replace(/"/g,"")}`;
// Remove from the registry
@@ -68,6 +75,7 @@ async function main(argv): Promise<any> {
method: "DELETE",
uri: deleteURI,
headers,
resolveWithFullResponse: true,
simple: false,
}

View File

@@ -3,6 +3,10 @@ version: 0.1
log:
level: info
compatibility:
schema1:
enabled: true
storage:
delete:
enabled: true