Files
qbit-printing/dev/java/DevHost.java
T
james.maesandCodex 3592decba8 feat(dev): add isolated Docker printing and provider qualification (#21)
* feat(dev): add isolated Docker printing and provider qualification

Provide owned local QQQ, MinIO and optional Authentik development fixtures with pinned inputs, protected synthetic state and ownership-checked cleanup. Exercise real Core/Java WebSocket jobs, S3 retention/isolation and opt-in native PKCE/provider renewal and revocation. Add cross-platform script gates and hosted Docker smoke CI.

Include the opt-in 10000-job pooled-SQL/reopen regression and scoped evidence. The experimental API1.9 source remains explicitly unreleased and separate from the released runtime dependency set.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* fix(dev): report early workload failures before server waiting

Preserve bounded client diagnostics privately and emit only allowlisted stage and exception classifications. Early failed clients no longer hide behind the server completion timeout; ownership-checked cleanup remains in finally.

Five actual-entrypoint regression cases pass; all33script tests and parser/analyzer checks pass. This exposes the still-unknown hosted client failure without changing production behavior.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* fix(dev): accept compatible realtime printing API minor versions

Use production Core discovery's strict stable SemVer validation and the realtime major2 compatibility gate instead of requiring exactly2.0.0. Protected main now advertises compatible2.1.0; the old fixture rejected it before login.

Pinned-Core build reproduced2.1.0 rejection before the fix and passes all9positive/negative HTTP discovery cases after it. Docker builds run these checks. No production client, server, dependency or runtime change.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-11-01 10:12:08 -06:00

126 lines
5.4 KiB
Java

// SPDX-License-Identifier: Apache-2.0
package com.kingsrook.qbits.printing;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData;
import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3TableBackendDetails;
import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3ClientFactory;
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncher;
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
/** Docker-only developer host. Compose publishes its port to host loopback only. */
public final class DevHost extends PrintingLocalHost
{
private static final String TABLE = "printing_documents";
private DevHost() throws Exception
{
super(new PrintingLocalSettings("jdbc:h2:file:/data/printing", "sa", required("QQQ_DATABASE_PASSWORD"), "development",
Integer.parseInt(required("QQQ_HTTP_PORT")), required("QQQ_MANAGEMENT_TOKEN"), "developer", required("QQQ_ADMIN_PASSWORD")));
}
public static void main(String[] args) throws Exception
{
waitStorage();
if(args.length == 1 && "--wait-storage".equals(args[0]))
{
return;
}
DevHost host = new DevHost();
QApplicationLauncher.run(host, new QApplicationLauncherConfig().withServerCustomizer(server ->
{
host.configure(server, true);
// Container interface only. The immutable Compose mapping binds host 127.0.0.1.
var original = server.getJavalinConfigCustomizer();
server.withJavalinConfigCustomizer(config ->
{
original.accept(config);
config.jetty.host = "0.0.0.0";
});
}));
}
@Override
public QInstance defineQInstance() throws QException
{
QInstance instance = super.defineQInstance();
S3BackendMetaData backend = new S3BackendMetaData().withName("printing_development_storage").withBucketName("printing-development")
.withEndpoint("http://127.0.0.1:9000").withPathStyleAccess(true).withRegion("us-east-1")
.withAccessKey(required("MINIO_ROOT_USER")).withSecretKey(required("MINIO_ROOT_PASSWORD"));
backend.setConnectTimeoutMillis(3000);
backend.setSocketTimeoutMillis(15000);
instance.addBackend(backend);
instance.addTable(new QTableMetaData().withName(TABLE).withBackendName(backend.getName())
.withBackendDetails(new S3TableBackendDetails().withCardinality(Cardinality.ONE).withFileNameFieldName("id").withContentsFieldName("content"))
.withPrimaryKeyField("id").withField(new QFieldMetaData("id", QFieldType.STRING)).withField(new QFieldMetaData("content", QFieldType.BLOB)));
var storage = S3ClientFactory.getClient(backend);
{
try
{
storage.headBucket(HeadBucketRequest.builder().bucket("printing-development").build());
}
catch(S3Exception missing)
{
if(missing.statusCode() != 404)
{
throw missing;
}
storage.createBucket(CreateBucketRequest.builder().bucket("printing-development").build());
}
}
PrintingQBitConfig config = instance.getQBits().values().stream().map(value -> value.getConfig())
.filter(PrintingQBitConfig.class::isInstance).map(PrintingQBitConfig.class::cast).findFirst().orElseThrow();
config.withStorageTableName(TABLE);
config.getService().configureStorage(instance, TABLE);
return instance;
}
private static void waitStorage() throws Exception
{
try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build())
{
long deadline = System.nanoTime() + Duration.ofSeconds(60).toNanos();
while(System.nanoTime() < deadline)
{
try
{
if(client.send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:9000/minio/health/live"))
.timeout(Duration.ofSeconds(2)).GET().build(), HttpResponse.BodyHandlers.discarding()).statusCode() == 200)
{
return;
}
}
catch(java.io.IOException notReady)
{
// Only the fixture startup window retries; application work is never retried here.
}
Thread.sleep(200);
}
throw new IllegalStateException("Owned MinIO startup timed out");
}
}
private static String required(String name)
{
String value = System.getenv(name);
if(value == null || value.isBlank())
{
throw new IllegalArgumentException("Missing development environment setting: " + name);
}
return value;
}
}