Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,31 @@ on:
type: [opened, reopened, edited, synchronize]

jobs:
prepare:
name: Prepare Maven cache
runs-on: ubuntu-latest

env:
MAVEN_ARGS: --batch-mode -Dstyle.color=always

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
cache: 'maven'

- name: Download dependencies for JDBC
run: mvn $MAVEN_ARGS dependency:resolve-plugins dependency:go-offline

build:
name: Build JDBC Driver
runs-on: ubuntu-latest
needs: prepare

strategy:
matrix:
Expand Down Expand Up @@ -75,9 +97,6 @@ jobs:
if: env.NEED_SDK == 'true'
run: rm -rf yc

- name: Download dependencies
run: mvn $MAVEN_ARGS dependency:resolve-plugins dependency:go-offline

- name: Build with Maven
run: mvn $MAVEN_ARGS package

Expand Down Expand Up @@ -144,9 +163,6 @@ jobs:
if: env.NEED_SDK == 'true'
run: rm -rf yc

- name: Download dependencies
run: mvn $MAVEN_ARGS dependency:resolve-plugins dependency:go-offline

- name: Build with Maven
run: mvn $MAVEN_ARGS test

Expand Down
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
1) Drop in [JDBC driver](https://github.com/ydb-platform/ydb-jdbc-driver/releases) to classpath or pick this file in IDE
2) Connect to YDB
* Local or remote Docker (anonymous authentication):<br>`jdbc:ydb:grpc://localhost:2136/local`
* Self-hosted cluster:<br>`jdbc:ydb:grpcs://<host>:2135/Root/testdb?secureConnectionCertificate=file:~/myca.cer`
* Connect with token to the cloud instance:<br>`jdbc:ydb:grpcs://<host>:2135/path/to/database?token=file:~/my_token`
* Connect with service account to the cloud instance:<br>`jdbc:ydb:grpcs://<host>:2135/path/to/database?saFile=file:~/sa_key.json`
* Self-hosted cluster:<br>`jdbc:ydb:grpcs://<host>:2135/Root/testdb?secureConnectionCertificate=~/myca.cer`
* Connect with token to the cloud instance:<br>`jdbc:ydb:grpcs://<host>:2135/path/to/database?tokenFile=~/my_token`
* Connect with service account to the cloud instance:<br>`jdbc:ydb:grpcs://<host>:2135/path/to/database?saKeyFile=~/sa_key.json`
3) Execute queries, see example in [YdbDriverExampleTest.java](jdbc/src/test/java/tech/ydb/jdbc/YdbDriverExampleTest.java)

### Usage with Maven
Expand Down Expand Up @@ -53,20 +53,15 @@ YDB JDBC Driver supports the following [authentication modes](https://ydb.tech/e
### Driver properties reference

Driver supports the following configuration properties, which can be specified in the URL or passed via extra properties:
* `saFile` - service account key for authentication, can be passed either as literal JSON value or as a file reference;
* `saKeyFile` - file location of service account key for authentication;
* `tokenFile` - file location with token value for token based authentication;
* `iamEndpoint` - custom IAM endpoint for authentication via service account key;
* `token` - token value for authentication, can be passed either as literal value or as a file reference;
* `useMetadata` - boolean value, true if metadata authentication should be used, false otherwise (and default);
* `metadataURL` - custom metadata endpoint;
* `localDatacenter` - name of the datacenter local to the application being connected;
* `secureConnection` - boolean value, true if TLS should be enforced (normally configured via `grpc://` or `grpcs://` scheme in the JDBC URL);
* `secureConnectionCertificate` - custom CA certificate for TLS connections, can be passed either as literal value or as a file reference.

File references for `saFile`, `token` or `secureConnectionCertificate` must be prefixed with the `file:` URL scheme, for example:
* `saFile=file:~/mysaley1.json`
* `token=file:/opt/secret/token-file`
* `secureConnectionCertificate=file:/etc/ssl/cacert.cer`

### Building
By default all tests are run using a local YDB instance in Docker (if host has Docker or Docker Machine installed)
To disable these tests run `mvn test -DYDB_DISABLE_INTEGRATION_TESTS=true`
Expand Down
5 changes: 3 additions & 2 deletions jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public final class YdbConst {
public static final String DRIVER_IS_NOT_REGISTERED = "Driver is not registered "
+ "(or it has not been registered using YdbDriver.register() method)";

public static final String MISSING_DRIVER_OPTION = "Missing value for option ";
public static final String INVALID_DRIVER_OPTION_VALUE = "Cannot process value %s for option %s: %s";

public static final String PREPARED_CALLS_UNSUPPORTED = "Prepared calls are not supported";
public static final String ARRAYS_UNSUPPORTED = "Arrays are not supported";
public static final String STRUCTS_UNSUPPORTED = "Structs are not supported";
Expand Down Expand Up @@ -140,8 +143,6 @@ public final class YdbConst {
public static final String INDEXED_PARAMETER_PREFIX = "p";
public static final String VARIABLE_PARAMETER_PREFIX = "$";
public static final String AUTO_GENERATED_PARAMETER_PREFIX = VARIABLE_PARAMETER_PREFIX + "jp";
public static final String DEFAULT_BATCH_PARAMETER = "$values";
public static final String OPTIONAL_TYPE_SUFFIX = "?";

private YdbConst() {
//
Expand Down
8 changes: 5 additions & 3 deletions jdbc/src/main/java/tech/ydb/jdbc/YdbDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,11 @@ public int getConnectionCount() {
}

public void close() {
LOGGER.log(Level.INFO, "Closing {0} cached connection(s)...", cache.size());
cache.values().forEach(YdbContext::close);
cache.clear();
if (!cache.isEmpty()) {
LOGGER.log(Level.FINE, "Closing {0} cached connection(s)...", cache.size());
cache.values().forEach(YdbContext::close);
cache.clear();
}
}

public static boolean isRegistered() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ public StreamQueryResult(String msg, YdbStatement statement, YdbQuery query, Run

public void onStreamResultSet(int index, ResultSetReader rsr) {
CompletableFuture<Result<LazyResultSet>> future = resultFutures.get(index);

if (!future.isDone()) {
ColumnInfo[] columns = ColumnInfo.fromResultSetReader(rsr);
future.complete(Result.success(new LazyResultSet(statement, columns)));
LazyResultSet rs = new LazyResultSet(statement, columns);
rs.addResultSet(rsr);
if (future.complete(Result.success(rs))) {
return;
}
}

Result<LazyResultSet> res = future.join();
Expand Down
3 changes: 2 additions & 1 deletion jdbc/src/main/java/tech/ydb/jdbc/settings/YdbConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ public DriverPropertyInfo[] toPropertyInfo() throws SQLException {
YdbConnectionProperties.USE_SECURE_CONNECTION.toInfo(properties),
YdbConnectionProperties.SECURE_CONNECTION_CERTIFICATE.toInfo(properties),
YdbConnectionProperties.TOKEN.toInfo(properties),
YdbConnectionProperties.SERVICE_ACCOUNT_FILE.toInfo(properties),
YdbConnectionProperties.TOKEN_FILE.toInfo(properties),
YdbConnectionProperties.SA_KEY_FILE.toInfo(properties),
YdbConnectionProperties.USE_METADATA.toInfo(properties),
YdbConnectionProperties.IAM_ENDPOINT.toInfo(properties),
YdbConnectionProperties.METADATA_URL.toInfo(properties),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,42 @@

import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import tech.ydb.auth.TokenAuthProvider;
import tech.ydb.auth.iam.CloudAuthHelper;
import tech.ydb.core.auth.StaticCredentials;
import tech.ydb.core.grpc.BalancingSettings;
import tech.ydb.core.grpc.GrpcTransportBuilder;
import tech.ydb.jdbc.YdbDriver;


public class YdbConnectionProperties {
private static final Logger LOGGER = Logger.getLogger(YdbDriver.class.getName());

static final YdbProperty<String> TOKEN = YdbProperty.content(YdbConfig.TOKEN_KEY, "Authentication token");

static final YdbProperty<String> TOKEN_FILE = YdbProperty.file("tokenFile",
"Path to token file for the token-based authentication");

static final YdbProperty<String> LOCAL_DATACENTER = YdbProperty.string("localDatacenter",
"Local Datacenter");

static final YdbProperty<Boolean> USE_SECURE_CONNECTION = YdbProperty.bool("secureConnection",
"Use TLS connection");

static final YdbProperty<byte[]> SECURE_CONNECTION_CERTIFICATE = YdbProperty.bytes("secureConnectionCertificate",
"Use TLS connection with certificate from provided path");
static final YdbProperty<byte[]> SECURE_CONNECTION_CERTIFICATE = YdbProperty.fileBytes(
"secureConnectionCertificate", "Use TLS connection with certificate from provided path"
);

@Deprecated
static final YdbProperty<String> SERVICE_ACCOUNT_FILE = YdbProperty.content("saFile",
"Service account file based authentication");

static final YdbProperty<String> SA_KEY_FILE = YdbProperty.file("saKeyFile",
"Path to key file for the service account authentication");

static final YdbProperty<Boolean> USE_METADATA = YdbProperty.bool("useMetadata",
"Use metadata service for authentication");

Expand All @@ -41,7 +54,9 @@ public class YdbConnectionProperties {
private final YdbValue<Boolean> useSecureConnection;
private final YdbValue<byte[]> secureConnectionCertificate;
private final YdbValue<String> token;
private final YdbValue<String> tokenFile;
private final YdbValue<String> serviceAccountFile;
private final YdbValue<String> saKeyFile;
private final YdbValue<Boolean> useMetadata;
private final YdbValue<String> iamEndpoint;
private final YdbValue<String> metadataUrl;
Expand All @@ -56,7 +71,9 @@ public YdbConnectionProperties(YdbConfig config) throws SQLException {
this.useSecureConnection = USE_SECURE_CONNECTION.readValue(props);
this.secureConnectionCertificate = SECURE_CONNECTION_CERTIFICATE.readValue(props);
this.token = TOKEN.readValue(props);
this.tokenFile = TOKEN_FILE.readValue(props);
this.serviceAccountFile = SERVICE_ACCOUNT_FILE.readValue(props);
this.saKeyFile = SA_KEY_FILE.readValue(props);
this.useMetadata = USE_METADATA.readValue(props);
this.iamEndpoint = IAM_ENDPOINT.readValue(props);
this.metadataUrl = METADATA_URL.readValue(props);
Expand Down Expand Up @@ -87,33 +104,78 @@ public GrpcTransportBuilder applyToGrpcTransport(GrpcTransportBuilder builder) {
builder = builder.withSecureConnection(secureConnectionCertificate.getValue());
}

String usedProvider = null;

if (username != null && !username.isEmpty()) {
builder = builder.withAuthProvider(new StaticCredentials(username, password));
usedProvider = "username & password credentials";
}

if (useMetadata.hasValue()) {
if (usedProvider != null) {
LOGGER.log(Level.WARNING, "Dublicate authentication config! Metadata credentials replaces {0}",
usedProvider);
}

if (metadataUrl.hasValue()) {
String url = metadataUrl.getValue();
builder = builder.withAuthProvider(CloudAuthHelper.getMetadataAuthProvider(url));
} else {
builder = builder.withAuthProvider(CloudAuthHelper.getMetadataAuthProvider());
}
usedProvider = "metadata credentials";
}

if (tokenFile.hasValue()) {
if (usedProvider != null) {
LOGGER.log(Level.WARNING, "Dublicate authentication config! Token credentials replaces {0}",
usedProvider);
}
builder = builder.withAuthProvider(new TokenAuthProvider(tokenFile.getValue()));
usedProvider = "token file credentitals";
}

if (token.hasValue()) {
if (usedProvider != null) {
LOGGER.log(Level.WARNING, "Dublicate authentication config! Token credentials replaces {0}",
usedProvider);
}
builder = builder.withAuthProvider(new TokenAuthProvider(token.getValue()));
usedProvider = "token value credentitals";
}

if (serviceAccountFile.hasValue()) {
String json = serviceAccountFile.getValue();
if (saKeyFile.hasValue()) {
if (usedProvider != null) {
LOGGER.log(Level.WARNING, "Dublicate authentication config! Token credentials replaces {0}",
usedProvider);
}
String json = saKeyFile.getValue();
if (iamEndpoint.hasValue()) {
String endpoint = iamEndpoint.getValue();
builder = builder.withAuthProvider(CloudAuthHelper.getServiceAccountJsonAuthProvider(json, endpoint));
} else {
builder = builder.withAuthProvider(CloudAuthHelper.getServiceAccountJsonAuthProvider(json));
}
builder = builder.withAuthProvider(new TokenAuthProvider(token.getValue()));
usedProvider = "service account credentitals";
}

if (useMetadata.hasValue()) {
if (metadataUrl.hasValue()) {
String url = metadataUrl.getValue();
builder = builder.withAuthProvider(CloudAuthHelper.getMetadataAuthProvider(url));
if (serviceAccountFile.hasValue()) {
LOGGER.warning("Option 'saFile' is deprecated and will be removed in next versions. "
+ "Use options 'saKeyFile' instead");
if (usedProvider != null) {
LOGGER.log(Level.WARNING, "Dublicate authentication config! Token credentials replaces {0}",
usedProvider);
}
String json = serviceAccountFile.getValue();
if (iamEndpoint.hasValue()) {
String endpoint = iamEndpoint.getValue();
builder = builder.withAuthProvider(CloudAuthHelper.getServiceAccountJsonAuthProvider(json, endpoint));
} else {
builder = builder.withAuthProvider(CloudAuthHelper.getMetadataAuthProvider());
builder = builder.withAuthProvider(CloudAuthHelper.getServiceAccountJsonAuthProvider(json));
}
}

if (username != null && !username.isEmpty()) {
builder = builder.withAuthProvider(new StaticCredentials(username, password));
}

return builder;
}
}
56 changes: 53 additions & 3 deletions jdbc/src/main/java/tech/ydb/jdbc/settings/YdbLookup.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,78 @@
package tech.ydb.jdbc.settings;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.SQLException;
import java.util.Optional;

import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;

import tech.ydb.jdbc.YdbConst;

/**
*
* @author Aleksandr Gorshenin
*/
public class YdbLookup {
private static final String FILE_REF = "file:";
private static final String CLASSPATH_REF = "classpath:";
private static final String HOME_REF = "~";
private static final String HOME_REF = "~/";
private static final String FILE_HOME_REF = FILE_REF + HOME_REF;

private YdbLookup() { }

public static String readFileAsString(String name, String value) throws SQLException {
try (Reader reader = new InputStreamReader(readFile(name, value))) {
return CharStreams.toString(reader).trim();
} catch (IOException e) {
String msg = String.format(YdbConst.INVALID_DRIVER_OPTION_VALUE, value, name, e.getMessage());
throw new SQLException(msg);
}
}

public static byte[] readFileAsBytes(String name, String value) throws SQLException {
try (InputStream is = readFile(name, value)) {
return ByteStreams.toByteArray(is);
} catch (IOException e) {
String msg = String.format(YdbConst.INVALID_DRIVER_OPTION_VALUE, value, name, e.getMessage());
throw new SQLException(msg);
}
}

private static InputStream readFile(String name, String value) throws SQLException, IOException {
if (value == null || value.trim().isEmpty()) {
throw new SQLException(YdbConst.MISSING_DRIVER_OPTION + name);
}

String path = value.trim();
if (path.toLowerCase().startsWith(CLASSPATH_REF)) {
URL resource = ClassLoader.getSystemResource(path.substring(CLASSPATH_REF.length()));
if (resource == null) {
String msg = String.format(YdbConst.INVALID_DRIVER_OPTION_VALUE, value, name, "resource not found");
throw new SQLException(msg);
}
return resource.openStream();
}

if (path.toLowerCase().startsWith(FILE_REF)) {
path = path.substring(FILE_REF.length());
}

if (path.startsWith(HOME_REF)) {
String home = System.getProperty("user.home");
path = path.substring(HOME_REF.length() - 1) + home;
}

return new FileInputStream(path);
}


public static String stringFileReference(String ref) {
Optional<URL> urlOpt = resolvePath(ref);
if (urlOpt.isPresent()) {
Expand Down Expand Up @@ -56,8 +106,8 @@ static Optional<URL> resolvePath(String ref) {
try {
String home = System.getProperty("user.home");
String fixedRef = ref.startsWith(HOME_REF)
? ref.substring(HOME_REF.length())
: ref.substring(FILE_HOME_REF.length());
? ref.substring(HOME_REF.length() - 1)
: ref.substring(FILE_HOME_REF.length() - 1);
return Optional.of(new URL(FILE_REF + home + fixedRef));
} catch (MalformedURLException e) {
throw new RuntimeException("Unable to parse ref from home: " + ref, e);
Expand Down
Loading