feat: add native Adminer database access

This commit is contained in:
reaper
2026-08-22 04:26:37 -05:00
parent 92b1c98f36
commit 5a61c1c6ff
11 changed files with 163 additions and 26 deletions
+9 -6
View File
@@ -116,8 +116,10 @@ async function assertNativeStack(
throw new Error('Installed native runtime does not provide MariaDB 11.8.')
if (!versions.has('nginx') || !versions.has('wp-cli'))
throw new Error('Installed native runtime is missing nginx or WP-CLI.')
if (stack.adminer !== false || stack.redis || stack.mailpit || stack.xdebug)
throw new Error('Adminer, Redis, Mailpit, and Xdebug are not enabled for Aurora Native yet.')
if (stack.adminer !== false && !versions.has('adminer'))
throw new Error('Installed native runtime does not provide Adminer.')
if (stack.redis || stack.mailpit || stack.xdebug)
throw new Error('Redis, Mailpit, and Xdebug are not enabled for Aurora Native yet.')
}
async function loadRegistry(): Promise<Registry> {
@@ -532,10 +534,11 @@ export async function describeProject(name: string): Promise<AuroraProjectDetail
firefox_trust_status: c.runtimeEngine === 'native' ? 'unknown' : await firefoxTrustStatus(),
chromium_trust_status: c.runtimeEngine === 'native' ? 'unknown' : await chromiumTrustStatus(),
module_metadata: moduleMetadata,
adminer_url:
c.runtimeEngine === 'container' && c.modules.includes('adminer')
? `https://adminer.${projectHost(c.name)}`
: undefined,
adminer_url: c.modules.includes('adminer')
? c.runtimeEngine === 'native'
? `${urlSet.http}/__aurora/adminer/`
: `https://adminer.${projectHost(c.name)}`
: undefined,
services,
xdebug_enabled: c.xdebug === true,
runtime_engine: c.runtimeEngine ?? 'container',
+20
View File
@@ -7,6 +7,7 @@ import { promisify } from 'util'
import { describe, expect, it } from 'vitest'
import {
renderMariaDbConfig,
renderNativeAdminerBootstrap,
renderNginxConfig,
renderPhpFpmConfig,
startNativeProject,
@@ -54,6 +55,16 @@ describe('native project configuration', () => {
expect(config).toContain('port=41003')
expect(config).toContain('/.aurora/native/data/mariadb')
})
it('creates a loopback Adminer endpoint with project database credentials', () => {
expect(renderNginxConfig(project)).toContain('location = /__aurora/adminer/')
const bootstrap = renderNativeAdminerBootstrap({
...project,
installedRuntimeRoot: '/tmp/aurora-runtime'
})
expect(bootstrap).toContain("value = '127.0.0.1:41003'")
expect(bootstrap).toContain("username.value = 'db'")
expect(bootstrap).toContain('/tmp/aurora-runtime/root/usr/share/aurora/adminer.php')
})
})
it.runIf(Boolean(process.env.AURORA_NATIVE_SMOKE_ROOT))(
@@ -170,6 +181,15 @@ it.runIf(Boolean(process.env.AURORA_NATIVE_WORDPRESS_SMOKE_ROOT))(
{ cwd: root, env: process.env }
)
expect(restored.stdout.trim()).toBe('Aurora native smoke')
const adminerResponse = await fetch(
`http://127.0.0.1:${definition.ports.http}/__aurora/adminer/`
)
const adminerHtml = await adminerResponse.text()
expect(adminerResponse.ok).toBe(true)
expect(adminerHtml).toContain('Adminer')
expect(adminerHtml).toContain("username.value = 'db'")
expect(adminerHtml).toContain("value = '127.0.0.1:")
} finally {
await stopNativeProject(definition)
}
+41
View File
@@ -68,6 +68,22 @@ http {
server_name localhost;
root "${quoteNginx(webroot)}";
index index.php index.html;
location = /__aurora/adminer/ {
fastcgi_pass 127.0.0.1:${project.ports.php};
fastcgi_param SCRIPT_FILENAME "${quoteNginx(join(directory, 'adminer.php'))}";
fastcgi_param SCRIPT_NAME /__aurora/adminer/;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param HTTP_HOST $http_host;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
}
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \\.php$ {
fastcgi_pass 127.0.0.1:${project.ports.php};
@@ -92,6 +108,30 @@ http {
`
}
export function renderNativeAdminerBootstrap(project: NativeProjectDefinition): string {
const adminer = join(installedRoot(project), 'root', 'usr', 'share', 'aurora', 'adminer.php')
return `<?php
// Generated by Aurora Core. This endpoint binds only to the project's loopback server.
?>
<script>
addEventListener('DOMContentLoaded', () => {
const username = document.querySelector('input[name="auth[username]"]');
if (!username) return;
const form = username.form;
form.querySelector('input[name="auth[server]"]').value = '127.0.0.1:${project.ports.database}';
username.value = 'db';
form.querySelector('input[name="auth[password]"]').value = 'db';
form.querySelector('input[name="auth[db]"]').value = 'db';
const permanent = form.querySelector('input[name="auth[permanent]"]');
if (permanent) permanent.checked = true;
form.submit();
});
</script>
<?php
require '${adminer.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}';
`
}
export function renderMariaDbConfig(
project: NativeProjectDefinition,
installedRuntimeRoot = runtimeRoot()
@@ -128,6 +168,7 @@ export async function provisionNativeProject(project: NativeProjectDefinition):
await Promise.all([
writeFile(join(directory, 'config', 'php-fpm.conf'), renderPhpFpmConfig(project)),
writeFile(join(directory, 'config', 'nginx.conf'), renderNginxConfig(project)),
writeFile(join(directory, 'adminer.php'), renderNativeAdminerBootstrap(project)),
writeFile(
join(directory, 'config', 'mariadb.cnf'),
renderMariaDbConfig(project, installedRoot(project))
+4 -1
View File
@@ -15,12 +15,15 @@ const componentIds = new Set([
'nginx',
'apache',
'mariadb',
'mariadb-client',
'mariadb-dump',
'mysql',
'postgres',
'node',
'composer',
'wp-cli',
'drush'
'drush',
'adminer'
])
export function validateNativeRuntimeManifest(value: unknown): AuroraNativeRuntimeManifest {
+40 -9
View File
@@ -10,7 +10,8 @@ import type {
AuroraRuntimeUpdateStatus
} from '../shared/types'
const catalogUrl = process.env.AURORA_RUNTIME_CATALOG_URL || 'https://auroradockside.com/runtime/catalog.json'
const catalogUrl =
process.env.AURORA_RUNTIME_CATALOG_URL || 'https://auroradockside.com/runtime/catalog.json'
const publicKeyPem = process.env.AURORA_RUNTIME_CATALOG_PUBLIC_KEY
function compareVersions(left: string, right: string): number {
@@ -25,13 +26,31 @@ function compareVersions(left: string, right: string): number {
export function validateRuntimeCatalog(value: unknown): AuroraRuntimeCatalog {
if (!value || typeof value !== 'object') throw new Error('Runtime catalog must be an object.')
const catalog = value as Record<string, unknown>
if (catalog.schema !== 1 || typeof catalog.generatedAt !== 'string' || !Array.isArray(catalog.releases))
if (
catalog.schema !== 1 ||
typeof catalog.generatedAt !== 'string' ||
!Array.isArray(catalog.releases)
)
throw new Error('Unsupported runtime catalog.')
const releases = catalog.releases.map((entry) => {
if (!entry || typeof entry !== 'object') throw new Error('Invalid runtime catalog release.')
const release = entry as Record<string, unknown>
if (
!['php', 'nginx', 'apache', 'mariadb', 'mysql', 'postgres', 'node', 'composer', 'wp-cli', 'drush'].includes(String(release.component)) ||
![
'php',
'nginx',
'apache',
'mariadb',
'mariadb-client',
'mariadb-dump',
'mysql',
'postgres',
'node',
'composer',
'wp-cli',
'drush',
'adminer'
].includes(String(release.component)) ||
typeof release.version !== 'string' ||
!['linux', 'darwin', 'win32'].includes(String(release.platform)) ||
!['x64', 'arm64'].includes(String(release.arch)) ||
@@ -40,7 +59,8 @@ export function validateRuntimeCatalog(value: unknown): AuroraRuntimeCatalog {
!release.url.startsWith('https://') ||
typeof release.sha256 !== 'string' ||
!/^[a-f0-9]{64}$/i.test(release.sha256)
) throw new Error('Invalid runtime catalog release.')
)
throw new Error('Invalid runtime catalog release.')
return release as unknown as AuroraRuntimeCatalogRelease
})
return { schema: 1, generatedAt: catalog.generatedAt, releases }
@@ -53,12 +73,20 @@ export function findRuntimeUpdates(
arch: string
): AuroraRuntimeUpdate[] {
return installed.flatMap((component) => {
const candidates = catalog.releases.filter((release) =>
release.component === component.id && release.platform === platform && release.arch === arch
const candidates = catalog.releases.filter(
(release) =>
release.component === component.id && release.platform === platform && release.arch === arch
)
const latest = candidates.sort((a, b) => compareVersions(b.version, a.version))[0]
return latest && compareVersions(latest.version, component.version) > 0
? [{ component: component.id, installedVersion: component.version, availableVersion: latest.version, channel: latest.channel }]
? [
{
component: component.id,
installedVersion: component.version,
availableVersion: latest.version,
channel: latest.channel
}
]
: []
})
}
@@ -76,7 +104,8 @@ async function remoteCatalog(): Promise<AuroraRuntimeCatalog> {
fetch(catalogUrl, { signal: controller.signal }),
fetch(`${catalogUrl}.sig`, { signal: controller.signal })
])
if (!catalogResponse.ok || !signatureResponse.ok) throw new Error('Runtime catalog server is unavailable.')
if (!catalogResponse.ok || !signatureResponse.ok)
throw new Error('Runtime catalog server is unavailable.')
const body = Buffer.from(await catalogResponse.arrayBuffer())
const signature = Buffer.from((await signatureResponse.text()).trim(), 'base64')
if (!verify(null, body, createPublicKey(publicKeyPem), signature))
@@ -87,7 +116,9 @@ async function remoteCatalog(): Promise<AuroraRuntimeCatalog> {
}
}
export async function getRuntimeUpdates(installed: AuroraNativeRuntimeComponent[]): Promise<AuroraRuntimeUpdateStatus> {
export async function getRuntimeUpdates(
installed: AuroraNativeRuntimeComponent[]
): Promise<AuroraRuntimeUpdateStatus> {
const cacheDirectory = join(app.getPath('userData'), 'runtime-catalog')
const cachePath = join(cacheDirectory, 'catalog.json')
let source: AuroraRuntimeUpdateStatus['source'] = 'bundled'