diff --git a/backend/controller/sql/database_integration_test.go b/backend/controller/sql/database_integration_test.go
index 11dc9d0d84..462cf22d07 100644
--- a/backend/controller/sql/database_integration_test.go
+++ b/backend/controller/sql/database_integration_test.go
@@ -11,6 +11,7 @@ import (
func TestDatabase(t *testing.T) {
in.Run(t,
+ in.WithLanguages("go", "java"),
in.WithFTLConfig("database/ftl-project.toml"),
// deploy real module against "testdb"
in.CopyModule("database"),
@@ -21,7 +22,7 @@ func TestDatabase(t *testing.T) {
// run tests which should only affect "testdb_test"
in.CreateDBAction("database", "testdb", true),
- in.ExecModuleTest("database"),
+ in.IfLanguage("go", in.ExecModuleTest("database")),
in.QueryRow("testdb", "SELECT data FROM requests", "hello"),
)
}
diff --git a/backend/controller/sql/testdata/java/database/ftl.toml b/backend/controller/sql/testdata/java/database/ftl.toml
new file mode 100644
index 0000000000..5247fce7df
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/ftl.toml
@@ -0,0 +1,2 @@
+module = "database"
+language = "java"
diff --git a/backend/controller/sql/testdata/java/database/pom.xml b/backend/controller/sql/testdata/java/database/pom.xml
new file mode 100644
index 0000000000..4eaf94c5ef
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+ xyz.block.ftl.examples
+ database
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-java
+ 1.0-SNAPSHOT
+
+
+
+
+ io.quarkus
+ quarkus-hibernate-orm-panache
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+
+
diff --git a/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Database.java b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Database.java
new file mode 100644
index 0000000000..ab194f03f7
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Database.java
@@ -0,0 +1,17 @@
+package xyz.block.ftl.java.test.database;
+
+import jakarta.transaction.Transactional;
+
+import xyz.block.ftl.Verb;
+
+public class Database {
+
+ @Verb
+ @Transactional
+ public InsertResponse insert(InsertRequest insertRequest) {
+ Request request = new Request();
+ request.data = insertRequest.getData();
+ request.persist();
+ return new InsertResponse();
+ }
+}
diff --git a/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertRequest.java b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertRequest.java
new file mode 100644
index 0000000000..38c55f33bf
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertRequest.java
@@ -0,0 +1,14 @@
+package xyz.block.ftl.java.test.database;
+
+public class InsertRequest {
+ private String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public InsertRequest setData(String data) {
+ this.data = data;
+ return this;
+ }
+}
diff --git a/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertResponse.java b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertResponse.java
new file mode 100644
index 0000000000..aa0f82476e
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/InsertResponse.java
@@ -0,0 +1,4 @@
+package xyz.block.ftl.java.test.database;
+
+public class InsertResponse {
+}
diff --git a/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Request.java b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Request.java
new file mode 100644
index 0000000000..64268ddd02
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/src/main/java/xyz/block/ftl/java/test/database/Request.java
@@ -0,0 +1,22 @@
+package xyz.block.ftl.java.test.database;
+
+import java.sql.Timestamp;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+
+import io.quarkus.hibernate.orm.panache.PanacheEntity;
+
+@Entity
+@Table(name = "requests")
+public class Request extends PanacheEntity {
+ public String data;
+
+ @Column(name = "created_at")
+ public Timestamp createdAt;
+
+ @Column(name = "updated_at")
+ public Timestamp updatedAt;
+
+}
diff --git a/backend/controller/sql/testdata/java/database/src/main/resources/application.properties b/backend/controller/sql/testdata/java/database/src/main/resources/application.properties
new file mode 100644
index 0000000000..890fc4bc37
--- /dev/null
+++ b/backend/controller/sql/testdata/java/database/src/main/resources/application.properties
@@ -0,0 +1,3 @@
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.datasource.testdb.db-kind=postgresql
+quarkus.hibernate-orm.datasource=testdb
\ No newline at end of file
diff --git a/docs/content/docs/reference/matrix.md b/docs/content/docs/reference/matrix.md
index 12172bd4de..3e1e6bdfc8 100644
--- a/docs/content/docs/reference/matrix.md
+++ b/docs/content/docs/reference/matrix.md
@@ -35,7 +35,7 @@ top = false
| | Config | ✔️ | ✔️ | |
| | Secrets | ✔️ | ✔️ | |
| | HTTP Ingress | ✔️ | ✔️ | |
-| **Resources** | PostgreSQL | ✔️ | ️ | |
+| **Resources** | PostgreSQL | ✔️ | ✔️ | |
| | MySQL | | | |
| | Kafka | | | |
| **PubSub** | Declaring Topic | ✔️ | ✔️ | |
diff --git a/internal/buildengine/build_java.go b/internal/buildengine/build_java.go
index 4b4994b4ac..98e449f525 100644
--- a/internal/buildengine/build_java.go
+++ b/internal/buildengine/build_java.go
@@ -20,7 +20,9 @@ func buildJavaModule(ctx context.Context, module Module) error {
logger.Warnf("unable to update ftl.version in %s: %s", module.Config.Dir, err.Error())
}
logger.Infof("Using build command '%s'", module.Config.Build)
- err := exec.Command(ctx, log.Debug, module.Config.Dir, "bash", "-c", module.Config.Build).RunBuffered(ctx)
+ command := exec.Command(ctx, log.Debug, module.Config.Dir, "bash", "-c", module.Config.Build)
+ command.Env = append(command.Env, "FTL_MODULE_NAME="+module.Config.Module)
+ err := command.RunBuffered(ctx)
if err != nil {
return fmt.Errorf("failed to build module %q: %w", module.Config.Module, err)
}
diff --git a/jvm-runtime/ftl-runtime/common/build-parent/pom.xml b/jvm-runtime/ftl-runtime/common/build-parent/pom.xml
index 8d4a1e5659..7c37b16ea4 100644
--- a/jvm-runtime/ftl-runtime/common/build-parent/pom.xml
+++ b/jvm-runtime/ftl-runtime/common/build-parent/pom.xml
@@ -19,7 +19,7 @@
UTF-8
quarkus-bom
io.quarkus.platform
- 3.12.3
+ 3.13.2
true
3.2.5
diff --git a/jvm-runtime/ftl-runtime/common/deployment/pom.xml b/jvm-runtime/ftl-runtime/common/deployment/pom.xml
index 5cf9f54ca1..766da8745e 100644
--- a/jvm-runtime/ftl-runtime/common/deployment/pom.xml
+++ b/jvm-runtime/ftl-runtime/common/deployment/pom.xml
@@ -23,6 +23,14 @@
io.quarkus
quarkus-rest-jackson-deployment
+
+ io.quarkus
+ quarkus-agroal-spi
+
+
+ io.quarkus
+ quarkus-credentials-deployment
+
xyz.block.ftl
diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLBuildTimeConfig.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLBuildTimeConfig.java
new file mode 100644
index 0000000000..174014bb04
--- /dev/null
+++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLBuildTimeConfig.java
@@ -0,0 +1,16 @@
+package xyz.block.ftl.deployment;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+@ConfigRoot(name = "ftl")
+public class FTLBuildTimeConfig {
+
+ /**
+ * The FTL module name, should be set automatically during build
+ */
+ @ConfigItem
+ public Optional moduleName;
+}
diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
index 94d1dacbd8..046995426b 100644
--- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
+++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java
@@ -39,6 +39,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.Capabilities;
@@ -51,7 +52,9 @@
import io.quarkus.deployment.builditem.ApplicationStartBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
+import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
@@ -76,7 +79,7 @@
import xyz.block.ftl.Subscription;
import xyz.block.ftl.Verb;
import xyz.block.ftl.VerbName;
-import xyz.block.ftl.runtime.FTLController;
+import xyz.block.ftl.runtime.FTLDatasourceCredentials;
import xyz.block.ftl.runtime.FTLHttpHandler;
import xyz.block.ftl.runtime.FTLRecorder;
import xyz.block.ftl.runtime.JsonSerializationConfig;
@@ -86,11 +89,14 @@
import xyz.block.ftl.runtime.VerbRegistry;
import xyz.block.ftl.runtime.builtin.HttpRequest;
import xyz.block.ftl.runtime.builtin.HttpResponse;
+import xyz.block.ftl.runtime.config.FTLConfigSource;
+import xyz.block.ftl.runtime.config.FTLConfigSourceFactoryBuilder;
import xyz.block.ftl.v1.CallRequest;
import xyz.block.ftl.v1.schema.Array;
import xyz.block.ftl.v1.schema.Bool;
import xyz.block.ftl.v1.schema.Bytes;
import xyz.block.ftl.v1.schema.Data;
+import xyz.block.ftl.v1.schema.Database;
import xyz.block.ftl.v1.schema.Decl;
import xyz.block.ftl.v1.schema.Field;
import xyz.block.ftl.v1.schema.Float;
@@ -133,9 +139,13 @@ class FtlProcessor {
public static final DotName NOT_NULL = DotName.createSimple(NotNull.class);
@BuildStep
- ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem) {
- return new ModuleNameBuildItem(applicationInfoBuildItem.getName());
+ ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem, FTLBuildTimeConfig buildTimeConfig) {
+ return new ModuleNameBuildItem(buildTimeConfig.moduleName.orElse(applicationInfoBuildItem.getName()));
+ }
+ @BuildStep
+ RunTimeConfigBuilderBuildItem runTimeConfigBuilderBuildItem() {
+ return new RunTimeConfigBuilderBuildItem(FTLConfigSourceFactoryBuilder.class.getName());
}
@BuildStep
@@ -159,8 +169,9 @@ BindableServiceBuildItem verbService() {
AdditionalBeanBuildItem beans() {
return AdditionalBeanBuildItem.builder()
.addBeanClasses(VerbHandler.class,
- VerbRegistry.class, FTLHttpHandler.class, FTLController.class,
- TopicHelper.class, VerbClientHelper.class, JsonSerializationConfig.class)
+ VerbRegistry.class, FTLHttpHandler.class,
+ TopicHelper.class, VerbClientHelper.class, JsonSerializationConfig.class,
+ FTLDatasourceCredentials.class)
.setUnremovable().build();
}
@@ -223,7 +234,10 @@ public void registerVerbs(CombinedIndexBuildItem index,
TopicsBuildItem topics,
VerbClientBuildItem verbClients,
ModuleNameBuildItem moduleNameBuildItem,
- SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem) throws Exception {
+ SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem,
+ List datasources,
+ BuildProducer systemPropProducer,
+ BuildProducer generatedResourceBuildItemBuildProducer) throws Exception {
String moduleName = moduleNameBuildItem.getModuleName();
Module.Builder moduleBuilder = Module.newBuilder()
.setName(moduleName)
@@ -233,8 +247,36 @@ public void registerVerbs(CombinedIndexBuildItem index,
new HashSet<>(), new HashSet<>(), topics.getTopics(), verbClients.getVerbClients());
var beans = AdditionalBeanBuildItem.builder().setUnremovable();
- //register all the topics we are defining in the module definition
+ List namedDatasources = new ArrayList<>();
+ for (var ds : datasources) {
+ if (!ds.getDbKind().equals("postgresql")) {
+ throw new RuntimeException("only postgresql is supported not " + ds.getDbKind());
+ }
+ //default name is which is not a valid name
+ String sanitisedName = ds.getName().replace("<", "").replace(">", "");
+ //we use a dynamic credentials provider
+ if (ds.isDefault()) {
+ systemPropProducer
+ .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider", sanitisedName));
+ systemPropProducer
+ .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider-name",
+ FTLDatasourceCredentials.NAME));
+ } else {
+ namedDatasources.add(ds.getName());
+ systemPropProducer.produce(new SystemPropertyBuildItem(
+ "quarkus.datasource." + ds.getName() + ".credentials-provider", sanitisedName));
+ systemPropProducer.produce(new SystemPropertyBuildItem(
+ "quarkus.datasource." + ds.getName() + ".credentials-provider-name", FTLDatasourceCredentials.NAME));
+ }
+ moduleBuilder.addDecls(
+ Decl.newBuilder().setDatabase(
+ Database.newBuilder().setType("postgres").setName(sanitisedName))
+ .build());
+ }
+ generatedResourceBuildItemBuildProducer.produce(new GeneratedResourceBuildItem(FTLConfigSource.DATASOURCE_NAMES,
+ String.join("\n", namedDatasources).getBytes(StandardCharsets.UTF_8)));
+ //register all the topics we are defining in the module definition
for (var topic : topics.getTopics().values()) {
extractionContext.moduleBuilder.addDecls(Decl.newBuilder().setTopic(xyz.block.ftl.v1.schema.Topic.newBuilder()
.setExport(topic.exported())
diff --git a/jvm-runtime/ftl-runtime/common/runtime/pom.xml b/jvm-runtime/ftl-runtime/common/runtime/pom.xml
index 3ac8b03549..b62496d022 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/pom.xml
+++ b/jvm-runtime/ftl-runtime/common/runtime/pom.xml
@@ -28,6 +28,10 @@
com.fasterxml.jackson.module
jackson-module-kotlin
+
+ io.quarkus
+ quarkus-credentials
+
io.grpc
grpc-stub
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLConfigSource.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLConfigSource.java
deleted file mode 100644
index 15ff77949d..0000000000
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLConfigSource.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package xyz.block.ftl.runtime;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Set;
-
-import org.eclipse.microprofile.config.spi.ConfigSource;
-
-public class FTLConfigSource implements ConfigSource {
-
- final static String SEPARATE_SERVER = "quarkus.grpc.server.use-separate-server";
- final static String PORT = "quarkus.http.port";
- final static String HOST = "quarkus.http.host";
-
- final static String FTL_BIND = "FTL_BIND";
-
- @Override
- public Set getPropertyNames() {
- return Set.of(SEPARATE_SERVER, PORT, HOST);
- }
-
- @Override
- public int getOrdinal() {
- return 1;
- }
-
- @Override
- public String getValue(String s) {
- switch (s) {
- case SEPARATE_SERVER -> {
- return "false";
- }
- case PORT -> {
- String bind = System.getenv(FTL_BIND);
- if (bind == null) {
- return null;
- }
- try {
- URI uri = new URI(bind);
- return Integer.toString(uri.getPort());
- } catch (URISyntaxException e) {
- return null;
- }
- }
- case HOST -> {
- String bind = System.getenv(FTL_BIND);
- if (bind == null) {
- return null;
- }
- try {
- URI uri = new URI(bind);
- return uri.getHost();
- } catch (URISyntaxException e) {
- return null;
- }
- }
- }
- return null;
- }
-
- @Override
- public String getName() {
- return "FTL Config";
- }
-}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
index dbf1d3d7ff..94f953d205 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLController.java
@@ -1,22 +1,21 @@
package xyz.block.ftl.runtime;
import java.net.URI;
+import java.net.URISyntaxException;
import java.time.Duration;
import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
-import jakarta.annotation.PreDestroy;
-import jakarta.inject.Singleton;
-
-import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
-import io.quarkus.runtime.Startup;
import xyz.block.ftl.LeaseClient;
import xyz.block.ftl.LeaseFailedException;
import xyz.block.ftl.LeaseHandle;
@@ -31,8 +30,6 @@
import xyz.block.ftl.v1.VerbServiceGrpc;
import xyz.block.ftl.v1.schema.Ref;
-@Singleton
-@Startup
public class FTLController implements LeaseClient {
private static final Logger log = Logger.getLogger(FTLController.class);
final String moduleName;
@@ -40,52 +37,37 @@ public class FTLController implements LeaseClient {
private Throwable currentError;
private volatile ModuleContextResponse moduleContextResponse;
private boolean waiters = false;
- private volatile boolean closed = false;
final VerbServiceGrpc.VerbServiceStub verbService;
- final StreamObserver moduleObserver = new StreamObserver<>() {
- @Override
- public void onNext(ModuleContextResponse moduleContextResponse) {
- synchronized (this) {
- currentError = null;
- FTLController.this.moduleContextResponse = moduleContextResponse;
- if (waiters) {
- this.notifyAll();
- waiters = false;
- }
- }
+ final StreamObserver moduleObserver = new ModuleObserver();
- }
+ private static volatile FTLController controller;
- @Override
- public void onError(Throwable throwable) {
- log.error("GRPC connection error", throwable);
- synchronized (this) {
- currentError = throwable;
- if (waiters) {
- this.notifyAll();
- waiters = false;
+ /**
+ * TODO: look at how init should work, this is terrible and will break dev mode
+ */
+ public static FTLController instance() {
+ if (controller == null) {
+ synchronized (FTLController.class) {
+ if (controller == null) {
+ controller = new FTLController();
}
}
- if (!closed) {
- verbService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
- }
- }
-
- @Override
- public void onCompleted() {
- onError(new RuntimeException("connection closed"));
}
- };
-
- @PreDestroy
- void shutdown() {
-
+ return controller;
}
- public FTLController(@ConfigProperty(name = "ftl.endpoint", defaultValue = "http://localhost:8892") URI uri,
- @ConfigProperty(name = "ftl.module.name") String moduleName) {
- this.moduleName = moduleName;
+ FTLController() {
+ String endpoint = System.getenv("FTL_ENDPOINT");
+ String testEndpoint = System.getProperty("ftl.test.endpoint"); //set by the test framework
+ if (testEndpoint != null) {
+ endpoint = testEndpoint;
+ }
+ if (endpoint == null) {
+ endpoint = "http://localhost:8892";
+ }
+ var uri = URI.create(endpoint);
+ this.moduleName = System.getProperty("ftl.module.name");
var channelBuilder = ManagedChannelBuilder.forAddress(uri.getHost(), uri.getPort());
if (uri.getScheme().equals("http")) {
channelBuilder.usePlaintext();
@@ -111,6 +93,17 @@ public byte[] getConfig(String secretName) {
throw new RuntimeException("Config not found: " + secretName);
}
+ public Datasource getDatasource(String name) {
+ //TODO: only one database is supported at the moment
+ List databasesList = getModuleContext().getDatabasesList();
+ for (var i : databasesList) {
+ if (i.getName().equals(name)) {
+ return Datasource.fromDSN(i.getDsn());
+ }
+ }
+ return null;
+ }
+
public byte[] callVerb(String name, String module, byte[] payload) {
CompletableFuture cf = new CompletableFuture<>();
@@ -234,4 +227,80 @@ private ModuleContextResponse getModuleContext() {
}
}
+ private class ModuleObserver implements StreamObserver {
+
+ final AtomicInteger failCount = new AtomicInteger();
+
+ @Override
+ public void onNext(ModuleContextResponse moduleContextResponse) {
+ synchronized (this) {
+ currentError = null;
+ FTLController.this.moduleContextResponse = moduleContextResponse;
+ if (waiters) {
+ this.notifyAll();
+ waiters = false;
+ }
+ }
+
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ log.error("GRPC connection error", throwable);
+ synchronized (this) {
+ currentError = throwable;
+ if (waiters) {
+ this.notifyAll();
+ waiters = false;
+ }
+ }
+ if (failCount.incrementAndGet() < 5) {
+ verbService.getModuleContext(ModuleContextRequest.newBuilder().setModule(moduleName).build(), moduleObserver);
+ }
+ }
+
+ @Override
+ public void onCompleted() {
+ onError(new RuntimeException("connection closed"));
+ }
+ }
+
+ public record Datasource(String connectionString, String username, String password) {
+
+ public static Datasource fromDSN(String dsn) {
+ try {
+ URI uri = new URI(dsn);
+ String username = "";
+ String password = "";
+ String userInfo = uri.getUserInfo();
+ if (userInfo != null) {
+ var split = userInfo.split(":");
+ username = split[0];
+ password = split[1];
+ return new Datasource(
+ new URI("jdbc:postgresql", null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), null)
+ .toASCIIString(),
+ username, password);
+ } else {
+ //TODO: this is horrible, just quick hack for now
+ var matcher = Pattern.compile("[&?]user=([^?]*)").matcher(dsn);
+ if (matcher.find()) {
+ username = matcher.group(1);
+ }
+ dsn = matcher.replaceAll("");
+ matcher = Pattern.compile("[&?]password=([^?]*)").matcher(dsn);
+ if (matcher.find()) {
+ password = matcher.group(1);
+ }
+ dsn = matcher.replaceAll("");
+ dsn = dsn.replaceAll("postgresql://", "jdbc:postgresql://");
+ dsn = dsn.replaceAll("postgres://", "jdbc:postgresql://");
+ return new Datasource(dsn, username, password);
+ }
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLDatasourceCredentials.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLDatasourceCredentials.java
new file mode 100644
index 0000000000..255b79d3cd
--- /dev/null
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLDatasourceCredentials.java
@@ -0,0 +1,24 @@
+package xyz.block.ftl.runtime;
+
+import java.util.Map;
+
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+
+import io.quarkus.credentials.CredentialsProvider;
+
+@Named(FTLDatasourceCredentials.NAME)
+@Singleton
+public class FTLDatasourceCredentials implements CredentialsProvider {
+
+ public static final String NAME = "ftl-datasource-credentials";
+
+ @Override
+ public Map getCredentials(String credentialsProviderName) {
+ FTLController.Datasource datasource = FTLController.instance().getDatasource(credentialsProviderName);
+ if (datasource == null) {
+ return null;
+ }
+ return Map.of("user", datasource.username(), "password", datasource.password());
+ }
+}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
index 9b4b03f7b1..6f0208c38e 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/FTLRecorder.java
@@ -12,7 +12,6 @@
import io.quarkus.arc.Arc;
import io.quarkus.runtime.annotations.Recorder;
-import xyz.block.ftl.LeaseClient;
import xyz.block.ftl.v1.CallRequest;
@Recorder
@@ -76,14 +75,10 @@ public Object apply(ObjectMapper mapper, CallRequest callRequest) {
public BiFunction leaseClientSupplier() {
return new BiFunction() {
- volatile LeaseClient leaseClient;
@Override
public Object apply(ObjectMapper mapper, CallRequest callRequest) {
- if (leaseClient == null) {
- leaseClient = Arc.container().instance(LeaseClient.class).get();
- }
- return leaseClient;
+ return FTLController.instance();
}
};
}
@@ -129,14 +124,9 @@ public ParameterExtractor leaseClientExtractor() {
try {
return new ParameterExtractor() {
- volatile LeaseClient leaseClient;
-
@Override
public Object extractParameter(ResteasyReactiveRequestContext context) {
- if (leaseClient == null) {
- leaseClient = Arc.container().instance(LeaseClient.class).get();
- }
- return leaseClient;
+ return FTLController.instance();
}
};
} catch (Exception e) {
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/TopicHelper.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/TopicHelper.java
index aa1e0fb20b..1e8bbdfa80 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/TopicHelper.java
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/TopicHelper.java
@@ -10,17 +10,15 @@
@Singleton
public class TopicHelper {
- final FTLController controller;
final ObjectMapper mapper;
- public TopicHelper(FTLController controller, ObjectMapper mapper) {
- this.controller = controller;
+ public TopicHelper(ObjectMapper mapper) {
this.mapper = mapper;
}
public void publish(String topic, String verb, Object message) {
try {
- controller.publishEvent(topic, verb, mapper.writeValueAsBytes(message));
+ FTLController.instance().publishEvent(topic, verb, mapper.writeValueAsBytes(message));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbClientHelper.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbClientHelper.java
index b28037c76c..3cb8877902 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbClientHelper.java
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbClientHelper.java
@@ -11,11 +11,9 @@
@Singleton
public class VerbClientHelper {
- final FTLController controller;
final ObjectMapper mapper;
- public VerbClientHelper(FTLController controller, ObjectMapper mapper) {
- this.controller = controller;
+ public VerbClientHelper(ObjectMapper mapper) {
this.mapper = mapper;
}
@@ -27,7 +25,7 @@ public Object call(String verb, String module, Object message, Class> returnTy
//TODO: what about optional?
message = Map.of();
}
- var result = controller.callVerb(verb, module, mapper.writeValueAsBytes(message));
+ var result = FTLController.instance().callVerb(verb, module, mapper.writeValueAsBytes(message));
if (listReturnType) {
return mapper.readerForArrayOf(returnType).readValue(result);
} else if (mapReturnType) {
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
index 2208fe51ea..cb9a7e9d64 100644
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/VerbRegistry.java
@@ -123,8 +123,6 @@ public static class SecretSupplier implements BiFunction inputClass;
- volatile FTLController ftlController;
-
public SecretSupplier(String name, Class> inputClass) {
this.name = name;
this.inputClass = inputClass;
@@ -132,10 +130,8 @@ public SecretSupplier(String name, Class> inputClass) {
@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
- if (ftlController == null) {
- ftlController = Arc.container().instance(FTLController.class).get();
- }
- var secret = ftlController.getSecret(name);
+
+ var secret = FTLController.instance().getSecret(name);
try {
return mapper.createParser(secret).readValueAs(inputClass);
} catch (IOException e) {
@@ -162,8 +158,6 @@ public static class ConfigSupplier implements BiFunction inputClass;
- volatile FTLController ftlController;
-
public ConfigSupplier(String name, Class> inputClass) {
this.name = name;
this.inputClass = inputClass;
@@ -171,10 +165,7 @@ public ConfigSupplier(String name, Class> inputClass) {
@Override
public Object apply(ObjectMapper mapper, CallRequest in) {
- if (ftlController == null) {
- ftlController = Arc.container().instance(FTLController.class).get();
- }
- var secret = ftlController.getConfig(name);
+ var secret = FTLController.instance().getConfig(name);
try {
return mapper.createParser(secret).readValueAs(inputClass);
} catch (IOException e) {
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSource.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSource.java
new file mode 100644
index 0000000000..a3d85f5064
--- /dev/null
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSource.java
@@ -0,0 +1,142 @@
+package xyz.block.ftl.runtime.config;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+import xyz.block.ftl.runtime.FTLController;
+
+public class FTLConfigSource implements ConfigSource {
+
+ public static final String DATASOURCE_NAMES = "ftl-datasource-names.txt";
+
+ final static String SEPARATE_SERVER = "quarkus.grpc.server.use-separate-server";
+ final static String PORT = "quarkus.http.port";
+ final static String HOST = "quarkus.http.host";
+
+ final static String FTL_BIND = "FTL_BIND";
+
+ final FTLController controller;
+
+ private static final String DEFAULT_USER = "quarkus.datasource.username";
+ private static final String DEFAULT_PASSWORD = "quarkus.datasource.password";
+ private static final String DEFAULT_URL = "quarkus.datasource.jdbc.url";
+ private static final Pattern USER_PATTERN = Pattern.compile("^quarkus\\.datasource\\.\"?([^.]+?)\"?.jdbc.username$");
+ private static final Pattern PASSWORD_PATTERN = Pattern.compile("^quarkus\\.datasource\\.\"?([^.]+?)\"?.jdbc.password$");
+ private static final Pattern URL_PATTERN = Pattern.compile("^quarkus\\.datasource\\.\"?([^.]+?)\"?.jdbc\\.url$");
+
+ final Set propertyNames;
+
+ public FTLConfigSource(FTLController controller) {
+ this.controller = controller;
+ this.propertyNames = new HashSet<>(List.of(SEPARATE_SERVER, PORT, HOST));
+ try (var in = Thread.currentThread().getContextClassLoader().getResourceAsStream(DATASOURCE_NAMES)) {
+ String s = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ for (String name : s.split("\n")) {
+ if (name.isEmpty()) {
+ continue;
+ }
+ propertyNames.add("quarkus.datasource." + name + ".username");
+ propertyNames.add("quarkus.datasource." + name + ".password");
+ propertyNames.add("quarkus.datasource." + name + ".jdbc.url");
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("failed to read datasource file, this should have been generated as part of the build",
+ e);
+ }
+ }
+
+ @Override
+ public Set getPropertyNames() {
+ return propertyNames;
+ }
+
+ @Override
+ public int getOrdinal() {
+ return 400;
+ }
+
+ @Override
+ public String getValue(String s) {
+ switch (s) {
+ case SEPARATE_SERVER -> {
+ return "false";
+ }
+ case PORT -> {
+ String bind = System.getenv(FTL_BIND);
+ if (bind == null) {
+ return null;
+ }
+ try {
+ URI uri = new URI(bind);
+ return Integer.toString(uri.getPort());
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+ case HOST -> {
+ String bind = System.getenv(FTL_BIND);
+ if (bind == null) {
+ return null;
+ }
+ try {
+ URI uri = new URI(bind);
+ return uri.getHost();
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+ }
+ if (s.startsWith("quarkus.datasource")) {
+ System.out.println("prop: " + s);
+ switch (s) {
+ case DEFAULT_USER -> {
+ return Optional.ofNullable(controller.getDatasource("default")).map(FTLController.Datasource::username)
+ .orElse(null);
+ }
+ case DEFAULT_PASSWORD -> {
+ return Optional.ofNullable(controller.getDatasource("default")).map(FTLController.Datasource::password)
+ .orElse(null);
+ }
+ case DEFAULT_URL -> {
+ return Optional.ofNullable(controller.getDatasource("default"))
+ .map(FTLController.Datasource::connectionString)
+ .orElse(null);
+ }
+ //TODO: just support the default datasource for now
+ }
+ Matcher m = USER_PATTERN.matcher(s);
+ if (m.matches()) {
+ System.out.println("match: " + s);
+ return Optional.ofNullable(controller.getDatasource(m.group(1))).map(FTLController.Datasource::username)
+ .orElse(null);
+ }
+ m = PASSWORD_PATTERN.matcher(s);
+ if (m.matches()) {
+ System.out.println("match: " + s);
+ return Optional.ofNullable(controller.getDatasource(m.group(1))).map(FTLController.Datasource::password)
+ .orElse(null);
+ }
+ m = URL_PATTERN.matcher(s);
+ if (m.matches()) {
+ System.out.println("match: " + s);
+ return Optional.ofNullable(controller.getDatasource(m.group(1))).map(FTLController.Datasource::connectionString)
+ .orElse(null);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "FTL Config";
+ }
+}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactory.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactory.java
new file mode 100644
index 0000000000..9ddb550bf6
--- /dev/null
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactory.java
@@ -0,0 +1,18 @@
+package xyz.block.ftl.runtime.config;
+
+import java.util.List;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+import io.smallrye.config.ConfigSourceContext;
+import io.smallrye.config.ConfigSourceFactory;
+import xyz.block.ftl.runtime.FTLController;
+
+public class FTLConfigSourceFactory implements ConfigSourceFactory {
+
+ @Override
+ public Iterable getConfigSources(ConfigSourceContext context) {
+ var controller = FTLController.instance();
+ return List.of(new FTLConfigSource(controller));
+ }
+}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactoryBuilder.java b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactoryBuilder.java
new file mode 100644
index 0000000000..bbb1fdf518
--- /dev/null
+++ b/jvm-runtime/ftl-runtime/common/runtime/src/main/java/xyz/block/ftl/runtime/config/FTLConfigSourceFactoryBuilder.java
@@ -0,0 +1,12 @@
+package xyz.block.ftl.runtime.config;
+
+import io.quarkus.runtime.configuration.ConfigBuilder;
+import io.smallrye.config.SmallRyeConfigBuilder;
+
+public class FTLConfigSourceFactoryBuilder implements ConfigBuilder {
+ @Override
+ public SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder) {
+ builder.withSources(new FTLConfigSourceFactory());
+ return builder;
+ }
+}
diff --git a/jvm-runtime/ftl-runtime/common/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/jvm-runtime/ftl-runtime/common/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
deleted file mode 100644
index 28ef804d0b..0000000000
--- a/jvm-runtime/ftl-runtime/common/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
+++ /dev/null
@@ -1 +0,0 @@
-xyz.block.ftl.runtime.FTLConfigSource
\ No newline at end of file
diff --git a/jvm-runtime/ftl-runtime/pom.xml b/jvm-runtime/ftl-runtime/pom.xml
index 52b82ca84a..6c85daa8bc 100644
--- a/jvm-runtime/ftl-runtime/pom.xml
+++ b/jvm-runtime/ftl-runtime/pom.xml
@@ -21,7 +21,7 @@
17
UTF-8
UTF-8
- 3.12.3
+ 3.13.2
3.2.5
${basedir}/../../../..
1.65.1
diff --git a/jvm-runtime/ftl-runtime/test-framework/src/main/java/xyz/block/ftl/java/test/internal/FTLTestResource.java b/jvm-runtime/ftl-runtime/test-framework/src/main/java/xyz/block/ftl/java/test/internal/FTLTestResource.java
index f7fb23accf..fd13b1493b 100644
--- a/jvm-runtime/ftl-runtime/test-framework/src/main/java/xyz/block/ftl/java/test/internal/FTLTestResource.java
+++ b/jvm-runtime/ftl-runtime/test-framework/src/main/java/xyz/block/ftl/java/test/internal/FTLTestResource.java
@@ -12,7 +12,9 @@ public class FTLTestResource implements QuarkusTestResourceLifecycleManager {
public Map start() {
server = new FTLTestServer();
server.start();
- return Map.of("ftl.endpoint", "http://127.0.0.1:" + server.getPort());
+ String endpoint = "http://127.0.0.1:" + server.getPort();
+ System.setProperty("ftl.test.endpoint", endpoint);
+ return Map.of("ftl.endpoint", endpoint);
}
@Override
diff --git a/jvm-runtime/testdata/java/passthrough/pom.xml b/jvm-runtime/testdata/java/passthrough/pom.xml
index 29b323fd33..49218c0d72 100644
--- a/jvm-runtime/testdata/java/passthrough/pom.xml
+++ b/jvm-runtime/testdata/java/passthrough/pom.xml
@@ -11,4 +11,15 @@
1.0-SNAPSHOT
+
+
+ io.quarkus
+ quarkus-hibernate-orm
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+