mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-15 12:49:52 +00:00
Merge branch 'dev' into resource-policies
This commit is contained in:
@@ -361,7 +361,7 @@ export async function updateClientResources(
|
||||
} else {
|
||||
let aliasAddress: string | null = null;
|
||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
|
||||
}
|
||||
|
||||
let domainInfo:
|
||||
|
||||
@@ -1871,7 +1871,11 @@ async function getDomainId(
|
||||
return null;
|
||||
}
|
||||
|
||||
const domainSelection = validDomains[0].domains;
|
||||
// Pick the most specific (longest baseDomain) valid domain so that, e.g.,
|
||||
// *.test.dev.example.com is assigned to *.dev.example.com rather than *.example.com.
|
||||
const domainSelection = validDomains.sort(
|
||||
(a, b) => b.domains.baseDomain.length - a.domains.baseDomain.length
|
||||
)[0].domains;
|
||||
const baseDomain = domainSelection.baseDomain;
|
||||
|
||||
// Wildcard full-domains are not allowed on namespace (provided/free) domains
|
||||
|
||||
@@ -25,9 +25,9 @@ import { tierMatrix } from "./billing/tierMatrix";
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string,
|
||||
trx?: Transaction
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<void> {
|
||||
const execute = async (transaction: Transaction) => {
|
||||
const execute = async (transaction: Transaction | typeof db) => {
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<
|
||||
string,
|
||||
@@ -437,7 +437,7 @@ export async function calculateUserClientsForOrgs(
|
||||
|
||||
async function cleanupOrphanedClients(
|
||||
userId: string,
|
||||
trx: Transaction,
|
||||
trx: Transaction | typeof db,
|
||||
userOrgIds: string[] = []
|
||||
): Promise<void> {
|
||||
// Find all OLM clients for this user that should be deleted
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.18.2";
|
||||
export const APP_VERSION = "1.18.4";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
212
server/lib/ip.ts
212
server/lib/ip.ts
@@ -6,6 +6,7 @@ import z from "zod";
|
||||
import logger from "@server/logger";
|
||||
import semver from "semver";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
|
||||
interface IPRange {
|
||||
start: bigint;
|
||||
@@ -327,120 +328,146 @@ export async function getNextAvailableClientSubnet(
|
||||
orgId: string,
|
||||
transaction: Transaction | typeof db = db
|
||||
): Promise<string> {
|
||||
const [org] = await transaction
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
return await lockManager.withLock(
|
||||
`client-subnet-allocation:${orgId}`,
|
||||
async () => {
|
||||
const [org] = await transaction
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
|
||||
if (!org.subnet) {
|
||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||
}
|
||||
if (!org.subnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no subnet defined`
|
||||
);
|
||||
}
|
||||
|
||||
const existingAddressesSites = await transaction
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||
const existingAddressesSites = await transaction
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||
|
||||
const existingAddressesClients = await transaction
|
||||
.select({
|
||||
address: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
|
||||
const existingAddressesClients = await transaction
|
||||
.select({
|
||||
address: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(
|
||||
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
|
||||
);
|
||||
|
||||
const addresses = [
|
||||
...existingAddressesSites.map(
|
||||
(site) => `${site.address?.split("/")[0]}/32`
|
||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||
...existingAddressesClients.map(
|
||||
(client) => `${client.address.split("/")}/32`
|
||||
)
|
||||
].filter((address) => address !== null) as string[];
|
||||
const addresses = [
|
||||
...existingAddressesSites.map(
|
||||
(site) => `${site.address?.split("/")[0]}/32`
|
||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||
...existingAddressesClients.map(
|
||||
(client) => `${client.address.split("/")}/32`
|
||||
)
|
||||
].filter((address) => address !== null) as string[];
|
||||
|
||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
return subnet;
|
||||
return subnet;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNextAvailableAliasAddress(
|
||||
orgId: string
|
||||
orgId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<string> {
|
||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||
return await lockManager.withLock(
|
||||
`alias-address-allocation:${orgId}`,
|
||||
async () => {
|
||||
const [org] = await trx
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
}
|
||||
|
||||
if (!org.subnet) {
|
||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||
}
|
||||
if (!org.subnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no subnet defined`
|
||||
);
|
||||
}
|
||||
|
||||
if (!org.utilitySubnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no utility subnet defined`
|
||||
);
|
||||
}
|
||||
if (!org.utilitySubnet) {
|
||||
throw new Error(
|
||||
`Organization with ID ${orgId} has no utility subnet defined`
|
||||
);
|
||||
}
|
||||
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
isNotNull(siteResources.aliasAddress),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
const existingAddresses = await trx
|
||||
.select({
|
||||
aliasAddress: siteResources.aliasAddress
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
isNotNull(siteResources.aliasAddress),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
const addresses = [
|
||||
...existingAddresses.map(
|
||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||
),
|
||||
// reserve a /29 for the dns server and other stuff
|
||||
`${org.utilitySubnet.split("/")[0]}/29`
|
||||
].filter((address) => address !== null) as string[];
|
||||
const addresses = [
|
||||
...existingAddresses.map(
|
||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||
),
|
||||
// reserve a /29 for the dns server and other stuff
|
||||
`${org.utilitySubnet.split("/")[0]}/29`
|
||||
].filter((address) => address !== null) as string[];
|
||||
|
||||
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
let subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
32,
|
||||
org.utilitySubnet
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
// remove the cidr
|
||||
subnet = subnet.split("/")[0];
|
||||
// remove the cidr
|
||||
subnet = subnet.split("/")[0];
|
||||
|
||||
return subnet;
|
||||
return subnet;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
subnet: orgs.subnet
|
||||
})
|
||||
.from(orgs)
|
||||
.where(isNotNull(orgs.subnet));
|
||||
return await lockManager.withLock("org-subnet-allocation", async () => {
|
||||
const existingAddresses = await db
|
||||
.select({
|
||||
subnet: orgs.subnet
|
||||
})
|
||||
.from(orgs)
|
||||
.where(isNotNull(orgs.subnet));
|
||||
|
||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||
|
||||
const subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().orgs.block_size,
|
||||
config.getRawConfig().orgs.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
const subnet = findNextAvailableCidr(
|
||||
addresses,
|
||||
config.getRawConfig().orgs.block_size,
|
||||
config.getRawConfig().orgs.subnet_group
|
||||
);
|
||||
if (!subnet) {
|
||||
throw new Error("No available subnets remaining in space");
|
||||
}
|
||||
|
||||
return subnet;
|
||||
return subnet;
|
||||
});
|
||||
}
|
||||
|
||||
export function generateRemoteSubnets(
|
||||
@@ -478,7 +505,12 @@ export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||
|
||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||
return allSiteResources
|
||||
.filter((sr) => sr.aliasAddress && ((sr.alias && sr.mode == "host") || (sr.fullDomain && sr.mode == "http")))
|
||||
.filter(
|
||||
(sr) =>
|
||||
sr.aliasAddress &&
|
||||
((sr.alias && sr.mode == "host") ||
|
||||
(sr.fullDomain && sr.mode == "http"))
|
||||
)
|
||||
.map((sr) => ({
|
||||
alias: sr.alias || sr.fullDomain,
|
||||
aliasAddress: sr.aliasAddress
|
||||
|
||||
@@ -24,8 +24,11 @@ export async function getCachedStatusHistory(
|
||||
return cached;
|
||||
}
|
||||
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const startSec = nowSec - days * 86400;
|
||||
// Anchor to UTC midnight so the query window aligns with stable calendar days
|
||||
const utcToday = new Date();
|
||||
utcToday.setUTCHours(0, 0, 0, 0);
|
||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
||||
const startSec = todayMidnightSec - days * 86400;
|
||||
|
||||
const events = await logsDb
|
||||
.select()
|
||||
@@ -110,11 +113,18 @@ export function computeBuckets(
|
||||
days: number
|
||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
|
||||
// and don't drift as the cache expires and is recomputed
|
||||
const utcToday = new Date();
|
||||
utcToday.setUTCHours(0, 0, 0, 0);
|
||||
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
|
||||
|
||||
const buckets: StatusHistoryDayBucket[] = [];
|
||||
let totalDowntime = 0;
|
||||
|
||||
for (let d = 0; d < days; d++) {
|
||||
const dayStartSec = nowSec - (days - d) * 86400;
|
||||
const dayStartSec = todayMidnightSec - (days - 1 - d) * 86400;
|
||||
const dayEndSec = dayStartSec + 86400;
|
||||
|
||||
const dayEvents = events.filter(
|
||||
|
||||
Reference in New Issue
Block a user