Skip to content
Draft
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
4 changes: 4 additions & 0 deletions dbaas-client/dbaas-client-java/dbaas-client-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,9 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.netcracker.cloud.dbaas.client.management;


import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netcracker.cloud.dbaas.client.DbaasClient;
import com.netcracker.cloud.dbaas.client.DbaasConst;
import com.netcracker.cloud.dbaas.client.entity.database.AbstractConnectorSettings;
import com.netcracker.cloud.dbaas.client.entity.database.AbstractDatabase;
import com.netcracker.cloud.dbaas.client.entity.database.type.DatabaseType;
import com.netcracker.cloud.dbaas.client.service.LogicalDbProvider;
import com.netcracker.cloud.dbaas.client.service.mountedsecret.MountedSecretSource;
import com.netcracker.cloud.dbaas.client.service.mountedsecret.SecretMetadata;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -31,6 +35,22 @@ public class DatabasePool {
private final Comparator<Object> postConnectProcessorsOrder;
private List<LogicalDbProvider> dbProviders;
private Map<Class<? extends AbstractDatabase<?>>, DatabaseClientCreator<?, ?>> mapDatabaseClientCreators = new ConcurrentHashMap<>();

/**
* Reads connection properties from Secrets mounted at {@code /etc/secrets/dbaas-secrets},
* consulted before the REST call in {@link #createDatabase}. Always registered; when nothing is
* mounted it returns empty and the pool falls back to REST exactly as before.
*/
private MountedSecretSource mountedSecretSource = new MountedSecretSource();

/**
* Used to build a typed {@link AbstractDatabase} from a mounted Secret via the synthetic-response
* mechanism (a property map converted to {@code DatabaseType#getDatabaseClass()}). Unknown
* connection-property keys (e.g. {@code roHost}) are tolerated, matching the REST deserialization.
*/
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

/**
* L1 cache holds cached databases connections. When client asks for a database, we first look in L1 cache.
* Databases in this cache are ready-for-use, their post-processors have been already successfully applied.
Expand Down Expand Up @@ -178,6 +198,12 @@ protected <T, D extends AbstractDatabase<T>> D createDatabase(DatabaseKey<T, D>
return logDb;
}

D mountedDb = getDbFromMountedSecret(classifier, databaseConfig, key.getDbType());
if (mountedDb != null) {
log.debug("Logical database was obtained from mounted secret. Classifier: {}, type {}", classifier, key.getDbType());
return mountedDb;
}

databaseDefinitionHandler.applyDefinitionProcess(key.getDbType(), databaseConfig, classifier, namespace);
return dbaasClient.getOrCreateDatabase(
key.getDbType(),
Expand All @@ -186,6 +212,53 @@ protected <T, D extends AbstractDatabase<T>> D createDatabase(DatabaseKey<T, D>
databaseConfig);
}

private <T, D extends AbstractDatabase<T>> D getDbFromMountedSecret(Map<String, Object> classifier,
DatabaseConfig databaseConfig,
DatabaseType<T, D> type) {
String role = databaseConfig != null ? databaseConfig.getUserRole() : null;
return mountedSecretSource.resolve(classifier, type.getName(), role)
.map(resolved -> buildAbstractDatabase(type, classifier, resolved))
.orElse(null);
}

/**
* Builds the typed database from a mounted Secret (synthetic-response): assemble a map mirroring
* the dbaas REST response and convert it to {@code type.getDatabaseClass()} with the same
* deserialization semantics as the REST path. No provisioning and no REST call happen here.
*/
private <T, D extends AbstractDatabase<T>> D buildAbstractDatabase(DatabaseType<T, D> type,
Map<String, Object> classifier,
MountedSecretSource.Resolved resolved) {
SecretMetadata meta = resolved.metadata();
Map<String, Object> synthetic = new HashMap<>();
synthetic.put("classifier", meta.getClassifier() != null ? meta.getClassifier() : classifier);
synthetic.put("connectionProperties", resolved.connectionProperties());

String name = meta.getName() != null ? meta.getName() : asString(resolved.connectionProperties().get("name"));
if (name != null) {
synthetic.put("name", name);
}
String dbNamespace = meta.getNamespace() != null ? meta.getNamespace() : asString(classifier.get(DbaasConst.NAMESPACE));
if (dbNamespace != null) {
synthetic.put("namespace", dbNamespace);
}
if (meta.getSettings() != null) {
synthetic.put("settings", meta.getSettings());
}
return objectMapper.convertValue(synthetic, type.getDatabaseClass());
}

private static String asString(Object value) {
return value instanceof String s ? s : null;
}

/**
* Test seam: package-private so unit tests can point the mounted-secret source at a temp directory.
*/
void setMountedSecretSource(MountedSecretSource mountedSecretSource) {
this.mountedSecretSource = mountedSecretSource;
}

private Comparator<Object> getComparator() {
return postConnectProcessorsOrder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.netcracker.cloud.dbaas.client.service.mountedsecret;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

/**
* Builds the canonical lookup key used to match a request's (classifier, type, role) against a
* mounted Secret's descriptor. Both the client's own classifier and the descriptor's classifier are
* canonicalized by this same code, so they only have to agree with each other — there is no need to
* be byte-identical with any other language client.
* <p>
* Canonicalization mirrors the Go provider semantics: keys sorted recursively, {@code scope}
* lower-cased, empty top-level {@code namespace}/{@code tenantId} omitted, and empty nested objects
* dropped. Role matching is exact-string after trim (case-sensitive), and an empty role matches a
* descriptor whose {@code userRole} was left unset.
*/
@Slf4j
final class ClassifierMatcher {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Object OMIT = new Object();

private ClassifierMatcher() {
}

static String matchingKey(Map<String, Object> classifier, String type, String role) {
return canonical(classifier)
+ "|" + (type == null ? "" : type.toLowerCase(Locale.ROOT))
+ "|" + (role == null ? "" : role.strip());
}

static String canonical(Map<String, Object> classifier) {
try {
return MAPPER.writeValueAsString(normalizeMap(classifier, true));
} catch (Exception e) {
log.warn("mounted-secret: failed to canonicalize classifier {}: {}", classifier, e.toString());
return "";
}
}

private static Object normalizeMap(Map<String, Object> map, boolean topLevel) {
TreeMap<String, Object> out = new TreeMap<>();
if (map != null) {
for (Map.Entry<String, Object> e : map.entrySet()) {
Object normalized = normalizeValue(e.getKey(), e.getValue(), topLevel);
if (normalized != OMIT) {
out.put(e.getKey(), normalized);
}
}
}
// Drop empty nested objects; keep the (possibly empty) top-level object.
if (!topLevel && out.isEmpty()) {
return null;
}
return out;
}

@SuppressWarnings("unchecked")
private static Object normalizeValue(String key, Object value, boolean topLevel) {
if (value == null) {
return OMIT;
}
if (value instanceof String s) {
if ("scope".equals(key)) {
s = s.toLowerCase(Locale.ROOT);
}
if (topLevel && s.isEmpty() && ("namespace".equals(key) || "tenantId".equals(key))) {
return OMIT;
}
return s;
}
if (value instanceof Map<?, ?> nested) {
Object normalizedNested = normalizeMap((Map<String, Object>) nested, false);
return normalizedNested == null ? OMIT : normalizedNested;
}
// numbers / booleans / arrays are serialized as-is (no recursion into arrays, matching Go).
return value;
}
}
Loading
Loading