Skip to content

Commit 571af0b

Browse files
committed
sanity
1 parent bd58bcb commit 571af0b

File tree

9 files changed

+119
-108
lines changed

9 files changed

+119
-108
lines changed

airbyte-db/db-lib/src/main/java/io/airbyte/db/jdbc/JdbcUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class JdbcUtils {
3434
public static final String USERNAME_KEY = "username";
3535
public static final String MODE_KEY = "mode";
3636
public static final String AMPERSAND = "&";
37+
public static final String EQUALS = "=";
3738
private static final JdbcSourceOperations defaultSourceOperations = new JdbcSourceOperations();
3839

3940
private static final JSONFormat defaultJSONFormat = new JSONFormat().recordFormat(JSONFormat.RecordFormat.OBJECT);

airbyte-integrations/connectors/source-jdbc/src/main/java/io/airbyte/integrations/source/jdbc/AbstractJdbcSource.java

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static io.airbyte.db.jdbc.JdbcConstants.JDBC_COLUMN_SIZE;
1818
import static io.airbyte.db.jdbc.JdbcConstants.JDBC_COLUMN_TABLE_NAME;
1919
import static io.airbyte.db.jdbc.JdbcConstants.JDBC_COLUMN_TYPE_NAME;
20+
import static io.airbyte.db.jdbc.JdbcUtils.EQUALS;
2021

2122
import com.fasterxml.jackson.databind.JsonNode;
2223
import com.google.common.collect.ImmutableList;
@@ -72,6 +73,42 @@
7273
* for a relational DB which has a JDBC driver, make an effort to use this class.
7374
*/
7475
public abstract class AbstractJdbcSource<Datatype> extends AbstractRelationalDbSource<Datatype, JdbcDatabase> implements Source {
76+
public static final String SSL_MODE = "sslMode";
77+
78+
public static final String TRUST_KEY_STORE_URL = "trustCertificateKeyStoreUrl";
79+
public static final String TRUST_KEY_STORE_PASS = "trustCertificateKeyStorePassword";
80+
public static final String CLIENT_KEY_STORE_URL = "clientCertificateKeyStoreUrl";
81+
public static final String CLIENT_KEY_STORE_PASS = "clientCertificateKeyStorePassword";
82+
public static final String CLIENT_KEY_STORE_TYPE = "clientCertificateKeyStoreType";
83+
public static final String TRUST_KEY_STORE_TYPE = "trustCertificateKeyStoreType";
84+
public static final String KEY_STORE_TYPE_PKCS12 = "PKCS12";
85+
public static final String PARAM_MODE = "mode";
86+
Pair<URI, String> caCertKeyStorePair;
87+
Pair<URI, String> clientCertKeyStorePair;
88+
89+
public enum SslMode {
90+
91+
DISABLED("disable"),
92+
ALLOWED("allow"),
93+
PREFERRED("preferred", "prefer"),
94+
REQUIRED("required", "require"),
95+
VERIFY_CA("verify_ca", "verify-ca"),
96+
VERIFY_IDENTITY("verify_identity", "verify-full");
97+
98+
public final List<String> spec;
99+
100+
SslMode(final String... spec) {
101+
this.spec = Arrays.asList(spec);
102+
}
103+
104+
public static Optional<SslMode> bySpec(final String spec) {
105+
return Arrays.stream(SslMode.values())
106+
.filter(sslMode -> sslMode.spec.contains(spec))
107+
.findFirst();
108+
}
109+
110+
}
111+
75112

76113
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJdbcSource.class);
77114

@@ -385,50 +422,14 @@ public void close() {
385422
dataSources.clear();
386423
}
387424

388-
public static final String SSL_MODE = "sslMode";
389-
390-
public static final String TRUST_KEY_STORE_URL = "trustCertificateKeyStoreUrl";
391-
public static final String TRUST_KEY_STORE_PASS = "trustCertificateKeyStorePassword";
392-
public static final String CLIENT_KEY_STORE_URL = "clientCertificateKeyStoreUrl";
393-
public static final String CLIENT_KEY_STORE_PASS = "clientCertificateKeyStorePassword";
394-
public static final String CLIENT_KEY_STORE_TYPE = "clientCertificateKeyStoreType";
395-
public static final String TRUST_KEY_STORE_TYPE = "trustCertificateKeyStoreType";
396-
public static final String KEY_STORE_TYPE_PKCS12 = "PKCS12";
397-
public static final String PARAM_MODE = "mode";
398-
Pair<URI, String> caCertKeyStorePair;
399-
Pair<URI, String> clientCertKeyStorePair;
400-
401-
public enum SslMode {
402-
403-
DISABLED("disable"),
404-
ALLOWED("allow"),
405-
PREFERRED("preferred", "prefer"),
406-
REQUIRED("required", "require"),
407-
VERIFY_CA("verify_ca", "verify-ca"),
408-
VERIFY_IDENTITY("verify_identity", "verify-full");
409-
410-
public final List<String> spec;
411-
412-
SslMode(final String... spec) {
413-
this.spec = Arrays.asList(spec);
414-
}
415-
416-
public static Optional<SslMode> bySpec(final String spec) {
417-
return Arrays.stream(SslMode.values())
418-
.filter(sslMode -> sslMode.spec.contains(spec))
419-
.findFirst();
420-
}
421-
422-
}
423-
424425
/**
425426
* Parses SSL related configuration and generates keystores to be used by connector
426427
*
427428
* @param config configuration
428429
* @return map containing relevant parsed values including location of keystore or an empty map
429430
*/
430431
public Map<String, String> parseSSLConfig(final JsonNode config) {
431-
LOGGER.info("*** config: {}", config);
432+
LOGGER.debug("source config: {}", config);
432433
final Map<String, String> additionalParameters = new HashMap<>();
433434
// assume ssl if not explicitly mentioned.
434435
if (!config.has(JdbcUtils.SSL_KEY) || config.get(JdbcUtils.SSL_KEY).asBoolean()) {
@@ -442,7 +443,7 @@ public Map<String, String> parseSSLConfig(final JsonNode config) {
442443
}
443444

444445
if (Objects.nonNull(caCertKeyStorePair)) {
445-
LOGGER.info("*** uri for ca cert keystore: {}", caCertKeyStorePair.getLeft().toString());
446+
LOGGER.debug("uri for ca cert keystore: {}", caCertKeyStorePair.getLeft().toString());
446447
try {
447448
additionalParameters.putAll(Map.of(
448449
TRUST_KEY_STORE_URL, caCertKeyStorePair.getLeft().toURL().toString(),
@@ -459,7 +460,7 @@ public Map<String, String> parseSSLConfig(final JsonNode config) {
459460
}
460461

461462
if (Objects.nonNull(clientCertKeyStorePair)) {
462-
LOGGER.info("*** uri for client cert keystore: {} / {}", clientCertKeyStorePair.getLeft().toString(), clientCertKeyStorePair.getRight());
463+
LOGGER.debug("uri for client cert keystore: {} / {}", clientCertKeyStorePair.getLeft().toString(), clientCertKeyStorePair.getRight());
463464
try {
464465
additionalParameters.putAll(Map.of(
465466
CLIENT_KEY_STORE_URL, clientCertKeyStorePair.getLeft().toURL().toString(),
@@ -473,7 +474,7 @@ public Map<String, String> parseSSLConfig(final JsonNode config) {
473474
additionalParameters.put(SSL_MODE, SslMode.DISABLED.name());
474475
}
475476
}
476-
LOGGER.info("*** additional params: {}", additionalParameters);
477+
LOGGER.debug("additional params: {}", additionalParameters);
477478
return additionalParameters;
478479
}
479480

@@ -490,9 +491,9 @@ public String toJDBCQueryParams(final Map<String, String> sslParams) {
490491
.stream()
491492
.map((entry) -> {
492493
if (entry.getKey().equals(SSL_MODE)) {
493-
return entry.getKey() + "=" + toSslJdbcParam(SslMode.valueOf(entry.getValue()));
494+
return entry.getKey() + EQUALS + toSslJdbcParam(SslMode.valueOf(entry.getValue()));
494495
} else {
495-
return entry.getKey() + "=" + entry.getValue();
496+
return entry.getKey() + EQUALS + entry.getValue();
496497
}
497498
})
498499
.collect(Collectors.joining(JdbcUtils.AMPERSAND));

airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlCdcProperties.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import static io.airbyte.integrations.source.jdbc.AbstractJdbcSource.CLIENT_KEY_STORE_PASS;
88
import static io.airbyte.integrations.source.jdbc.AbstractJdbcSource.CLIENT_KEY_STORE_URL;
9+
import static io.airbyte.integrations.source.jdbc.AbstractJdbcSource.SSL_MODE;
910
import static io.airbyte.integrations.source.jdbc.AbstractJdbcSource.TRUST_KEY_STORE_PASS;
1011
import static io.airbyte.integrations.source.jdbc.AbstractJdbcSource.TRUST_KEY_STORE_URL;
1112

1213
import com.fasterxml.jackson.databind.JsonNode;
1314
import io.airbyte.db.jdbc.JdbcDatabase;
1415
import io.airbyte.db.jdbc.JdbcUtils;
16+
import io.airbyte.integrations.source.jdbc.AbstractJdbcSource.SslMode;
1517
import java.net.URI;
1618
import java.nio.file.Path;
1719
import java.util.Properties;
@@ -62,8 +64,9 @@ static Properties getDebeziumProperties(final JdbcDatabase database) {
6264
// Check params for SSL connection in config and add properties for CDC SSL connection
6365
// https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-property-database-ssl-mode
6466
if (!sourceConfig.has(JdbcUtils.SSL_KEY) || sourceConfig.get(JdbcUtils.SSL_KEY).asBoolean()) {
65-
if (sourceConfig.has(JdbcUtils.SSL_MODE_KEY) && sourceConfig.get(JdbcUtils.SSL_MODE_KEY).has(JdbcUtils.MODE_KEY)) {
66-
props.setProperty("database.ssl.mode", sourceConfig.get(JdbcUtils.SSL_MODE_KEY).get(JdbcUtils.MODE_KEY).asText());
67+
// if (sourceConfig.has(JdbcUtils.SSL_MODE_KEY) && sourceConfig.get(JdbcUtils.SSL_MODE_KEY).has(JdbcUtils.MODE_KEY)) {
68+
if (dbConfig.has(SSL_MODE) && !dbConfig.get(SSL_MODE).asText().isEmpty()) {
69+
props.setProperty("database.sslmode", MySqlSource.toSslJdbcParamInternal(SslMode.valueOf(dbConfig.get(SSL_MODE).asText())));
6770
props.setProperty("database.history.producer.security.protocol", "SSL");
6871
props.setProperty("database.history.consumer.security.protocol", "SSL");
6972

airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ public Set<String> getExcludedInternalNameSpaces() {
208208

209209
@Override
210210
protected String toSslJdbcParam(final SslMode sslMode) {
211+
return toSslJdbcParamInternal(sslMode);
212+
}
213+
214+
protected static String toSslJdbcParamInternal(final SslMode sslMode) {
211215
final var result = switch (sslMode) {
212216
case DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY -> sslMode.name();
213217
default -> throw new IllegalArgumentException("unexpected ssl mode");

airbyte-integrations/connectors/source-postgres/src/main/java/io/airbyte/integrations/source/postgres/PostgresCdcProperties.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,18 @@ static Properties getDebeziumDefaultProperties(final JdbcDatabase database) {
4949
LOGGER.info("dbConfig: {}", dbConfig);
5050

5151
if (dbConfig.has(SSL_MODE) && !dbConfig.get(SSL_MODE).asText().isEmpty()) {
52-
LOGGER.info("sslMode: {}", dbConfig.get(SSL_MODE).asText());
52+
LOGGER.debug("sslMode: {}", dbConfig.get(SSL_MODE).asText());
5353
props.setProperty("database.sslmode", PostgresSource.toSslJdbcParamInternal(SslMode.valueOf(dbConfig.get(SSL_MODE).asText())));
5454
props.setProperty("database.history.producer.security.protocol", "SSL");
5555
props.setProperty("database.history.consumer.security.protocol", "SSL");
5656
}
5757

58-
if (dbConfig.has("ca_certificate_path") && !dbConfig.get("ca_certificate_path").asText().isEmpty()) {
59-
props.setProperty("database.sslrootcert", dbConfig.get("ca_certificate_path").asText());
58+
if (dbConfig.has(PostgresSource.CA_CERTIFICATE_PATH) && !dbConfig.get(PostgresSource.CA_CERTIFICATE_PATH).asText().isEmpty()) {
59+
props.setProperty("database.sslrootcert", dbConfig.get(PostgresSource.CA_CERTIFICATE_PATH).asText());
6060
props.setProperty("database.history.producer.ssl.truststore.location",
61-
dbConfig.get("ca_certificate_path").asText());
61+
dbConfig.get(PostgresSource.CA_CERTIFICATE_PATH).asText());
6262
props.setProperty("database.history.consumer.ssl.truststore.location",
63-
dbConfig.get("ca_certificate_path").asText());
63+
dbConfig.get(PostgresSource.CA_CERTIFICATE_PATH).asText());
6464
props.setProperty("database.history.producer.ssl.truststore.type", "PKCS12");
6565
props.setProperty("database.history.consumer.ssl.truststore.type", "PKCS12");
6666

airbyte-integrations/connectors/source-postgres/src/main/java/io/airbyte/integrations/source/postgres/PostgresSource.java

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
package io.airbyte.integrations.source.postgres;
66

7+
import static io.airbyte.db.jdbc.JdbcUtils.AMPERSAND;
8+
import static io.airbyte.db.jdbc.JdbcUtils.EQUALS;
79
import static io.airbyte.integrations.debezium.AirbyteDebeziumHandler.shouldUseCDC;
810
import static io.airbyte.integrations.source.jdbc.JdbcSSLConnectionUtils.PARAM_CA_CERTIFICATE;
911
import static io.airbyte.integrations.util.PostgresSslConnectionUtils.DISABLE;
1012
import static io.airbyte.integrations.util.PostgresSslConnectionUtils.PARAM_SSL_MODE;
11-
import static io.airbyte.integrations.util.PostgresSslConnectionUtils.obtainConnectionOptions;
1213
import static java.util.stream.Collectors.toList;
1314
import static java.util.stream.Collectors.toSet;
1415

@@ -72,8 +73,7 @@
7273
import java.util.Set;
7374
import java.util.function.Supplier;
7475
import java.util.stream.Collectors;
75-
import org.postgresql.ssl.DefaultJavaSSLFactory;
76-
import org.postgresql.ssl.LibPQFactory;
76+
import org.postgresql.jdbc.SslMode;
7777
import org.slf4j.Logger;
7878
import org.slf4j.LoggerFactory;
7979

@@ -82,7 +82,16 @@ public class PostgresSource extends AbstractJdbcSource<JDBCType> implements Sour
8282
private static final Logger LOGGER = LoggerFactory.getLogger(PostgresSource.class);
8383
private static final int INTERMEDIATE_STATE_EMISSION_FREQUENCY = 10_000;
8484

85+
public static final String PARAM_SSLMODE = "sslmode";
86+
public static final String PARAM_SSL = "ssl";
87+
public static final String PARAM_SSL_TRUE = "true";
88+
public static final String PARAM_SSL_FALSE = "false";
89+
public static final String SSL_ROOT_CERT = "sslrootcert";
90+
8591
static final String DRIVER_CLASS = DatabaseDriver.POSTGRESQL.getDriverClassName();
92+
public static final String CA_CERTIFICATE_PATH = "ca_certificate_path";
93+
public static final String SSL_KEY = "sslkey";
94+
public static final String SSL_PASSWORD = "sslpassword";
8695
static final Map<String, String> SSL_JDBC_PARAMETERS = ImmutableMap.of(
8796
"ssl", "true",
8897
"sslmode", "require");
@@ -117,38 +126,15 @@ public JsonNode toDatabaseConfig(final JsonNode config) {
117126
config.get(JdbcUtils.DATABASE_KEY).asText()));
118127

119128
if (config.get(JdbcUtils.JDBC_URL_PARAMS_KEY) != null && !config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText().isEmpty()) {
120-
jdbcUrl.append(config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText()).append("&");
129+
jdbcUrl.append(config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText()).append(AMPERSAND);
121130
}
122131

123132
final Map<String, String> sslParameters = parseSSLConfig(config);
124133
if (config.has(PARAM_SSL_MODE) && config.get(PARAM_SSL_MODE).has(PARAM_CA_CERTIFICATE)) {
125-
LOGGER.info("*** saving CA cert to file");
126-
sslParameters.put("ca_certificate_path", JdbcSSLConnectionUtils.fileFromCertPem(config.get(PARAM_SSL_MODE).get(PARAM_CA_CERTIFICATE).asText()).toString());
127-
LOGGER.info("*** crt file: {}", sslParameters.get("ca_certificate_path"));
134+
sslParameters.put(CA_CERTIFICATE_PATH, JdbcSSLConnectionUtils.fileFromCertPem(config.get(PARAM_SSL_MODE).get(PARAM_CA_CERTIFICATE).asText()).toString());
135+
LOGGER.debug("root ssl ca crt file: {}", sslParameters.get(CA_CERTIFICATE_PATH));
128136
}
129137

130-
// System.setProperty("javax.net.ssl.trustStore", sslParameters.get(TRUST_KEY_STORE_URL));
131-
// System.setProperty("javax.net.ssl.trustStorePassword", sslParameters.get(TRUST_KEY_STORE_PASS));
132-
133-
// // assume ssl if not explicitly mentioned.
134-
// if (!config.has(PARAM_SSL) || config.get(PARAM_SSL).asBoolean()) {
135-
// if (config.has(PARAM_SSL_MODE)) {
136-
// if (DISABLE.equals(config.get(PARAM_SSL_MODE).get(PARAM_MODE).asText())) {
137-
// additionalParameters.add("sslmode=disable");
138-
// } else {
139-
// final var parametersList = obtainConnectionOptions(config.get(PARAM_SSL_MODE))
140-
// .entrySet()
141-
// .stream()
142-
// .map(e -> e.getKey() + "=" + e.getValue())
143-
// .toList();
144-
// additionalParameters.addAll(parametersList);
145-
// }
146-
// } else {
147-
// additionalParameters.add("ssl=true");
148-
// additionalParameters.add("sslmode=require");
149-
// }
150-
// }
151-
152138
if (config.has(JdbcUtils.SCHEMAS_KEY) && config.get(JdbcUtils.SCHEMAS_KEY).isArray()) {
153139
schemas = new ArrayList<>();
154140
for (final JsonNode schema : config.get(JdbcUtils.SCHEMAS_KEY)) {
@@ -163,8 +149,7 @@ public JsonNode toDatabaseConfig(final JsonNode config) {
163149
additionalParameters.forEach(x -> jdbcUrl.append(x).append("&"));
164150

165151
jdbcUrl.append(toJDBCQueryParams(sslParameters));
166-
// jdbcUrl.append("&sslfactory=" + DefaultJavaSSLFactory.class.getCanonicalName());
167-
LOGGER.info("jdbc url: {}", jdbcUrl.toString());
152+
LOGGER.debug("jdbc url: {}", jdbcUrl.toString());
168153
final Builder<Object, Object> configBuilder = ImmutableMap.builder()
169154
.put(JdbcUtils.USERNAME_KEY, config.get(JdbcUtils.USERNAME_KEY).asText())
170155
.put(JdbcUtils.JDBC_URL_KEY, jdbcUrl.toString());
@@ -178,11 +163,6 @@ public JsonNode toDatabaseConfig(final JsonNode config) {
178163
return Jsons.jsonNode(configBuilder.build());
179164
}
180165

181-
public static final String PARAM_SSLMODE = "sslmode";
182-
public static final String PARAM_SSL = "ssl";
183-
public static final String PARAM_SSL_TRUE = "true";
184-
public static final String PARAM_SSL_FALSE = "false";
185-
186166
@Override
187167
public String toJDBCQueryParams(final Map<String, String> sslParams) {
188168
return Objects.isNull(sslParams) ? ""
@@ -191,11 +171,11 @@ public String toJDBCQueryParams(final Map<String, String> sslParams) {
191171
.map((entry) -> {
192172
try {
193173
final String result = switch (entry.getKey()) {
194-
case SSL_MODE -> PARAM_SSLMODE + "=" + toSslJdbcParam(SslMode.valueOf(entry.getValue()))
195-
+ JdbcUtils.AMPERSAND + PARAM_SSL + "=" + (entry.getValue() == DISABLE ? PARAM_SSL_FALSE : PARAM_SSL_TRUE);
196-
case "ca_certificate_path" -> "sslrootcert" + "=" + entry.getValue();
197-
case CLIENT_KEY_STORE_URL -> "sslkey" + "=" + Path.of(new URI(entry.getValue()));
198-
case CLIENT_KEY_STORE_PASS -> "sslpassword" + "=" + entry.getValue();
174+
case SSL_MODE -> PARAM_SSLMODE + EQUALS + toSslJdbcParam(SslMode.valueOf(entry.getValue()))
175+
+ JdbcUtils.AMPERSAND + PARAM_SSL + EQUALS + (entry.getValue() == DISABLE ? PARAM_SSL_FALSE : PARAM_SSL_TRUE);
176+
case CA_CERTIFICATE_PATH -> SSL_ROOT_CERT + EQUALS + entry.getValue();
177+
case CLIENT_KEY_STORE_URL -> SSL_KEY + EQUALS + Path.of(new URI(entry.getValue()));
178+
case CLIENT_KEY_STORE_PASS -> SSL_PASSWORD + EQUALS + entry.getValue();
199179
default -> "";
200180
};
201181
return result;
@@ -344,7 +324,6 @@ public List<AutoCloseableIterator<AirbyteMessage>> getIncrementalIterators(final
344324
final Map<String, TableInfo<CommonField<JDBCType>>> tableNameToTable,
345325
final StateManager stateManager,
346326
final Instant emittedAt) {
347-
LOGGER.info("*** getIncrementalIterators");
348327
final JsonNode sourceConfig = database.getSourceConfig();
349328
if (PostgresUtils.isCdc(sourceConfig) && shouldUseCDC(catalog)) {
350329
final Duration firstRecordWaitTime = PostgresUtils.getFirstRecordWaitTime(sourceConfig);
@@ -511,12 +490,12 @@ protected String toSslJdbcParam(final SslMode sslMode) {
511490

512491
protected static String toSslJdbcParamInternal(final SslMode sslMode) {
513492
final var result = switch (sslMode) {
514-
case DISABLED -> "disable";
515-
case ALLOWED -> "allow";
516-
case PREFERRED -> "prefer";
517-
case REQUIRED -> "require";
518-
case VERIFY_CA -> "verify-ca";
519-
case VERIFY_IDENTITY -> "verify-full";
493+
case DISABLED -> org.postgresql.jdbc.SslMode.DISABLE.value;
494+
case ALLOWED -> org.postgresql.jdbc.SslMode.ALLOW.value;
495+
case PREFERRED -> org.postgresql.jdbc.SslMode.PREFER.value;
496+
case REQUIRED -> org.postgresql.jdbc.SslMode.REQUIRE.value;
497+
case VERIFY_CA -> org.postgresql.jdbc.SslMode.VERIFY_CA.value;
498+
case VERIFY_IDENTITY -> org.postgresql.jdbc.SslMode.VERIFY_FULL.value;
520499
default -> throw new IllegalArgumentException("unexpected ssl mode");
521500
};
522501
LOGGER.info("{} toSslJdbcParam {}", sslMode.name(), result);

0 commit comments

Comments
 (0)