Skip to content

Commit bc79fee

Browse files
Perform a dry-run Ingress create to verify subdomain does not exist (#27)
1 parent dd5db48 commit bc79fee

2 files changed

Lines changed: 54 additions & 8 deletions

File tree

backend/src/service/helper/deploymentConfig.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
RepositoryNotFoundError,
2727
ValidationError,
2828
} from "../common/errors.ts";
29+
import { canCreateIngress } from "../isSubdomainAvailable.ts";
2930

3031
type GitWorkloadConfig = components["schemas"]["WorkloadConfigOptions"] & {
3132
source: "git";
@@ -364,8 +365,16 @@ export class DeploymentConfigService {
364365
}
365366

366367
const appWithSubdomain = await this.appRepo.getAppBySubdomain(subdomain);
367-
if (appWithSubdomain && appWithSubdomain.id !== existingAppId) {
368-
throw new ValidationError("Subdomain is in use");
368+
if (appWithSubdomain) {
369+
if (appWithSubdomain.id !== existingAppId) {
370+
throw new ValidationError(
371+
"Subdomain is in use by another AnvilOps app",
372+
);
373+
}
374+
} else {
375+
if (!(await canCreateIngress(subdomain))) {
376+
throw new ValidationError("Subdomain is in use");
377+
}
369378
}
370379
}
371380
}
Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,51 @@
1+
import { ApiException } from "@kubernetes/client-node";
12
import { db } from "../db/index.ts";
3+
import { svcK8s } from "../lib/cluster/kubernetes.ts";
4+
import { createIngressConfig } from "../lib/cluster/resources/ingress.ts";
5+
import { env } from "../lib/env.ts";
6+
import { isRFC1123 } from "../lib/validate.ts";
27
import { ValidationError } from "./common/errors.ts";
38

49
export async function isSubdomainAvailable(subdomain: string) {
5-
if (
6-
subdomain.length > 54 ||
7-
subdomain.match(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/) === null
8-
) {
10+
if (!isRFC1123(subdomain)) {
911
throw new ValidationError("Invalid subdomain.");
1012
}
1113

12-
const appUsingSubdomain = await db.app.getAppBySubdomain(subdomain);
13-
return appUsingSubdomain === null;
14+
const [appUsingSubdomain, ingressDryRun] = await Promise.all([
15+
db.app.getAppBySubdomain(subdomain),
16+
canCreateIngress(subdomain),
17+
]);
18+
19+
return appUsingSubdomain === null && ingressDryRun;
20+
}
21+
22+
/**
23+
* Does a dry-run of creating an Ingress with the specified subdomain.
24+
* @returns true if the dry-run succeeded, or false if it failed due to a request error (4xx), which indicates that the subdomain is probably taken.
25+
*/
26+
export async function canCreateIngress(subdomain: string) {
27+
const config = createIngressConfig({
28+
createIngress: true,
29+
name: "anvilops-ingress-probe",
30+
namespace: env.CURRENT_NAMESPACE,
31+
port: 80,
32+
serviceName: "anvilops-ingress-probe",
33+
subdomain: subdomain,
34+
servicePort: 80,
35+
});
36+
37+
try {
38+
await svcK8s["KubernetesObjectApi"].create(
39+
config,
40+
undefined,
41+
/* dryRun = */ "All",
42+
);
43+
return true;
44+
} catch (err) {
45+
if (err instanceof ApiException && err.code >= 400 && err.code < 500) {
46+
// The dry-run failed. This is probably due to an existing Ingress using the subdomain and path that we want to reserve.
47+
return false;
48+
}
49+
throw err;
50+
}
1451
}

0 commit comments

Comments
 (0)