Skip to content

Commit

Permalink
[PLAT-2540] Allow for configuring WsClient used for HA using RuntimeC…
Browse files Browse the repository at this point in the history
…onfiguration

Summary:
At high level idea is to use runtime config to set **HA** specific `wsClient ` config.
This can be done by setting newly added config object `yb.ha.ws`  in GLOBAL scope using existing runtime config API to set key to HOCON or JSON object. (See `PlatformInstanceClientFactoryTest.setWsConfig()` for how to do this.)
This config object uses same schema and everything that the default configuration of ws client via `play.ws.ssl` and supports all the same features set (thru reuse)

# Solved problem of dynamically refreshing wsclient (close current and create new with updated config) whenever config changed.
# Added ability to trigger such refresh by adding API to notify on a key about config change - so far this is a strawman implementation but can be improved easily if/when more use cases present.
# Given that we had to solve #1, it made sense to allow for HA specific wsClient configuration - this way we can have separate configuration for any other wsClients (better security posture)
# This should also allow for HA specific higher timeouts.
# Note that you can use "+=" to append certificate to existing list of certificates (This is a generic feature implemented for runtime config)
# You can also use `${yb.some.config.key}` to refer to other config. Such substitution (aka config-resolution) should now work.

**WS Config Reference:**
https://github.com/playframework/play-ws/blob/main/play-ws-standalone/src/main/resources/reference.conf

Test Plan:
Added unit test (`PlatformInstanceClientFactoryTest`) for case of specific interest - ability to set certs to establish trust between HA nodes.
Itests:
https://jenkins.dev.yugabyte.com/job/dev-itest-pipeline/846
https://jenkins.dev.yugabyte.com/job/dev-itest-pipeline/847

Reviewers: amalyshev, spotachev

Reviewed By: amalyshev, spotachev

Subscribers: jenkins-bot, yugaware

Differential Revision: https://phabricator.dev.yugabyte.com/D14399
  • Loading branch information
sb-yb committed May 23, 2022
1 parent b611025 commit d0cb7c9
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 51 deletions.
2 changes: 1 addition & 1 deletion managed/src/main/java/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.yugabyte.yw.common.NativeKubernetesManager;
import com.yugabyte.yw.common.NetworkManager;
import com.yugabyte.yw.common.NodeManager;
import com.yugabyte.yw.common.PlatformInstanceClientFactory;
import com.yugabyte.yw.common.ReleaseManager;
import com.yugabyte.yw.common.ShellKubernetesManager;
import com.yugabyte.yw.common.ShellProcessHandler;
Expand All @@ -38,6 +37,7 @@
import com.yugabyte.yw.common.alerts.QueryAlerts;
import com.yugabyte.yw.common.config.RuntimeConfigFactory;
import com.yugabyte.yw.common.config.impl.SettableRuntimeConfigFactory;
import com.yugabyte.yw.common.ha.PlatformInstanceClientFactory;
import com.yugabyte.yw.common.ha.PlatformReplicationHelper;
import com.yugabyte.yw.common.ha.PlatformReplicationManager;
import com.yugabyte.yw.common.kms.EncryptionAtRestManager;
Expand Down
16 changes: 15 additions & 1 deletion managed/src/main/java/com/yugabyte/yw/common/ApiHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import akka.util.ByteString;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.net.HttpURLConnection;
Expand All @@ -17,6 +18,8 @@
import java.util.UUID;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import play.libs.Json;
import play.libs.ws.WSClient;
import play.libs.ws.WSRequest;
Expand All @@ -25,11 +28,18 @@

/** Helper class API specific stuff */
@Singleton
@Slf4j
public class ApiHelper {

private static final Duration DEFAULT_GET_REQUEST_TIMEOUT = Duration.ofSeconds(10);

@Inject WSClient wsClient;
@Getter(onMethod_ = {@VisibleForTesting})
private final WSClient wsClient;

@Inject
public ApiHelper(WSClient wsClient) {
this.wsClient = wsClient;
}

public boolean postRequest(String url) {
try {
Expand Down Expand Up @@ -124,7 +134,11 @@ private JsonNode handleJSONPromise(CompletionStage<String> jsonPromise) {
String jsonString = jsonPromise.toCompletableFuture().get();
return Json.parse(jsonString);
} catch (InterruptedException | ExecutionException e) {
log.warn("Unexpected exception while parsing response", e);
return ApiResponse.errorJSON(e.getMessage());
} catch (RuntimeException e) {
log.warn("Unexpected exception while parsing response", e);
throw e;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2021 YugaByte, Inc. and Contributors
*
* Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you
* may not use this file except in compliance with the License. You
* may obtain a copy of the License at
*
* http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt
*/

package com.yugabyte.yw.common;

import akka.stream.Materializer;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.typesafe.config.Config;
import java.util.concurrent.CompletableFuture;
import play.Environment;
import play.inject.ApplicationLifecycle;
import play.libs.ws.WSClient;
import play.libs.ws.ahc.AhcWSClient;
import play.libs.ws.ahc.AhcWSClientConfigFactory;

@Singleton
public class CustomWsClientFactory {

private final ApplicationLifecycle lifecycle;
private final Materializer materializer;
private final Environment environment;

@Inject
public CustomWsClientFactory(
ApplicationLifecycle lifecycle, Materializer materializer, Environment environment) {
this.lifecycle = lifecycle;
this.materializer = materializer;
this.environment = environment;
}

public WSClient forCustomConfig(Config customConfig) {
AhcWSClient customeWsClient =
AhcWSClient.create(
AhcWSClientConfigFactory.forConfig(customConfig, environment.classLoader()),
null, // no HTTP caching
materializer);
lifecycle.addStopHook(
() -> {
customeWsClient.close();
return CompletableFuture.completedFuture(null);
});
return customeWsClient;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 YugaByte, Inc. and Contributors
* Copyright 2022 YugaByte, Inc. and Contributors
*
* Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you
* may not use this file except in compliance with the License. You
Expand Down Expand Up @@ -97,7 +97,8 @@ public Config staticApplicationConf() {
private Config globalConfig() {
Config config =
getConfigForScope(GLOBAL_SCOPE_UUID, "Global Runtime Config (" + GLOBAL_SCOPE_UUID + ")")
.withFallback(appConfig);
.withFallback(appConfig)
.resolve();
LOG.trace("globalConfig : {}", config);
return config;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/*
* Copyright 2021 YugaByte, Inc. and Contributors
* Copyright 2022 YugaByte, Inc. and Contributors
*
* Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you
* may not use this file except in compliance with the License. You
* may obtain a copy of the License at
*
* https://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0
* .txt
* http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt
*/

package com.yugabyte.yw.common;
package com.yugabyte.yw.common.ha;

import static scala.compat.java8.JFunction.func;

Expand All @@ -18,14 +17,17 @@
import akka.util.ByteString;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.yugabyte.yw.common.ApiHelper;
import com.yugabyte.yw.controllers.HAAuthenticator;
import com.yugabyte.yw.controllers.ReverseInternalHAController;
import com.yugabyte.yw.models.HighAvailabilityConfig;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.libs.Json;
Expand All @@ -37,6 +39,7 @@ public class PlatformInstanceClient {

private static final Logger LOG = LoggerFactory.getLogger(PlatformInstanceClient.class);

@Getter(onMethod_ = {@VisibleForTesting})
private final ApiHelper apiHelper;

private final String remoteAddress;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2022 YugaByte, Inc. and Contributors
*
* Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you
* may not use this file except in compliance with the License. You
* may obtain a copy of the License at
*
* http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt
*/

package com.yugabyte.yw.common.ha;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import com.yugabyte.yw.common.ApiHelper;
import com.yugabyte.yw.common.CustomWsClientFactory;
import com.yugabyte.yw.common.config.RuntimeConfigFactory;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import play.libs.ws.WSClient;

@Singleton
@Slf4j
public class PlatformInstanceClientFactory {

private final CustomWsClientFactory customWsClientFactory;
private final RuntimeConfigFactory runtimeConfigFactory;
private final ApiHelper fallbackApiHelper;
private WSClient customWsClient = null;

@Inject
public PlatformInstanceClientFactory(
ApiHelper defaultApiHelper,
CustomWsClientFactory customWsClientFactory,
RuntimeConfigFactory runtimeConfigFactory) {
this.fallbackApiHelper = defaultApiHelper;
this.customWsClientFactory = customWsClientFactory;
this.runtimeConfigFactory = runtimeConfigFactory;
}

public synchronized void refreshWsClient(String haWsConfigPath) {
ConfigValue haWsOverrides = runtimeConfigFactory.globalRuntimeConf().getValue(haWsConfigPath);
log.info(
"Creating ws client with config override: {}",
haWsOverrides.render(ConfigRenderOptions.concise()));
Config customWsConfig =
ConfigFactory.empty()
.withValue("play.ws", haWsOverrides)
.withFallback(runtimeConfigFactory.staticApplicationConf())
.withOnlyPath("play.ws");
// Enable trace level logging to debug actual config value being resolved:
log.trace("Creating ws client with config: {}", customWsConfig.root().render());
closePreviousClient(customWsClient);
customWsClient = customWsClientFactory.forCustomConfig(customWsConfig);
}

private void closePreviousClient(WSClient previousWsClient) {
if (previousWsClient != null) {
try {
previousWsClient.close();
} catch (IOException e) {
log.warn("Exception while closing wsClient. Ignored", e);
}
}
}

public PlatformInstanceClient getClient(String clusterKey, String remoteAddress) {
if (customWsClient == null) {
log.info("Using fallbackApiHelper");
return new PlatformInstanceClient(fallbackApiHelper, clusterKey, remoteAddress);
} else {
log.info("Using customApiHelper");
return new PlatformInstanceClient(new ApiHelper(customWsClient), clusterKey, remoteAddress);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.yugabyte.yw.common.ApiHelper;
import com.yugabyte.yw.common.PlatformInstanceClient;
import com.yugabyte.yw.common.PlatformInstanceClientFactory;
import com.yugabyte.yw.common.ShellProcessHandler;
import com.yugabyte.yw.common.ShellResponse;
import com.yugabyte.yw.common.config.impl.SettableRuntimeConfigFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@

import static play.mvc.Http.Status.INTERNAL_SERVER_ERROR;

import java.util.List;
import java.util.concurrent.CompletionStage;

import com.yugabyte.yw.common.config.RuntimeConfigFactory;
import com.google.inject.Inject;
import com.yugabyte.yw.common.PlatformServiceException;

import com.yugabyte.yw.common.config.RuntimeConfigFactory;
import java.util.List;
import java.util.concurrent.CompletionStage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.mvc.Action;
import play.mvc.Http.Context;
import play.mvc.Result;
import play.routing.Router;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AuditAction extends Action.Simple {

public static final Logger LOG = LoggerFactory.getLogger(AuditAction.class);
Expand Down Expand Up @@ -61,7 +58,8 @@ public CompletionStage<Result> call(Context ctx) {

if (isAudited != shouldBeAudited) {
throw new PlatformServiceException(
INTERNAL_SERVER_ERROR, " Mismatch in Audit Logging intent.");
INTERNAL_SERVER_ERROR,
" Mismatch in Audit Logging intent for " + ctx.request().path());
}
}
return result;
Expand Down
Loading

0 comments on commit d0cb7c9

Please sign in to comment.