feat: resolve app template env placeholders

This commit is contained in:
2026-04-28 14:28:01 +00:00
parent 061ce7c3f2
commit 49c1830168
3 changed files with 65 additions and 4 deletions
+31 -2
View File
@@ -112,9 +112,15 @@ export class OneboxServicesManager {
// Merge platform env vars with user-specified env vars (user vars take precedence) // Merge platform env vars with user-specified env vars (user vars take precedence)
const mergedEnvVars = { ...platformEnvVars, ...(options.envVars || {}) }; const mergedEnvVars = { ...platformEnvVars, ...(options.envVars || {}) };
this.resolveEnvVarTemplates(mergedEnvVars, {
...platformEnvVars,
SERVICE_NAME: options.name,
SERVICE_DOMAIN: options.domain || '',
SERVICE_PORT: String(options.port),
});
// Update service with merged env vars // Update service with merged and resolved env vars.
if (Object.keys(platformEnvVars).length > 0) { if (Object.keys(mergedEnvVars).length > 0) {
this.database.updateService(service.id!, { envVars: mergedEnvVars }); this.database.updateService(service.id!, { envVars: mergedEnvVars });
} }
@@ -697,6 +703,29 @@ export class OneboxServicesManager {
} }
} }
private resolveEnvVarTemplates(
envVarsArg: Record<string, string>,
valuesArg: Record<string, string>,
): void {
for (const [key, value] of Object.entries(envVarsArg)) {
const missingValues = new Set<string>();
const resolvedValue = value.replace(/\$\{([A-Z0-9_]+)\}/g, (match, placeholderName) => {
const replacement = valuesArg[placeholderName];
if (replacement === undefined || replacement === '') {
missingValues.add(placeholderName);
return match;
}
return replacement;
});
if (missingValues.size > 0) {
throw new Error(
`Missing template value(s) for ${key}: ${Array.from(missingValues).join(', ')}`,
);
}
envVarsArg[key] = resolvedValue;
}
}
/** /**
* Start auto-update monitoring for registry services * Start auto-update monitoring for registry services
* Polls every 30 seconds for digest changes and restarts services if needed * Polls every 30 seconds for digest changes and restarts services if needed
+1 -1
View File
File diff suppressed because one or more lines are too long
+33 -1
View File
@@ -42,6 +42,9 @@ export class ObViewAppStore extends DeesElement {
@state() @state()
accessor serviceName: string = ''; accessor serviceName: string = '';
@state()
accessor serviceDomain: string = '';
@state() @state()
accessor loading: boolean = false; accessor loading: boolean = false;
@@ -474,6 +477,18 @@ export class ObViewAppStore extends DeesElement {
Lowercase letters, numbers, and hyphens only. Lowercase letters, numbers, and hyphens only.
</div> </div>
<div class="section-label" style="margin-top: 18px;">Domain</div>
<input
class="name-input"
type="text"
.value=${this.serviceDomain}
placeholder="e.g. cloudly.example.com"
@input=${(e: Event) => this.handleServiceDomainChange((e.target as HTMLInputElement).value)}
/>
<div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 6px;">
Optional. When configured, Onebox routes this domain to the deployed app.
</div>
<div class="actions-row"> <div class="actions-row">
<button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>Cancel</button> <button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>Cancel</button>
<button class="btn btn-primary" @click=${() => this.handleDeploy()}> <button class="btn btn-primary" @click=${() => this.handleDeploy()}>
@@ -560,6 +575,7 @@ export class ObViewAppStore extends DeesElement {
required: ev.required, required: ev.required,
platformInjected: ev.value?.includes('${') || false, platformInjected: ev.value?.includes('${') || false,
})); }));
this.serviceDomain = '';
} catch (err) { } catch (err) {
console.error('Failed to fetch app config:', err); console.error('Failed to fetch app config:', err);
} }
@@ -571,14 +587,29 @@ export class ObViewAppStore extends DeesElement {
this.editableEnvVars = updated; this.editableEnvVars = updated;
} }
private handleServiceDomainChange(valueArg: string) {
this.serviceDomain = this.normalizeDomain(valueArg);
}
private normalizeDomain(valueArg: string) {
return valueArg.trim().replace(/^https?:\/\//, '').replace(/\/$/, '');
}
private async handleDeploy() { private async handleDeploy() {
const app = this.selectedApp; const app = this.selectedApp;
const config = this.selectedAppConfig; const config = this.selectedAppConfig;
if (!app || !config) return; if (!app || !config) return;
const needsServiceDomain = (config.envVars || []).some((envVarArg) => {
return envVarArg.value?.includes('${SERVICE_DOMAIN}');
});
if (needsServiceDomain && !this.serviceDomain) {
console.error('A domain is required for this app.');
return;
}
const envVars: Record<string, string> = {}; const envVars: Record<string, string> = {};
for (const ev of this.editableEnvVars) { for (const ev of this.editableEnvVars) {
if (ev.key && ev.value && !ev.platformInjected) { if (ev.key && ev.value) {
envVars[ev.key] = ev.value; envVars[ev.key] = ev.value;
} }
} }
@@ -588,6 +619,7 @@ export class ObViewAppStore extends DeesElement {
name: this.serviceName || app.id, name: this.serviceName || app.id,
image: config.image, image: config.image,
port: config.port || 80, port: config.port || 80,
domain: this.serviceDomain || undefined,
envVars, envVars,
enableMongoDB: platformReqs.mongodb || false, enableMongoDB: platformReqs.mongodb || false,
enableS3: platformReqs.s3 || false, enableS3: platformReqs.s3 || false,