This commit is contained in:
jarek committed 2026-06-19 08:21:22 +02:00
1 parent efb634701c
commit ce12d7ae9f
17 files changed
+284 -203

No files matched your search

+26 -16
View File
@@ -2,16 +2,20 @@
* Pangolin label → public URL extraction (#2 follow-up).
*
* Pangolin Blueprints (https://docs.pangolin.net/manage/blueprints) annotates
* a container with one or more proxy resources. The relevant labels for URL
* extraction are:
* a container with one or more resources, scoped public OR private. The
* relevant labels for URL extraction are:
*
* pangolin.proxy-resources.<name>.name human-friendly label
* pangolin.proxy-resources.<name>.full-domain public hostname (mandatory)
* pangolin.proxy-resources.<name>.protocol http | https (defaults to https)
* pangolin.public-resources.<name>.name human-friendly label
* pangolin.public-resources.<name>.full-domain public hostname (mandatory)
* pangolin.public-resources.<name>.protocol http | https (defaults to https)
* pangolin.private-resources.<name>.{name,full-domain,protocol} private equivalents
*
* Both scopes resolve to a URL the user can click. The scope (`public`/
* `private`) is preserved on the result so callers can label or filter.
*
* The `targets[N].port` family is intentionally ignored — Pangolin terminates
* the public connection at full-domain; the internal target port is not part
* of the URL a user sees.
* the connection at full-domain; the internal target port is not part of the
* URL a user sees.
*
* Returns one URL per resource that declares a full-domain. Multiple
* resources on the same container yield multiple URLs. Identical URLs across
@@ -24,32 +28,37 @@ export interface PangolinUrl {
url: string;
/** The Pangolin resource key (the `<name>` in the label key). */
resource: string;
/** Scope from the label namespace — public or private. */
scope: 'public' | 'private';
/** Optional human-friendly name from the `.name` label, if set. */
displayName?: string;
}
const RESOURCE_KEY_RE =
/^pangolin\.proxy-resources\.([^.]+)\.(full-domain|protocol|name)$/;
/^pangolin\.(public|private)-resources\.([^.]+)\.(full-domain|protocol|name)$/;
export function extractPangolinUrls(
labels: Record<string, string> | undefined | null
): PangolinUrl[] {
if (!labels) return [];
// Group label values by resource key.
// Group label values by (scope, resource) key. A single resource name can
// legitimately appear under both scopes (rare but allowed by Pangolin);
// they're treated as independent resources here.
const byResource = new Map<
string,
{ fullDomain?: string; protocol?: string; name?: string }
{ scope: 'public' | 'private'; resource: string; fullDomain?: string; protocol?: string; name?: string }
>();
for (const [key, value] of Object.entries(labels)) {
const m = key.match(RESOURCE_KEY_RE);
if (!m) continue;
const [, resource, field] = m;
let entry = byResource.get(resource);
const [, scope, resource, field] = m as unknown as [string, 'public' | 'private', string, string];
const groupKey = `${scope}:${resource}`;
let entry = byResource.get(groupKey);
if (!entry) {
entry = {};
byResource.set(resource, entry);
entry = { scope, resource };
byResource.set(groupKey, entry);
}
const v = (value ?? '').trim();
if (!v) continue;
@@ -61,7 +70,7 @@ export function extractPangolinUrls(
const out: PangolinUrl[] = [];
const seen = new Set<string>();
for (const [resource, entry] of byResource) {
for (const entry of byResource.values()) {
if (!entry.fullDomain) continue;
const proto =
@@ -75,7 +84,8 @@ export function extractPangolinUrls(
out.push({
url,
resource,
resource: entry.resource,
scope: entry.scope,
displayName: entry.name
});
}
+12 -4
View File
@@ -135,16 +135,24 @@ export async function detectShells(
/**
* Get the best available shell from the detection result
* Returns the user's preferred shell if available, otherwise the default
*
* Preference matching is by shell name (basename), not full path — so a
* preference of `/bin/bash` will still match when the server resolved bash
* to a non-standard path like `/usr/bin/bash` (issue #1189).
*/
export function getBestShell(
result: ShellDetectionResult,
preferredShell: string
): string | null {
// If preferred shell is available, use it
if (result.shells.includes(preferredShell)) {
return preferredShell;
const exact = result.shells.find(s => s === preferredShell);
if (exact) return exact;
const preferredName = preferredShell.split('/').pop();
if (preferredName) {
const byName = result.shells.find(s => s.split('/').pop() === preferredName);
if (byName) return byName;
}
// Otherwise use the default shell
return result.defaultShell;
}