1.0.35
This commit is contained in:
17 files changed
+284
-203
No files matched your search
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user