Rules, client resources working

This commit is contained in:
Owen
2025-09-14 17:27:21 -07:00
parent 58c04fd196
commit eea0b86d6d
4 changed files with 188 additions and 121 deletions

View File

@@ -1,8 +1,24 @@
resources: client-resources:
resource-nice-id-duce: client-resource-nice-id-uno:
name: this is my resource
protocol: tcp
proxy-port: 3001
hostname: localhost
internal-port: 3000
site: lively-yosemite-toad
client-resource-nice-id-duce:
name: this is my resource
protocol: udp
proxy-port: 3000
hostname: localhost
internal-port: 3000
site: lively-yosemite-toad
proxy-resources:
resource-nice-id-uno:
name: this is my resource name: this is my resource
protocol: http protocol: http
full-domain: level1.test3.example.com full-domain: duce.test.example.com
host-header: example.com host-header: example.com
tls-server-name: example.com tls-server-name: example.com
# auth: # auth:
@@ -18,6 +34,16 @@ resources:
headers: headers:
- X-Example-Header: example-value - X-Example-Header: example-value
- X-Another-Header: another-value - X-Another-Header: another-value
rules:
- action: allow
match: ip
value: 1.1.1.1
- action: deny
match: cidr
value: 2.2.2.2/32
- action: pass
match: path
value: /admin
targets: targets:
- site: lively-yosemite-toad - site: lively-yosemite-toad
path: /path path: /path
@@ -31,7 +57,7 @@ resources:
pathMatchType: exact pathMatchType: exact
method: http method: http
port: 8001 port: 8001
resource-nice-id2: resource-nice-id-duce:
name: this is other resource name: this is other resource
protocol: tcp protocol: tcp
proxy-port: 3000 proxy-port: 3000

View File

@@ -52,10 +52,12 @@ function getContainerPort(container: Container): number | null {
} }
export function processContainerLabels(containers: Container[]): { export function processContainerLabels(containers: Container[]): {
resources: { [key: string]: ResourceConfig }; "proxy-resources": { [key: string]: ResourceConfig };
"client-resources": { [key: string]: ResourceConfig };
} { } {
const result: { resources: { [key: string]: ResourceConfig } } = { const result = {
resources: {} "proxy-resources": {} as { [key: string]: ResourceConfig },
"client-resources": {} as { [key: string]: ResourceConfig }
}; };
// Process each container // Process each container
@@ -64,111 +66,126 @@ export function processContainerLabels(containers: Container[]): {
return; return;
} }
const resourceLabels: DockerLabels = {}; const proxyResourceLabels: DockerLabels = {};
const clientResourceLabels: DockerLabels = {};
// Filter labels that start with "pangolin.proxy-resources." // Filter and separate proxy-resources and client-resources labels
Object.entries(container.labels).forEach(([key, value]) => { Object.entries(container.labels).forEach(([key, value]) => {
if (key.startsWith("pangolin.proxy-resources.") || key.startsWith("pangolin.client-resources.")) { if (key.startsWith("pangolin.proxy-resources.")) {
// remove the pangolin. prefix // remove the pangolin.proxy- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.", ""); const strippedKey = key.replace("pangolin.proxy-", "");
resourceLabels[strippedKey] = value; proxyResourceLabels[strippedKey] = value;
} else if (key.startsWith("pangolin.client-resources.")) {
// remove the pangolin.client- prefix to get "resources.xxx"
const strippedKey = key.replace("pangolin.client-", "");
clientResourceLabels[strippedKey] = value;
} }
}); });
// Skip containers with no resource labels // Process proxy resources
if (Object.keys(resourceLabels).length === 0) { if (Object.keys(proxyResourceLabels).length > 0) {
return; processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]);
} }
// Parse the labels using the existing parseDockerLabels logic // Process client resources
const tempResult: ParsedObject = {}; if (Object.keys(clientResourceLabels).length > 0) {
Object.entries(resourceLabels).forEach(([key, value]) => { processResourceLabels(clientResourceLabels, container, result["client-resources"]);
setNestedProperty(tempResult, key, value);
});
// Merge into main result
if (tempResult.resources) {
Object.entries(tempResult.resources).forEach(
([resourceKey, resourceConfig]: [string, any]) => {
// Initialize resource if it doesn't exist
if (!result.resources[resourceKey]) {
result.resources[resourceKey] = {};
}
// Merge all properties except targets
Object.entries(resourceConfig).forEach(
([propKey, propValue]) => {
if (propKey !== "targets") {
result.resources[resourceKey][propKey] =
propValue;
}
}
);
// Handle targets specially
if (
resourceConfig.targets &&
Array.isArray(resourceConfig.targets)
) {
const resource = result.resources[resourceKey];
if (resource) {
if (!resource.targets) {
resource.targets = [];
}
resourceConfig.targets.forEach(
(target: any, targetIndex: number) => {
// check if the target is an empty object
if (
typeof target === "object" &&
Object.keys(target).length === 0
) {
logger.debug(
`Skipping null target at index ${targetIndex} for resource ${resourceKey}`
);
resource.targets!.push(null);
return;
}
// Ensure targets array is long enough
while (
resource.targets!.length <= targetIndex
) {
resource.targets!.push({});
}
// Set default hostname and port if not provided
const finalTarget = { ...target };
if (!finalTarget.hostname) {
finalTarget.hostname =
container.name ||
container.hostname;
}
if (!finalTarget.port) {
const containerPort =
getContainerPort(container);
if (containerPort !== null) {
finalTarget.port = containerPort;
}
}
// Merge with existing target data
resource.targets![targetIndex] = {
...resource.targets![targetIndex],
...finalTarget
};
}
);
}
}
}
);
} }
}); });
return result; return result;
} }
function processResourceLabels(
resourceLabels: DockerLabels,
container: Container,
targetResult: { [key: string]: ResourceConfig }
) {
// Parse the labels using the existing parseDockerLabels logic
const tempResult: ParsedObject = {};
Object.entries(resourceLabels).forEach(([key, value]) => {
setNestedProperty(tempResult, key, value);
});
// Merge into target result
if (tempResult.resources) {
Object.entries(tempResult.resources).forEach(
([resourceKey, resourceConfig]: [string, any]) => {
// Initialize resource if it doesn't exist
if (!targetResult[resourceKey]) {
targetResult[resourceKey] = {};
}
// Merge all properties except targets
Object.entries(resourceConfig).forEach(
([propKey, propValue]) => {
if (propKey !== "targets") {
targetResult[resourceKey][propKey] = propValue;
}
}
);
// Handle targets specially
if (
resourceConfig.targets &&
Array.isArray(resourceConfig.targets)
) {
const resource = targetResult[resourceKey];
if (resource) {
if (!resource.targets) {
resource.targets = [];
}
resourceConfig.targets.forEach(
(target: any, targetIndex: number) => {
// check if the target is an empty object
if (
typeof target === "object" &&
Object.keys(target).length === 0
) {
logger.debug(
`Skipping null target at index ${targetIndex} for resource ${resourceKey}`
);
resource.targets!.push(null);
return;
}
// Ensure targets array is long enough
while (
resource.targets!.length <= targetIndex
) {
resource.targets!.push({});
}
// Set default hostname and port if not provided
const finalTarget = { ...target };
if (!finalTarget.hostname) {
finalTarget.hostname =
container.name ||
container.hostname;
}
if (!finalTarget.port) {
const containerPort =
getContainerPort(container);
if (containerPort !== null) {
finalTarget.port = containerPort;
}
}
// Merge with existing target data
resource.targets![targetIndex] = {
...resource.targets![targetIndex],
...finalTarget
};
}
);
}
}
}
);
}
}
// // Test example // // Test example
// const testContainers: Container[] = [ // const testContainers: Container[] = [
// { // {

View File

@@ -404,21 +404,31 @@ export async function updateProxyResources(
const existingRule = existingRules[index]; const existingRule = existingRules[index];
if (existingRule) { if (existingRule) {
if ( if (
existingRule.action !== rule.action || existingRule.action !== getRuleAction(rule.action) ||
existingRule.match !== rule.match || existingRule.match !== rule.match.toUpperCase() ||
existingRule.value !== rule.value existingRule.value !== rule.value
) { ) {
validateRule(rule);
await trx await trx
.update(resourceRules) .update(resourceRules)
.set({ .set({
action: rule.action, action: getRuleAction(rule.action),
match: rule.match, match: rule.match.toUpperCase(),
value: rule.value value: rule.value
}) })
.where( .where(
eq(resourceRules.ruleId, existingRule.ruleId) eq(resourceRules.ruleId, existingRule.ruleId)
); );
} }
} else {
validateRule(rule);
await trx.insert(resourceRules).values({
resourceId: existingResource.resourceId,
action: rule.action.toUpperCase(),
match: rule.match.toUpperCase(),
value: rule.value,
priority: index + 1 // start priorities at 1
});
} }
} }
@@ -550,25 +560,11 @@ export async function updateProxyResources(
} }
for (const [index, rule] of resourceData.rules?.entries() || []) { for (const [index, rule] of resourceData.rules?.entries() || []) {
if (rule.match === "cidr") { validateRule(rule);
if (!isValidCIDR(rule.value)) {
throw new Error(`Invalid CIDR provided: ${rule.value}`);
}
} else if (rule.match === "ip") {
if (!isValidIP(rule.value)) {
throw new Error(`Invalid IP provided: ${rule.value}`);
}
} else if (rule.match === "path") {
if (!isValidUrlGlobPattern(rule.value)) {
throw new Error(
`Invalid URL glob pattern: ${rule.value}`
);
}
}
await trx.insert(resourceRules).values({ await trx.insert(resourceRules).values({
resourceId: newResource.resourceId, resourceId: newResource.resourceId,
action: rule.action, action: getRuleAction(rule.action),
match: rule.match, match: rule.match.toUpperCase(),
value: rule.value, value: rule.value,
priority: index + 1 // start priorities at 1 priority: index + 1 // start priorities at 1
}); });
@@ -586,6 +582,34 @@ export async function updateProxyResources(
return results; return results;
} }
function getRuleAction(input: string) {
let action = "DROP";
if (input == "allow") {
action = "ACCEPT";
} else if (input == "deny") {
action = "DROP";
} else if (input == "pass") {
action = "PASS";
}
return action;
}
function validateRule(rule: any) {
if (rule.match === "cidr") {
if (!isValidCIDR(rule.value)) {
throw new Error(`Invalid CIDR provided: ${rule.value}`);
}
} else if (rule.match === "ip") {
if (!isValidIP(rule.value)) {
throw new Error(`Invalid IP provided: ${rule.value}`);
}
} else if (rule.match === "path") {
if (!isValidUrlGlobPattern(rule.value)) {
throw new Error(`Invalid URL glob pattern: ${rule.value}`);
}
}
}
async function syncRoleResources( async function syncRoleResources(
resourceId: number, resourceId: number,
ssoRoles: string[], ssoRoles: string[],

View File

@@ -173,7 +173,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
export const ClientResourceSchema = z.object({ export const ClientResourceSchema = z.object({
name: z.string().min(2).max(100), name: z.string().min(2).max(100),
site: z.string().min(2).max(100), site: z.string().min(2).max(100).optional(),
protocol: z.enum(["tcp", "udp"]), protocol: z.enum(["tcp", "udp"]),
"proxy-port": z.number().min(1).max(65535), "proxy-port": z.number().min(1).max(65535),
"hostname": z.string().min(1).max(255), "hostname": z.string().min(1).max(255),