diff --git a/src/driver/src/main/java/com/edgedb/driver/EdgeDBClient.java b/src/driver/src/main/java/com/edgedb/driver/EdgeDBClient.java index 765763e7..0e40533c 100644 --- a/src/driver/src/main/java/com/edgedb/driver/EdgeDBClient.java +++ b/src/driver/src/main/java/com/edgedb/driver/EdgeDBClient.java @@ -233,13 +233,21 @@ public CompletionStage transaction(@NotNull Function { - public final BaseEdgeDBClient client; - public final U result; + private final BaseEdgeDBClient client; + private final @Nullable U result; - private ExecutePair(BaseEdgeDBClient client, U result) { + private ExecutePair(BaseEdgeDBClient client, @Nullable U result) { this.client = client; this.result = result; } + + public @Nullable U getResult() { + return result; + } + + public BaseEdgeDBClient getClient() { + return client; + } } private CompletionStage executePooledQuery( @@ -254,21 +262,18 @@ private CompletionStage executePooledQuery( query, args, capabilities - ).handle((r, x) -> new ExecutePair<>(client, r)) + ).thenApply(r -> new ExecutePair<>(client, r)) ) - .handle((pair, exc) -> { - try { - pair.client.close(); - } catch (Exception e) { - throw new CompletionException(e); + .whenComplete((entry, exc) -> { + if(entry != null) { + try { + entry.getClient().close(); + } catch (Exception e) { + throw new CompletionException(e); + } } - - if(exc != null) { - throw new CompletionException(exc); - } - - return pair.result; - }); + }) + .thenApply(ExecutePair::getResult); } @Override diff --git a/src/driver/src/main/java/com/edgedb/driver/EdgeDBConnection.java b/src/driver/src/main/java/com/edgedb/driver/EdgeDBConnection.java index 6015505c..50f7378a 100644 --- a/src/driver/src/main/java/com/edgedb/driver/EdgeDBConnection.java +++ b/src/driver/src/main/java/com/edgedb/driver/EdgeDBConnection.java @@ -1,13 +1,11 @@ package com.edgedb.driver; import com.edgedb.driver.exceptions.ConfigurationException; -import com.edgedb.driver.util.ConfigUtils; -import com.edgedb.driver.util.EnumsUtil; -import com.edgedb.driver.util.QueryParamUtils; -import com.edgedb.driver.util.StringsUtil; +import com.edgedb.driver.util.*; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.json.JsonMapper; @@ -21,8 +19,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; @@ -49,6 +46,10 @@ public class EdgeDBConnection implements Cloneable { private static final String EDGEDB_HOST_ENV_NAME = "EDGEDB_HOST"; private static final String EDGEDB_PORT_ENV_NAME = "EDGEDB_PORT"; + private static final String EDGEDB_CLOUD_PROFILE_ENV_NAME = "EDGEDB_CLOUD_PROFILE"; + private static final String EDGEDB_SECRET_KEY_ENV_NAME = "EDGEDB_SECRET_KEY"; + private static final int DOMAIN_NAME_MAX_LEN = 62; + private static final Pattern DSN_FORMATTER = Pattern.compile("^([a-z]+)://"); private static final Pattern DSN_QUERY_PARAMETERS = Pattern.compile("((?:.(?!\\?))+$)"); private static final Pattern DSN_FILE_ARG = Pattern.compile("(.*?)_file"); @@ -109,6 +110,12 @@ public EdgeDBConnection() { } @JsonProperty("tls_security") private @Nullable TLSSecurityMode tlsSecurity; + @JsonIgnore + private @Nullable String secretKey; + + @JsonIgnore + private @Nullable String cloudProfile; + /** * Gets the current connections' username field. * @return The username part of the connection. @@ -232,6 +239,38 @@ protected void setTLSSecurity(TLSSecurityMode value) { tlsSecurity = value; } + /** + * Gets the secret key used to authenticate with cloud instances. + * @return The secret key if present; otherwise {@code null}. + */ + public @Nullable String getSecretKey() { + return this.secretKey; + } + + /** + * Sets the secret key used to authenticate with cloud instances. + * @param secretKey The secret key for cloud authentication. + */ + protected void setSecretKey(@Nullable String secretKey) { + this.secretKey = secretKey; + } + + /** + * Gets the name of the cloud profile to use to resolve the secret key. + * @return The cloud profile if present; otherwise {@code null}. + */ + public @Nullable String getCloudProfile() { + return this.cloudProfile == null ? "default" : this.cloudProfile; + } + + /** + * Sets the name of the cloud profile to use to resolve the secret key. + * @param cloudProfile The name of the cloud profile. + */ + protected void setCloudProfile(@Nullable String cloudProfile) { + this.cloudProfile = cloudProfile; + } + /** * Creates a {@linkplain EdgeDBConnection} from a given DSN string. * @param dsn The DSN to create the connection from. @@ -377,9 +416,10 @@ else if (envMatch.matches()) { * @param path The path to the {@code edgedb.toml} file * @return A {@linkplain EdgeDBConnection} that targets the instance hosting the project specified by the * {@code edgedb.toml} file. - * @throws IOException The project file or one of its dependants doesn't exist + * @throws IOException The project file or one of its dependants doesn't exist. + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid. */ - public static EdgeDBConnection fromProjectFile(@NotNull Path path) throws IOException { + public static EdgeDBConnection fromProjectFile(@NotNull Path path) throws IOException, ConfigurationException { return fromProjectFile(path.toFile()); } @@ -388,9 +428,10 @@ public static EdgeDBConnection fromProjectFile(@NotNull Path path) throws IOExce * @param path The path to the {@code edgedb.toml} file * @return A {@linkplain EdgeDBConnection} that targets the instance hosting the project specified by the * {@code edgedb.toml} file. - * @throws IOException The project file or one of its dependants doesn't exist + * @throws IOException The project file or one of its dependants doesn't exist. + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid. */ - public static EdgeDBConnection fromProjectFile(@NotNull String path) throws IOException { + public static EdgeDBConnection fromProjectFile(@NotNull String path) throws IOException, ConfigurationException { return fromProjectFile(new File(path)); } @@ -400,8 +441,9 @@ public static EdgeDBConnection fromProjectFile(@NotNull String path) throws IOEx * @return A {@linkplain EdgeDBConnection} that targets the instance hosting the project specified by the * {@code edgedb.toml} file. * @throws IOException The project file or one of its dependants doesn't exist + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid. */ - public static EdgeDBConnection fromProjectFile(@NotNull File file) throws IOException { + public static EdgeDBConnection fromProjectFile(@NotNull File file) throws IOException, ConfigurationException { if(!file.exists()) { throw new FileNotFoundException("Couldn't find the specified project file"); } @@ -416,9 +458,13 @@ public static EdgeDBConnection fromProjectFile(@NotNull File file) throws IOExce throw new FileNotFoundException(String.format("Couldn't find project directory for %s: %s", file, projectDir)); } - var instanceName = Files.readString(projectDir.resolve("instance-name"), StandardCharsets.UTF_8); + var instanceDetails = ConfigUtils.tryResolveInstanceCloudProfile(projectDir); - return fromInstanceName(instanceName); + if(instanceDetails.isEmpty() || instanceDetails.get().getLinkedInstanceName() == null) { + throw new FileNotFoundException("Could not find instance name under project directory " + projectDir); + } + + return fromInstanceName(instanceDetails.get().getLinkedInstanceName(), instanceDetails.get().getProfile()); } /** @@ -426,14 +472,39 @@ public static EdgeDBConnection fromProjectFile(@NotNull File file) throws IOExce * @param instanceName The name of the instance. * @return A {@linkplain EdgeDBConnection} that targets the specified instance. * @throws IOException The instance could not be found or one of its configuration files cannot be read. + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid. + * format. */ - public static EdgeDBConnection fromInstanceName(String instanceName) throws IOException { - var configPath = Paths.get(ConfigUtils.getCredentialsDir(), instanceName + ".json"); - - if(!Files.exists(configPath)) - throw new FileNotFoundException("Config file couldn't be found at " + configPath); + public static EdgeDBConnection fromInstanceName(String instanceName) throws IOException, ConfigurationException { + return fromInstanceName(instanceName, null); + } - return fromJSON(Files.readString(configPath, StandardCharsets.UTF_8)); + /** + * Creates a new {@linkplain EdgeDBConnection} from an instance name. + * @param instanceName The name of the instance. + * @param cloudProfile The optional cloud profile name if the instance is a cloud instance. + * @return A {@linkplain EdgeDBConnection} that targets the specified instance. + * @throws IOException The instance could not be found or one of its configuration files cannot be read. + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid. + * format. + */ + public static EdgeDBConnection fromInstanceName(String instanceName, @Nullable String cloudProfile) throws IOException, ConfigurationException { + if(Pattern.matches("^\\w(-?\\w)*$", instanceName)) { + var configPath = Paths.get(ConfigUtils.getCredentialsDir(), instanceName + ".json"); + + if(!Files.exists(configPath)) + throw new FileNotFoundException("Config file couldn't be found at " + configPath); + + return fromJSON(Files.readString(configPath, StandardCharsets.UTF_8)); + } else if (Pattern.matches("^([A-Za-z0-9](-?[A-Za-z0-9])*)/([A-Za-z0-9](-?[A-Za-z0-9])*)$", instanceName)) { + var connection = new EdgeDBConnection(); + connection.parseCloudInstanceName(instanceName, cloudProfile); + return connection; + } else { + throw new ConfigurationException( + String.format("Invalid instance name '%s'", instanceName) + ); + } } /** @@ -441,8 +512,9 @@ public static EdgeDBConnection fromInstanceName(String instanceName) throws IOEx * file to use to create the {@linkplain EdgeDBConnection}. * @return A resolved {@linkplain EdgeDBConnection}. * @throws IOException No {@code edgedb.toml} file could be found, or one of its configuration files cannot be read. + * @throws ConfigurationException A cloud instance parameter is invalid OR the instance name is in an invalid */ - public static EdgeDBConnection resolveEdgeDBTOML() throws IOException { + public static EdgeDBConnection resolveEdgeDBTOML() throws IOException, ConfigurationException { var dir = Paths.get(System.getProperty("user.dir")); while(true) { @@ -462,6 +534,75 @@ public static EdgeDBConnection resolveEdgeDBTOML() throws IOException { } } + private void parseCloudInstanceName(String name, @Nullable String cloudProfile) throws ConfigurationException, IOException { + if(name.length() > DOMAIN_NAME_MAX_LEN) { + throw new ConfigurationException( + String.format( + "Cloud instance name must be %d characters or less in length", + DOMAIN_NAME_MAX_LEN + ) + ); + } + + var secretKey = this.secretKey; + + if(secretKey == null) { + if(cloudProfile == null) { + cloudProfile = getCloudProfile(); + } + + var profile = ConfigUtils.readCloudProfile(cloudProfile, mapper); + + if(profile.secretKey == null) { + throw new ConfigurationException( + String.format("Secret key in cloud profile '%s' cannot be null", cloudProfile) + ); + } + + secretKey = profile.secretKey; + } + + var spl = secretKey.split("\\."); + + if(spl.length < 2) { + throw new ConfigurationException("Invalid secret key: doesn't contain payload"); + } + + TypeReference> typeRef = new TypeReference<>() {}; + + var json = Base64.getDecoder().decode(spl[1]); + var jsonData = mapper.readValue(json, typeRef); + + if(!jsonData.containsKey("iss")) { + throw new ConfigurationException( + "Invalid secret key: payload doesn't contain 'iss' value" + ); + } + + name = name.toLowerCase(Locale.ROOT); + + var dnsBucket = StringsUtil.padLeft( + Integer.toString((CRCHQX.CRCHqx(name.getBytes(StandardCharsets.UTF_8), 0) % 100)), + '0', + 2 + ); + + spl = name.split("/"); + + setHostname( + String.format( + "%s--%s.c-%s.i.%s", + spl[1], + spl[0], + dnsBucket, + jsonData.get("iss") + )); + + if(this.secretKey == null) { + setSecretKey(secretKey); + } + } + /** * Parses a connection from disc, and/or the connection argument as a DSN or instance name, then applying * environment variables to the connection. @@ -543,7 +684,7 @@ public static EdgeDBConnection parse( boolean isDSN = false; - if(autoResolve) { + if(autoResolve && !((connParam != null && connParam.contains("/")) || (connParam != null && !connParam.startsWith("edgedb://")))) { try { connection = connection.mergeInto(resolveEdgeDBTOML()); } catch (IOException x) { @@ -586,13 +727,31 @@ private static EdgeDBConnection applyEnv(EdgeDBConnection connection, @NotNull F var user = getEnv.apply(EDGEDB_USER_ENV_NAME); var pass = getEnv.apply(EDGEDB_PASSWORD_ENV_NAME); var db = getEnv.apply(EDGEDB_DATABASE_ENV_NAME); + var cloudProfile = getEnv.apply(EDGEDB_CLOUD_PROFILE_ENV_NAME); + var cloudSecret = getEnv.apply(EDGEDB_SECRET_KEY_ENV_NAME); + + if(cloudProfile != null) { + connection = connection.mergeInto(new EdgeDBConnection(){{ + setCloudProfile(cloudProfile); + }}); + } + + if(cloudSecret != null) { + connection = connection.mergeInto(new EdgeDBConnection(){{ + setSecretKey(cloudSecret); + }}); + } if(instanceName != null) { connection = connection.mergeInto(fromInstanceName(instanceName)); } if(dsn != null) { - connection = connection.mergeInto(fromDSN(dsn)); + if(Pattern.matches("^([A-Za-z0-9](-?[A-Za-z0-9])*)/([A-Za-z0-9](-?[A-Za-z0-9])*)$", dsn)) { + connection.parseCloudInstanceName(dsn, null); + } else { + connection = connection.mergeInto(fromDSN(dsn)); + } } if(host != null) { diff --git a/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBBinaryClient.java b/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBBinaryClient.java index e087766c..f08ad960 100644 --- a/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBBinaryClient.java +++ b/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBBinaryClient.java @@ -916,14 +916,22 @@ private CompletionStage connectInternal() { this.duplexer.reset(); return retryableConnect() - .thenApply(v -> getConnectionArguments()) - .thenApply(connection -> new ClientHandshake( + .thenApply(v -> { + var connection = getConnectionArguments(); + return connection.getSecretKey() != null + ? new ConnectionParam[] { + new ConnectionParam("user", connection.getUsername()), + new ConnectionParam("database", connection.getDatabase()), + new ConnectionParam("secret_key", connection.getSecretKey()) + } : new ConnectionParam[] { + new ConnectionParam("user", connection.getUsername()), + new ConnectionParam("database", connection.getDatabase()) + }; + }) + .thenApply(connectionParams -> new ClientHandshake( PROTOCOL_MAJOR_VERSION, PROTOCOL_MINOR_VERSION, - new ConnectionParam[] { - new ConnectionParam("user", connection.getUsername()), - new ConnectionParam("database", connection.getDatabase()) - }, + connectionParams, new ProtocolExtension[0] )) .thenCompose(this.duplexer::send); diff --git a/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBTCPClient.java b/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBTCPClient.java index 842376a2..c3f10575 100644 --- a/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBTCPClient.java +++ b/src/driver/src/main/java/com/edgedb/driver/clients/EdgeDBTCPClient.java @@ -15,7 +15,6 @@ import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslProvider; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; import org.jetbrains.annotations.NotNull; @@ -54,7 +53,6 @@ protected void initChannel(@NotNull SocketChannel ch) throws Exception { var pipeline = ch.pipeline(); var builder = SslContextBuilder.forClient() - .sslProvider(SslProvider.JDK) .protocols("TLSv1.3") .applicationProtocolConfig(new ApplicationProtocolConfig( ApplicationProtocolConfig.Protocol.ALPN, @@ -65,7 +63,14 @@ protected void initChannel(@NotNull SocketChannel ch) throws Exception { SslUtils.applyTrustManager(getConnectionArguments(), builder); - pipeline.addLast("ssl", builder.build().newHandler(ch.alloc())); + pipeline.addLast( + "ssl", + builder.build().newHandler( + ch.alloc(), + getConnectionArguments().getHostname(), // SNI + getConnectionArguments().getPort() // SNI + ) + ); // edgedb-binary protocol and duplexer pipeline.addLast( diff --git a/src/driver/src/main/java/com/edgedb/driver/datatypes/internal/CloudProfile.java b/src/driver/src/main/java/com/edgedb/driver/datatypes/internal/CloudProfile.java new file mode 100644 index 00000000..609b8fe5 --- /dev/null +++ b/src/driver/src/main/java/com/edgedb/driver/datatypes/internal/CloudProfile.java @@ -0,0 +1,8 @@ +package com.edgedb.driver.datatypes.internal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CloudProfile { + @JsonProperty("secret_key") + public String secretKey; +} diff --git a/src/driver/src/main/java/com/edgedb/driver/util/CRCHQX.java b/src/driver/src/main/java/com/edgedb/driver/util/CRCHQX.java new file mode 100644 index 00000000..904fae01 --- /dev/null +++ b/src/driver/src/main/java/com/edgedb/driver/util/CRCHQX.java @@ -0,0 +1,49 @@ +package com.edgedb.driver.util; + +public final class CRCHQX { + private static final int[] CRC_TAB_HQX = new int[]{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, + }; + + public static int CRCHqx(byte[] data, int crc) { + crc &= 0xffff; + + int i = 0; + while(i < data.length) { + crc = ((crc << 8) & 0xff00) ^ CRC_TAB_HQX[(crc >> 8) ^ data[i++]]; + } + + return crc; + } +} diff --git a/src/driver/src/main/java/com/edgedb/driver/util/ConfigUtils.java b/src/driver/src/main/java/com/edgedb/driver/util/ConfigUtils.java index b096d7fb..744473b0 100644 --- a/src/driver/src/main/java/com/edgedb/driver/util/ConfigUtils.java +++ b/src/driver/src/main/java/com/edgedb/driver/util/ConfigUtils.java @@ -3,12 +3,19 @@ import com.edgedb.driver.abstractions.OSType; import com.edgedb.driver.abstractions.SystemProvider; import com.edgedb.driver.abstractions.impl.DefaultSystemProvider; +import com.edgedb.driver.datatypes.internal.CloudProfile; +import com.edgedb.driver.exceptions.ConfigurationException; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Optional; import java.util.regex.Pattern; public class ConfigUtils { @@ -67,6 +74,7 @@ private static String getEdgeDBBasePath(@Nullable SystemProvider systemProvider) ? basePath : provider.combinePaths(provider.getHomeDir(), ".edgedb"); } + private static String getEdgeDBKnownBasePath(@Nullable SystemProvider systemProvider) { var provider = systemProvider == null ? DEFAULT_SYSTEM_PROVIDER : systemProvider; @@ -86,4 +94,105 @@ else if (provider.isOSPlatform(OSType.OSX)) { return provider.combinePaths(xdgConfigDir, "edgedb"); } } + + public static Optional tryResolveInstanceTOML() { + return tryResolveInstanceTOML(Path.of(System.getProperty("user.dir"))); + } + + public static Optional tryResolveInstanceTOML(Path startDir) { + Path dir = startDir; + + while(true) { + var target = dir.resolve("edgedb.toml"); + + if(target.toFile().exists()) { + return Optional.of(target); + } + + var parent = target.getParent(); + + if(parent == null || !Files.exists(parent)) { + break; + } + + dir = parent; + } + + return Optional.empty(); + } + + public static class CloudInstanceDetails { + private final @Nullable String profile; + private final @Nullable String linkedInstanceName; + public CloudInstanceDetails(@Nullable String profile, @Nullable String linkedInstanceName) { + this.profile = profile; + this.linkedInstanceName = linkedInstanceName; + } + + public @Nullable String getProfile() { + return profile; + } + + public @Nullable String getLinkedInstanceName() { + return linkedInstanceName; + } + } + + public static Optional tryResolveInstanceCloudProfile() throws IOException { + var instanceToml = tryResolveInstanceTOML(); + + if(instanceToml.isEmpty()) { + return Optional.empty(); + } + + var stashDir = + getInstanceProjectDirectory( + getInstanceProjectDirectory(instanceToml.get().getParent().toString()) + ); + + return tryResolveInstanceCloudProfile(Path.of(stashDir)); + } + + public static Optional tryResolveInstanceCloudProfile(Path stashDir) throws IOException { + if(!Files.exists(stashDir)) { + return Optional.empty(); + } + + String profile = null; + String linkedInstanceName = null; + + var cloudProfilePath = stashDir.resolve("cloud-profile"); + + if(Files.exists(cloudProfilePath)) { + profile = Files.readString(cloudProfilePath); + } + + var linkedInstancePath = stashDir.resolve("instance-name"); + + if(Files.exists(linkedInstancePath)) { + linkedInstanceName = Files.readString(linkedInstancePath); + } + + return profile == null && linkedInstanceName == null + ? Optional.empty() + : Optional.of(new CloudInstanceDetails(profile, linkedInstanceName)); + } + + public static CloudProfile readCloudProfile(String profile, JsonMapper mapper) + throws ConfigurationException, IOException { + return readCloudProfile(profile, DEFAULT_SYSTEM_PROVIDER, mapper); + } + + public static CloudProfile readCloudProfile(String profile, SystemProvider provider, JsonMapper mapper) + throws ConfigurationException, IOException { + var profilePath = Path.of(provider.combinePaths(getEdgeDBConfigDir(provider), "cloud-credentials", profile + ".json")); + + if(!Files.exists(profilePath)) { + throw new ConfigurationException( + String.format("Unknown cloud profile '%s'", profile) + ); + } + + return mapper.readValue(Files.readString(profilePath), CloudProfile.class); + } }