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 { sites } from "@server/db";
|
||||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
||||||
import { Config } from "./types";
|
import { Config } from "./types";
|
||||||
|
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
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({
|
results.push({
|
||||||
newSiteResource: updatedResource,
|
newSiteResource: updatedResource,
|
||||||
oldSiteResource: existingResource,
|
oldSiteResource: existingResource,
|
||||||
@@ -697,6 +705,13 @@ export async function updatePrivateResources(
|
|||||||
|
|
||||||
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
||||||
|
|
||||||
|
const labelIds = await getOrCreateLabelIds(
|
||||||
|
orgId,
|
||||||
|
resourceData.labels,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
await syncSiteResourceLabels(siteResourceId, labelIds, trx);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
newSiteResource: newResource,
|
newSiteResource: newResource,
|
||||||
newSites: allSites,
|
newSites: allSites,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
|
|||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
||||||
|
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import next from "next";
|
import next from "next";
|
||||||
@@ -1351,6 +1352,15 @@ export async function updatePublicResources(
|
|||||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
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({
|
results.push({
|
||||||
proxyResource: resource,
|
proxyResource: resource,
|
||||||
targetsToUpdate,
|
targetsToUpdate,
|
||||||
|
|||||||
@@ -225,7 +225,8 @@ export const PublicResourceSchema = z
|
|||||||
maintenance: MaintenanceSchema.optional(),
|
maintenance: MaintenanceSchema.optional(),
|
||||||
"auth-daemon": AuthDaemonSchema.optional(),
|
"auth-daemon": AuthDaemonSchema.optional(),
|
||||||
"proxy-protocol": z.boolean().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(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
@@ -493,6 +494,7 @@ export const PrivateResourceSchema = z
|
|||||||
}),
|
}),
|
||||||
users: z.array(z.string()).optional().default([]),
|
users: z.array(z.string()).optional().default([]),
|
||||||
machines: 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()
|
"auth-daemon": AuthDaemonSchema.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
|
|||||||
Reference in New Issue
Block a user