Skip to content

Commit 1b0a0de

Browse files
committed
Add github webhook and deploy on call
1 parent c8520dc commit 1b0a0de

File tree

8 files changed

+172
-49
lines changed

8 files changed

+172
-49
lines changed

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@deploy-cat/heroicons-solid": "^2.1.1",
1818
"@kubernetes/client-node": "^0.18.1",
1919
"@octokit/rest": "^21.0.2",
20+
"@octokit/webhooks": "^13.3.0",
2021
"@prisma/client": "^5.19.1",
2122
"@solid-mediakit/auth": "^2.1.3",
2223
"@solidjs/router": "^0.13.1",

src/components/cloud/CreateServiceForm.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type { Service } from "~/lib/knative";
99
import { toNumber } from "~/lib/knative";
1010
import { SourceInput } from "./service/SourceInput";
1111
import { k8sCore } from "~/lib/k8s";
12+
import { Octokit } from "@octokit/rest";
13+
import * as crypto from "crypto";
14+
import { config } from "~/lib/config";
1215

1316
const ensureGithubPullSecret = async (namespace: string) => {
1417
"use server";
@@ -51,6 +54,25 @@ const ensureGithubPullSecret = async (namespace: string) => {
5154
}
5255
};
5356

57+
const createGithubWebhook = async (namespace: string, service) => {
58+
"use server";
59+
const account = await getAccount();
60+
const ok = new Octokit({
61+
auth: account.access_token,
62+
});
63+
ok.repos.createWebhook({
64+
owner: service.ghPackageOwner,
65+
repo: service.ghPackageRepo,
66+
config: {
67+
url: `${config?.publicurl}/api/webhooks/github/apps/${namespace}/${service.name}/package`,
68+
content_type: "json",
69+
secret: service.webhookSecret,
70+
},
71+
events: ["package"],
72+
active: true,
73+
});
74+
};
75+
5476
const createServiceFromForm = async (form: FormData) => {
5577
"use server";
5678
const service = {
@@ -61,6 +83,7 @@ const createServiceFromForm = async (form: FormData) => {
6183
ghPackageTag: form.get("ghPackageTag") as string,
6284
ghPackageName: form.get("ghPackageName") as string,
6385
ghPackageOwner: form.get("ghPackageOwner") as string,
86+
ghPackageRepo: form.get("ghPackageRepo") as string,
6487
port: Number(form.get("port")) as number,
6588
resources: {
6689
cpuLimit: toNumber(form.get("cpuLimit")),
@@ -72,18 +95,21 @@ const createServiceFromForm = async (form: FormData) => {
7295
},
7396
envVars: JSON.parse(form.get("env") as string) as { [key: string]: string },
7497
} as Service;
98+
7599
const user = await getUser();
76100
if (service?.source === "ghcr") {
77101
try {
102+
service.webhookSecret = crypto.randomBytes(16).toString("hex");
78103
await ensureGithubPullSecret(user.name);
104+
await createGithubWebhook(user.name, service);
79105
} catch (e) {
80106
console.error(e);
81107
}
82108
service.image = `ghcr.io/${service.ghPackageOwner}/${service.ghPackageName}:${service.ghPackageTag}`;
83109
service.pullSecret = "pull-secret-ghcr";
84110
}
85111
try {
86-
await knative.createService(service, user.name);
112+
await knative.createService(service, user.name, service.source);
87113
} catch (e) {
88114
console.error(e);
89115
}

src/components/cloud/service/SourceInput.tsx

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSignal, Show, For } from "solid-js";
1+
import { createSignal, Show, For, createEffect, Accessor } from "solid-js";
22
import { cache, createAsync } from "@solidjs/router";
33
import { getAccount } from "~/lib/auth";
44
import { Octokit } from "@octokit/rest";
@@ -14,7 +14,7 @@ const getPackages = cache(async () => {
1414
await ok.packages.listPackagesForAuthenticatedUser({
1515
package_type: "container",
1616
});
17-
const packagesWithVersions = Promise.all(
17+
const packagesWithVersions = await Promise.all(
1818
packages.map(async (pkg) => {
1919
const { data: versions } =
2020
await ok.packages.getAllPackageVersionsForPackageOwnedByAuthenticatedUser(
@@ -48,7 +48,14 @@ const sourceOptions = [
4848
export const SourceInput = ({ data }) => {
4949
const [source, setSource] = createSignal("manual" as Source);
5050
const packages = createAsync(() => getPackages());
51-
const [pkg, setPkg] = createSignal(null as null | number);
51+
type Package = typeof packages extends Accessor<(infer U)[] | undefined>
52+
? U
53+
: never;
54+
const [pkg, setPkg] = createSignal(undefined as Package | undefined);
55+
56+
createEffect(() => {
57+
setPkg(packages()?.[0]);
58+
});
5259

5360
return (
5461
<>
@@ -87,39 +94,55 @@ export const SourceInput = ({ data }) => {
8794
</label>
8895
</Show>
8996
<Show when={source() === "ghcr"}>
90-
<label class="form-control w-full">
91-
<div class="label">
92-
<span class="label-text">Container Image</span>
93-
</div>
94-
<select
95-
name="ghPackage"
96-
class="select select-bordered w-full"
97-
onchange={(e) => setPkg(Number(e.target.value))}
98-
>
99-
<For each={packages()}>
100-
{(p) => (
101-
<option value={p.id}>
102-
{p.name} ({p.repository?.full_name})
103-
</option>
104-
)}
105-
</For>
106-
</select>
107-
<input
108-
type="hidden"
109-
name="ghPackageName"
110-
value={packages()?.find((p) => p.id === pkg())?.name}
111-
/>
112-
<input
113-
type="hidden"
114-
name="ghPackageOwner"
115-
value={packages()?.find((p) => p.id === pkg())?.owner?.login}
116-
/>
117-
<select name="ghPackageTag" class="select select-bordered w-full">
118-
<For each={packages()?.find((p) => p.id === pkg())?.tags}>
119-
{(t) => <option value={t}>{t}</option>}
120-
</For>
121-
</select>
122-
</label>
97+
<Show when={packages()}>
98+
{(pkgs) => (
99+
<>
100+
<label class="form-control w-full">
101+
<div class="label">
102+
<span class="label-text">Container Image</span>
103+
</div>
104+
<div class="join">
105+
<select
106+
name="ghPackage"
107+
class="select select-bordered w-full join-item"
108+
onInput={(e) =>
109+
setPkg(
110+
pkgs().find((p) => p.id === Number(e.target.value))
111+
)
112+
}
113+
>
114+
<For each={pkgs()}>
115+
{(p) => (
116+
<option value={p.id}>
117+
{p.name} ({p.repository?.full_name})
118+
</option>
119+
)}
120+
</For>
121+
</select>
122+
<select
123+
name="ghPackageTag"
124+
class="select select-bordered join-item"
125+
>
126+
<For each={pkg()?.tags}>
127+
{(t) => <option value={t}>{t}</option>}
128+
</For>
129+
</select>
130+
</div>
131+
</label>
132+
<input type="hidden" name="ghPackageName" value={pkg()?.name} />
133+
<input
134+
type="hidden"
135+
name="ghPackageOwner"
136+
value={pkg()?.owner?.login}
137+
/>
138+
<input
139+
type="hidden"
140+
name="ghPackageRepo"
141+
value={pkg()?.repository?.name}
142+
/>
143+
</>
144+
)}
145+
</Show>
123146
</Show>
124147
</>
125148
);

src/lib/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isServer } from "solid-js/web";
66
const configPath = process.cwd?.() && `${process.cwd()}/config.json`;
77

88
const schemaConfig = z.object({
9+
publicurl: z.string(),
910
database: z.object({
1011
url: z.string(),
1112
}),

src/lib/knative.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ const toKnService = (service: any) =>
2525
port: service.spec.template.spec.containers[0].ports[0].containerPort,
2626
resources: {
2727
cpuLimit: toNumber(
28-
service.spec.template.spec.containers[0].resources.limits?.cpu.replace(
28+
service.spec.template.spec.containers[0].resources.limits?.cpu?.replace(
2929
"m",
3030
""
3131
)
3232
),
3333
memoryLimit: toNumber(
34-
service.spec.template.spec.containers[0].resources.limits?.memory.replace(
34+
service.spec.template.spec.containers[0].resources.limits?.memory?.replace(
3535
"Mi",
3636
""
3737
)
@@ -106,7 +106,12 @@ export class Knative {
106106
return body.items;
107107
}
108108

109-
async createService(service: Service, namespace: string) {
109+
async createService(
110+
service: Service,
111+
namespace: string,
112+
source: string = "manual",
113+
revisionSource: string = "manual"
114+
) {
110115
const { body } = await this.customObjectsApi.createNamespacedCustomObject(
111116
"serving.knative.dev",
112117
"v1",
@@ -120,6 +125,10 @@ export class Knative {
120125
labels: {
121126
"app.kubernetes.io/managed-by": "deploycat",
122127
},
128+
annotations: {
129+
"apps.deploycat.io/webhook-secret": service.webhookSecret,
130+
"apps.deploycat.io/source": source,
131+
},
123132
},
124133
spec: {
125134
template: {
@@ -133,9 +142,7 @@ export class Knative {
133142
service.scaling.maxRequests &&
134143
service.scaling.maxRequests.toString(),
135144
"autoscaling.knative.dev/metric": "rps",
136-
},
137-
labels: {
138-
"apps.deploycat.io/source": "manual",
145+
"apps.deploycat.io/source": revisionSource,
139146
},
140147
},
141148
spec: {
@@ -189,7 +196,12 @@ export class Knative {
189196
return body;
190197
}
191198

192-
async updateService(name: string, service: Service, namespace: string) {
199+
async updateService(
200+
name: string,
201+
service: Service,
202+
namespace: string,
203+
revisionSource: string = "manual"
204+
) {
193205
const currentService = await this.getService(name, namespace);
194206

195207
const { body } = await this.customObjectsApi.replaceNamespacedCustomObject(
@@ -224,12 +236,13 @@ export class Knative {
224236
service.scaling.maxRequests &&
225237
service.scaling.maxRequests.toString(),
226238
"autoscaling.knative.dev/metric": "rps",
227-
},
228-
labels: {
229-
"apps.deploycat.io/source": "manual",
239+
"apps.deploycat.io/updated-at": new Date().toISOString(), // force new revision
240+
"apps.deploycat.io/source": revisionSource,
230241
},
231242
},
232243
spec: {
244+
imagePullSecrets:
245+
currentService.raw.spec.template.spec.imagePullSecrets,
233246
containerConcurrency: 0,
234247
containers: [
235248
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { APIEvent } from "@solidjs/start/server";
2+
import { knative } from "~/lib/k8s";
3+
import { Webhooks } from "@octokit/webhooks";
4+
5+
export async function POST({ params, request }: APIEvent) {
6+
const { namespace, app } = params;
7+
const body = await request.text();
8+
9+
const currentService = await knative.getService(app, namespace);
10+
const secret =
11+
currentService.raw.metadata.annotations["apps.deploycat.io/webhook-secret"];
12+
13+
const wh = new Webhooks({ secret });
14+
const signature = request.headers.get("x-hub-signature-256");
15+
if (!secret || !signature || !(await wh.verify(body, signature))) {
16+
return new Response("Not Authorized", { status: 403 });
17+
}
18+
19+
const data = JSON.parse(body);
20+
if (
21+
data.action === "published" &&
22+
data.package?.package_version?.package_url === currentService.image
23+
) {
24+
await knative.updateService(app, currentService, namespace, "ghcr");
25+
return new Response("Updated", { status: 200 });
26+
}
27+
return new Response("Not Found", { status: 404 });
28+
}

src/routes/cloud/apps/[app]/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ export default () => {
108108
%
109109
</td>
110110
<td class="hidden sm:table-cell">
111-
{revision.metadata.labels["apps.deploycat.io/source"] ??
112-
"unknown"}
111+
{revision.metadata.annotations[
112+
"apps.deploycat.io/source"
113+
] ?? "unknown"}
113114
</td>
114115
</tr>
115116
)}

0 commit comments

Comments
 (0)