mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-15 20:46:27 +00:00
Rules, client resources working
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -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[],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user