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 + + +