feat: add native Adminer database access
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -79,7 +79,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
||||
setWebServer('nginx')
|
||||
setDatabase('mariadb')
|
||||
setDatabaseVersion('11.8')
|
||||
setAdminer(false)
|
||||
setAdminer(true)
|
||||
setRedis(false)
|
||||
setMailpit(false)
|
||||
setXdebug(false)
|
||||
@@ -459,7 +459,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }): React.
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={runtimeEngine === 'native'}
|
||||
disabled={runtimeEngine === 'native' && label !== 'Adminer'}
|
||||
checked={value as boolean}
|
||||
onChange={(e) => (setter as (v: boolean) => void)(e.target.checked)}
|
||||
/>
|
||||
|
||||
+30
-5
@@ -2,7 +2,20 @@ export type ProjectStatus = 'running' | 'stopped' | 'paused' | 'starting' | 'sto
|
||||
export type AuroraRuntimeEngine = 'container' | 'native'
|
||||
|
||||
export interface AuroraNativeRuntimeComponent {
|
||||
id: 'php' | 'nginx' | 'apache' | 'mariadb' | 'mysql' | 'postgres' | 'node' | 'composer' | 'wp-cli' | 'drush'
|
||||
id:
|
||||
| 'php'
|
||||
| 'nginx'
|
||||
| 'apache'
|
||||
| 'mariadb'
|
||||
| 'mariadb-client'
|
||||
| 'mariadb-dump'
|
||||
| 'mysql'
|
||||
| 'postgres'
|
||||
| 'node'
|
||||
| 'composer'
|
||||
| 'wp-cli'
|
||||
| 'drush'
|
||||
| 'adminer'
|
||||
version: string
|
||||
executable: string
|
||||
sha256: string
|
||||
@@ -18,8 +31,20 @@ export interface AuroraNativeRuntimeManifest {
|
||||
|
||||
export interface AuroraRuntimeStatus {
|
||||
selectedEngine: AuroraRuntimeEngine
|
||||
container: { available: boolean; provider: 'docker' | 'podman' | null; version?: string; reason?: string }
|
||||
native: { available: boolean; platform: string; arch: string; runtimeVersion?: string; components: AuroraNativeRuntimeComponent[]; reason?: string }
|
||||
container: {
|
||||
available: boolean
|
||||
provider: 'docker' | 'podman' | null
|
||||
version?: string
|
||||
reason?: string
|
||||
}
|
||||
native: {
|
||||
available: boolean
|
||||
platform: string
|
||||
arch: string
|
||||
runtimeVersion?: string
|
||||
components: AuroraNativeRuntimeComponent[]
|
||||
reason?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type AuroraRuntimeComponentId = AuroraNativeRuntimeComponent['id']
|
||||
@@ -189,7 +214,6 @@ export interface AuroraSnapshot {
|
||||
Created: string
|
||||
}
|
||||
|
||||
|
||||
export type AuroraModuleCategory = 'application' | 'service' | 'tool'
|
||||
export type AuroraModuleSettingType = 'boolean' | 'select' | 'text' | 'number'
|
||||
|
||||
@@ -252,7 +276,8 @@ export interface AuroraModuleManifest {
|
||||
}
|
||||
}
|
||||
|
||||
export type AuroraModuleLifecycleHook = 'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall' | 'projectTool'
|
||||
export type AuroraModuleLifecycleHook =
|
||||
'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall' | 'projectTool'
|
||||
|
||||
export interface AuroraModuleCommand {
|
||||
command: string
|
||||
|
||||
Reference in New Issue
Block a user