mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 16:30:15 +02:00
Support labels on blueprints
Ref https://github.com/orgs/fosrl/discussions/2849
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
labels,
|
||||
resourceLabels,
|
||||
siteResourceLabels,
|
||||
Transaction
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
|
||||
// Matches the "gray" swatch in the label color palette used by the UI
|
||||
// (src/components/labels-selector.tsx), used as the default for labels
|
||||
// auto-created from a blueprint where no color is specified.
|
||||
const DEFAULT_LABEL_COLOR = "#b4b4b4";
|
||||
|
||||
/**
|
||||
* Looks up labels by name (case-insensitive) within an org, auto-creating
|
||||
* any that don't already exist. Returns the resolved, de-duplicated labelIds.
|
||||
*/
|
||||
export async function getOrCreateLabelIds(
|
||||
orgId: string,
|
||||
labelNames: string[],
|
||||
trx: Transaction
|
||||
): Promise<number[]> {
|
||||
const labelIds = new Set<number>();
|
||||
|
||||
for (const name of labelNames) {
|
||||
let [label] = await trx
|
||||
.select({ labelId: labels.labelId })
|
||||
.from(labels)
|
||||
.where(
|
||||
and(
|
||||
eq(labels.orgId, orgId),
|
||||
sql`LOWER(${labels.name}) = ${name.toLowerCase()}`
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!label) {
|
||||
[label] = await trx
|
||||
.insert(labels)
|
||||
.values({ name, color: DEFAULT_LABEL_COLOR, orgId })
|
||||
.returning({ labelId: labels.labelId });
|
||||
logger.info(
|
||||
`Auto-created label "${name}" in org ${orgId} from blueprint`
|
||||
);
|
||||
}
|
||||
|
||||
labelIds.add(label.labelId);
|
||||
}
|
||||
|
||||
return Array.from(labelIds);
|
||||
}
|
||||
|
||||
export async function syncResourceLabels(
|
||||
resourceId: number,
|
||||
labelIds: number[],
|
||||
trx: Transaction
|
||||
) {
|
||||
await trx
|
||||
.delete(resourceLabels)
|
||||
.where(eq(resourceLabels.resourceId, resourceId));
|
||||
|
||||
if (labelIds.length > 0) {
|
||||
await trx
|
||||
.insert(resourceLabels)
|
||||
.values(labelIds.map((labelId) => ({ resourceId, labelId })));
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncSiteResourceLabels(
|
||||
siteResourceId: number,
|
||||
labelIds: number[],
|
||||
trx: Transaction
|
||||
) {
|
||||
await trx
|
||||
.delete(siteResourceLabels)
|
||||
.where(eq(siteResourceLabels.siteResourceId, siteResourceId));
|
||||
|
||||
if (labelIds.length > 0) {
|
||||
await trx
|
||||
.insert(siteResourceLabels)
|
||||
.values(
|
||||
labelIds.map((labelId) => ({ siteResourceId, labelId }))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { sites } from "@server/db";
|
||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
||||
import { Config } from "./types";
|
||||
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||
import logger from "@server/logger";
|
||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||
import { getNextAvailableAliasAddress } from "../ip";
|
||||
@@ -443,6 +444,13 @@ export async function updatePrivateResources(
|
||||
);
|
||||
}
|
||||
|
||||
const labelIds = await getOrCreateLabelIds(
|
||||
orgId,
|
||||
resourceData.labels,
|
||||
trx
|
||||
);
|
||||
await syncSiteResourceLabels(siteResourceId, labelIds, trx);
|
||||
|
||||
results.push({
|
||||
newSiteResource: updatedResource,
|
||||
oldSiteResource: existingResource,
|
||||
@@ -697,6 +705,13 @@ export async function updatePrivateResources(
|
||||
|
||||
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
||||
|
||||
const labelIds = await getOrCreateLabelIds(
|
||||
orgId,
|
||||
resourceData.labels,
|
||||
trx
|
||||
);
|
||||
await syncSiteResourceLabels(siteResourceId, labelIds, trx);
|
||||
|
||||
results.push({
|
||||
newSiteResource: newResource,
|
||||
newSites: allSites,
|
||||
|
||||
@@ -50,6 +50,7 @@ import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
|
||||
import { tierMatrix } from "../billing/tierMatrix";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
||||
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import next from "next";
|
||||
@@ -1351,6 +1352,15 @@ export async function updatePublicResources(
|
||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||
}
|
||||
|
||||
if (!isTargetsOnlyResource(resourceData)) {
|
||||
const labelIds = await getOrCreateLabelIds(
|
||||
orgId,
|
||||
resourceData.labels || [],
|
||||
trx
|
||||
);
|
||||
await syncResourceLabels(resource.resourceId, labelIds, trx);
|
||||
}
|
||||
|
||||
results.push({
|
||||
proxyResource: resource,
|
||||
targetsToUpdate,
|
||||
|
||||
@@ -225,7 +225,8 @@ export const PublicResourceSchema = z
|
||||
maintenance: MaintenanceSchema.optional(),
|
||||
"auth-daemon": AuthDaemonSchema.optional(),
|
||||
"proxy-protocol": z.boolean().optional(),
|
||||
"proxy-protocol-version": z.int().min(1).optional()
|
||||
"proxy-protocol-version": z.int().min(1).optional(),
|
||||
labels: z.array(z.string().min(1)).optional()
|
||||
})
|
||||
.refine(
|
||||
(resource) => {
|
||||
@@ -493,6 +494,7 @@ export const PrivateResourceSchema = z
|
||||
}),
|
||||
users: z.array(z.string()).optional().default([]),
|
||||
machines: z.array(z.string()).optional().default([]),
|
||||
labels: z.array(z.string().min(1)).optional().default([]),
|
||||
"auth-daemon": AuthDaemonSchema.optional()
|
||||
})
|
||||
.refine(
|
||||
|
||||
Reference in New Issue
Block a user