Add a dynamic configuration manager

This commit is contained in:
Moxie Marlinspike 2021-02-01 08:01:58 -08:00 committed by GitHub
parent 5a9c8e304c
commit 92f6a79e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 233 additions and 2 deletions

View File

@ -83,12 +83,17 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.366</version>
<version>1.11.939</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>1.11.366</version>
<version>1.11.939</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-appconfig</artifactId>
<version>1.11.939</version>
</dependency>
<dependency>

View File

@ -9,6 +9,7 @@ import io.dropwizard.Configuration;
import io.dropwizard.client.JerseyClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
@ -216,6 +217,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private RemoteConfigConfiguration remoteConfig;
@Valid
@NotNull
@JsonProperty
private AppConfigConfiguration appConfig;
private Map<String, String> transparentDataIndex = new HashMap<>();
public RecaptchaConfiguration getRecaptchaConfiguration() {
@ -371,4 +377,8 @@ public class WhisperServerConfiguration extends Configuration {
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
return remoteConfig;
}
public AppConfigConfiguration getAppConfig() {
return appConfig;
}
}

View File

@ -116,6 +116,7 @@ import org.whispersystems.textsecuregcm.storage.ActiveUserCounter;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.FeatureFlags;
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
@ -328,6 +329,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheCluster);
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager(config.getAppConfig().getApplication(), config.getAppConfig().getEnvironment(), config.getAppConfig().getConfigurationName());
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
@ -373,6 +376,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(messagePersister);
environment.lifecycle().manage(clientPresenceManager);
environment.lifecycle().manage(featureFlagsManager);
environment.lifecycle().manage(dynamicConfigurationManager);
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);

View File

@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
public class AppConfigConfiguration {
@JsonProperty
@NotEmpty
private String application;
@JsonProperty
@NotEmpty
private String environment;
@JsonProperty
@NotEmpty
private String configuration;
public String getApplication() {
return application;
}
public String getEnvironment() {
return environment;
}
public String getConfigurationName() {
return configuration;
}
}

View File

@ -0,0 +1,5 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
public class DynamicConfiguration {
}

View File

@ -0,0 +1,132 @@
package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.services.appconfig.AmazonAppConfig;
import com.amazonaws.services.appconfig.AmazonAppConfigClient;
import com.amazonaws.services.appconfig.model.GetConfigurationRequest;
import com.amazonaws.services.appconfig.model.GetConfigurationResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.lifecycle.Managed;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.util.Util;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
public class DynamicConfigurationManager implements Managed {
private final String application;
private final String environment;
private final String configurationName;
private final String clientId;
private final AmazonAppConfig appConfigClient;
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final AtomicReference<DynamicConfiguration> configuration = new AtomicReference<>();
private final AtomicBoolean running = new AtomicBoolean(true);
private final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
private GetConfigurationResult lastConfigResult;
private boolean initialized = false;
public DynamicConfigurationManager(String application, String environment, String configurationName) {
this(AmazonAppConfigClient.builder()
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(10000).withRequestTimeout(10000))
.build(),
application, environment, configurationName, UUID.randomUUID().toString());
}
@VisibleForTesting
public DynamicConfigurationManager(AmazonAppConfig appConfigClient, String application, String environment, String configurationName, String clientId) {
this.appConfigClient = appConfigClient;
this.application = application;
this.environment = environment;
this.configurationName = configurationName;
this.clientId = clientId;
}
public DynamicConfiguration getConfiguration() {
synchronized (this) {
while (!initialized) Util.wait(this);
}
return configuration.get();
}
@Override
public void start() {
configuration.set(retrieveInitialDynamicConfiguration());
synchronized (this) {
this.initialized = true;
this.notifyAll();
}
new Thread(() -> {
while (running.get()) {
try {
retrieveDynamicConfiguration().ifPresent(configuration::set);
} catch (Throwable t) {
logger.warn("Error retrieving dynamic configuration", t);
}
Util.sleep(5000);
}
}).start();
}
@Override
public void stop() {
running.set(false);
}
private Optional<DynamicConfiguration> retrieveDynamicConfiguration() throws JsonProcessingException {
final String previousVersion = lastConfigResult != null ? lastConfigResult.getConfigurationVersion() : null;
lastConfigResult = appConfigClient.getConfiguration(new GetConfigurationRequest().withApplication(application)
.withEnvironment(environment)
.withConfiguration(configurationName)
.withClientId(clientId)
.withClientConfigurationVersion(previousVersion));
final Optional<DynamicConfiguration> maybeDynamicConfiguration;
if (!StringUtils.equals(lastConfigResult.getConfigurationVersion(), previousVersion)) {
logger.info("Received new config version: {}", lastConfigResult.getConfigurationVersion());
maybeDynamicConfiguration = Optional.of(mapper.readValue(StandardCharsets.UTF_8.decode(lastConfigResult.getContent().asReadOnlyBuffer()).toString(), DynamicConfiguration.class));
} else {
// No change since last version
maybeDynamicConfiguration = Optional.empty();
}
return maybeDynamicConfiguration;
}
private DynamicConfiguration retrieveInitialDynamicConfiguration() {
for (;;) {
try {
final Optional<DynamicConfiguration> maybeDynamicConfiguration = retrieveDynamicConfiguration();
if (maybeDynamicConfiguration.isPresent()) {
return maybeDynamicConfiguration.get();
} else {
throw new IllegalStateException("No initial configuration available");
}
} catch (Throwable t) {
logger.warn("Error retrieving initial dynamic configuration", t);
Util.sleep(1000);
}
}
}
}

View File

@ -0,0 +1,43 @@
package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.appconfig.AmazonAppConfig;
import com.amazonaws.services.appconfig.model.GetConfigurationRequest;
import com.amazonaws.services.appconfig.model.GetConfigurationResult;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.nio.ByteBuffer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DynamicConfigurationManagerTest {
private DynamicConfigurationManager dynamicConfigurationManager;
private AmazonAppConfig appConfig;
@Before
public void setup() {
this.appConfig = mock(AmazonAppConfig.class);
this.dynamicConfigurationManager = new DynamicConfigurationManager(appConfig, "foo", "bar", "baz", "poof");
}
@Test
public void testGetConfig() {
ArgumentCaptor<GetConfigurationRequest> captor = ArgumentCaptor.forClass(GetConfigurationRequest.class);
when(appConfig.getConfiguration(captor.capture())).thenReturn(new GetConfigurationResult().withContent(ByteBuffer.wrap("test: true".getBytes()))
.withConfigurationVersion("1"));
dynamicConfigurationManager.start();
assertThat(captor.getValue().getApplication()).isEqualTo("foo");
assertThat(captor.getValue().getEnvironment()).isEqualTo("bar");
assertThat(captor.getValue().getConfiguration()).isEqualTo("baz");
assertThat(captor.getValue().getClientId()).isEqualTo("poof");
assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull();
}
}