Allow overriding arbitrary settings via environment variables

This also allows overriding complex settings as a JSON value and adds documentation about these overrides to conf/jvm-server.options

patch by Paulo Motta; reviewed by Stefan Miklosovic, David Capwell for CASSANDRA-20749
This commit is contained in:
Paulo Motta 2025-07-07 15:23:05 -04:00 committed by Stefan Miklosovic
parent 2de6078289
commit b2037e473f
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
9 changed files with 440 additions and 19 deletions

View File

@ -1,4 +1,5 @@
5.1
* Allow overriding arbitrary settings via environment variables (CASSANDRA-20749)
* Optimize MessagingService.getVersionOrdinal (CASSANDRA-20816)
* Optimize TrieMemtable#getFlushSet (CASSANDRA-20760)
* Support manual secondary index selection at the CQL level (CASSANDRA-18112)

View File

@ -43,6 +43,22 @@
# The directory location of the cassandra.yaml file.
#-Dcassandra.config=directory
# Allow cassandra.yaml settings to be overriden via JVM properties
# When this setting is enabled, cassandra.yaml settings can be overriden via JVM properties in the format -Dcassandra.settings.<cassandra_yaml_property_name>
# For example, override cassandra.yaml property 'cdc_enabled' via JVM property -Dcassandra.settings.cdc_enabled
# Nested properties can be specified using '.' as separator, for example: -Dcassandra.settings.replica_filtering_protection.cached_rows_warn_threshold
# Complex property values should be specified as a JSON string, for example: -Dcassandra.settings.table_properties_warned="[\"sstable_preemptive_open_interval_in_mb\", \"index_summary_resize_interval_in_minutes\"]"
#-Dcassandra.config.allow_system_properties=true
# Allow cassandra.yaml settings to be overriden via environment variables
# When this setting is enabled, cassandra.yaml settings can be overriden via environment variables in the format 'CASSANDRA_SETTINGS_<CASSANDRA_YAML_PROPERTY_NAME>'
# For example, override cassandra.yaml property 'cdc_enabled' via env var: export CASSANDRA_SETTINGS_CDC_ENABLED=true
# Nested properties can be specified using '__' as separator, for example: export CASSANDRA_SETTINGS_REPLICA_FILTERING_PROTECTION__CACHED_ROWS_WARN_THRESHOLD=1000
# Complex property values should be specified as a JSON string, for example: export CASSANDRA_SETTINGS_TABLE_PROPERTIES_WARNED='["bloom_filter_fp_chance", "default_time_to_live"]'
# This feature can also be enabled via the CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES environment variable via export CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES=true'
# The system property has precedence over the environment variable when both are specified.
#-Dcassandra.config.allow_environment_variables=true
# Sets the initial partitioner token for a node the first time the node is started.
#-Dcassandra.initial_token=token

View File

@ -21,6 +21,7 @@ package org.apache.cassandra.config;
// checkstyle: suppress below 'blockSystemPropertyUsage'
import java.util.Arrays;
import java.util.Optional;
import org.apache.cassandra.exceptions.ConfigurationException;
@ -38,6 +39,10 @@ public enum CassandraRelevantEnv
/** By default, the standard Cassandra CLI layout is used for backward compatibility, however,
* the new Picocli layout can be enabled by setting this property to the {@code "picocli"}. */
CASSANDRA_CLI_LAYOUT("CASSANDRA_CLI_LAYOUT"),
/**
* Allow overriding
*/
CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES("CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES")
;
CassandraRelevantEnv(String key)
@ -61,6 +66,11 @@ public enum CassandraRelevantEnv
return Boolean.parseBoolean(System.getenv(key));
}
public boolean getBooleanOrDefault(boolean defaultValue)
{
return Optional.ofNullable(System.getenv(key)).map(Boolean::parseBoolean).orElse(defaultValue);
}
public String getKey() {
return key;
}

View File

@ -184,6 +184,7 @@ public enum CassandraRelevantProperties
* Default is set to false.
*/
COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH("com.sun.management.jmxremote.ssl.need.client.auth"),
CONFIG_ALLOW_ENVIRONMENT_VARIABLES("cassandra.config.allow_environment_variables"),
/** Defaults to false for 4.1 but plan to switch to true in a later release the thinking is that environments
* may not work right off the bat so safer to add this feature disabled by default */
CONFIG_ALLOW_SYSTEM_PROPERTIES("cassandra.config.allow_system_properties"),

View File

@ -476,6 +476,11 @@ public class DatabaseDescriptor
return conf;
}
public static boolean hasLoggedConfig()
{
return hasLoggedConfig;
}
@VisibleForTesting
public static Config loadConfig() throws ConfigurationException
{

View File

@ -91,6 +91,21 @@ public final class Properties
* @return map of all flattened properties
*/
public static Map<String, Property> flatten(Loader loader, Map<String, Property> input, String delimiter)
{
return flatten(loader, input, delimiter, false);
}
/**
* Given a map of Properties, takes any "nested" property (non primitive, value-type, or collection), and
* expands them, producing 1 or more Properties.
*
* @param loader for mapping type to map of properties
* @param input map to flatten
* @param delimiter for joining names
* @param withInnerProperties also adds intermediate properties among flattened ones.
* @return map of all flattened properties
*/
public static Map<String, Property> flatten(Loader loader, Map<String, Property> input, String delimiter, boolean withInnerProperties)
{
Queue<Property> queue = new ArrayDeque<>(input.values());
@ -106,6 +121,9 @@ public final class Properties
}
else
{
if (withInnerProperties)
output.put(prop.getName(), prop);
children.values().stream().map(p -> andThen(prop, p, delimiter)).forEach(queue::add);
}
}

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
@ -41,6 +42,8 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.utils.JsonUtils;
import org.apache.cassandra.utils.LocalizeString;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.TypeDescription;
@ -58,13 +61,29 @@ import org.yaml.snakeyaml.parser.ParserImpl;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import static org.apache.cassandra.config.CassandraRelevantEnv.CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES;
import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_DUPLICATE_CONFIG_KEYS;
import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_NEW_OLD_CONFIG_KEYS;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG;
import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_ALLOW_ENVIRONMENT_VARIABLES;
import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_ALLOW_SYSTEM_PROPERTIES;
import static org.apache.cassandra.config.Replacements.getNameReplacements;
public class YamlConfigurationLoader implements ConfigurationLoader
{
final static Set<String> OVERRIDABLE_CONFIG_NAMES;
static
{
// Configs can be overriden via system properties and environment variables entirely or partially
// For example: sai_options.prioritize_over_legacy_index=true or
// sai_options: {prioritize_over_legacy_index=true, segment_write_buffer_size=100MiB}
Loader loader = Properties.defaultLoader();
Map<String, Property> topLevelConfigs = loader.getProperties(Config.class);
Map<String, Property> flattenedConfigs = Properties.flatten(loader, loader.getProperties(Config.class), Properties.DELIMITER, true);
OVERRIDABLE_CONFIG_NAMES = Collections.unmodifiableSet(Sets.union(topLevelConfigs.keySet(), flattenedConfigs.keySet()));
}
private static final Logger logger = LoggerFactory.getLogger(YamlConfigurationLoader.class);
/**
@ -72,6 +91,9 @@ public class YamlConfigurationLoader implements ConfigurationLoader
* system properties do not conflict with other system properties; the name "settings" matches system_views.settings.
*/
static final String SYSTEM_PROPERTY_PREFIX = "cassandra.settings.";
static final String ENVIRONMENT_VARIABLE_PREFIX = "CASSANDRA_SETTINGS_";
public static final String NESTED_CONFIG_SEPARATOR = ".";
public static final String NESTED_CONFIG_SEPARATOR_ENVIRONMENT = "__";
/**
* Inspect the classpath to find storage configuration file
@ -154,27 +176,112 @@ public class YamlConfigurationLoader implements ConfigurationLoader
Yaml yaml = new Yaml(constructor);
Config result = loadConfig(yaml, configBytes);
propertiesChecker.check();
maybeAddEnvironmentVariables(result);
maybeAddSystemProperties(result);
return result;
}
private static void maybeAddSystemProperties(Object obj)
{
if (CassandraRelevantProperties.CONFIG_ALLOW_SYSTEM_PROPERTIES.getBoolean())
if (CONFIG_ALLOW_SYSTEM_PROPERTIES.getBoolean())
{
Map<String, String> orderedPropertiesMap = new TreeMap<>();
java.util.Properties props = System.getProperties();
Map<String, String> map = new HashMap<>();
for (String name : props.stringPropertyNames())
props.stringPropertyNames().forEach(key -> orderedPropertiesMap.put(key, props.getProperty(key)));
Map<String, Object> overridingProperties = new HashMap<>();
for (String originalKey : orderedPropertiesMap.keySet())
{
if (name.startsWith(SYSTEM_PROPERTY_PREFIX))
if (originalKey.startsWith(SYSTEM_PROPERTY_PREFIX))
{
String value = props.getProperty(name);
if (value != null)
map.put(name.replace(SYSTEM_PROPERTY_PREFIX, ""), value);
String value = props.getProperty(originalKey);
String configKey = originalKey.replace(SYSTEM_PROPERTY_PREFIX, "");
if (OVERRIDABLE_CONFIG_NAMES.contains(configKey))
{
if (value != null && !overridingProperties.containsKey(configKey))
{
if (!DatabaseDescriptor.hasLoggedConfig()) // CASSANDRA-9909: Avoid flooding config during initialization
logger.warn("Detected JVM property {}={} override for Cassandra configuration '{}'.", originalKey, value, configKey);
overridingProperties.put(configKey, getScalarOrJsonTree(value));
}
}
else
{
logger.warn("Used sytem property variable {} to override Cassandra configuration but there is no such system property counter-part to override.", originalKey);
}
}
}
if (!map.isEmpty())
updateFromMap(map, false, obj);
if (!overridingProperties.isEmpty())
updateFromMap(maybeFlattenNestedProperties(overridingProperties), false, obj);
}
}
private static void maybeAddEnvironmentVariables(Object obj)
{
if (CONFIG_ALLOW_ENVIRONMENT_VARIABLES.getBoolean(CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES.getBooleanOrDefault(false)))
{
Map<String, String> orderedEnvironmentMap = new TreeMap<>(System.getenv()); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
Map<String, Object> overridingProperties = new HashMap<>();
for (Map.Entry<String, String> env : orderedEnvironmentMap.entrySet())
{
String originalKey = env.getKey();
if (env.getKey().startsWith(ENVIRONMENT_VARIABLE_PREFIX))
{
String configKey = LocalizeString.toLowerCaseLocalized(originalKey.replace(ENVIRONMENT_VARIABLE_PREFIX, "")
.replace(NESTED_CONFIG_SEPARATOR_ENVIRONMENT, NESTED_CONFIG_SEPARATOR));
String configValue = env.getValue();
if (OVERRIDABLE_CONFIG_NAMES.contains(configKey))
{
if (configValue != null && !overridingProperties.containsKey(configKey))
{
if (!DatabaseDescriptor.hasLoggedConfig()) // CASSANDRA-9909: Avoid flooding config during initialization
logger.warn("Detected environment variable {}={} override for Cassandra configuration '{}'.", originalKey, configValue, configKey);
overridingProperties.put(configKey, getScalarOrJsonTree(configValue));
}
}
else
{
logger.warn("Used environment property variable {} to override Cassandra configuration but there is no such environment property counter-part to override.", originalKey);
}
}
}
if (!overridingProperties.isEmpty())
updateFromMap(maybeFlattenNestedProperties(overridingProperties), false, obj);
}
}
private static Map<String, Object> maybeFlattenNestedProperties(Map<String, Object> overridingProperties)
{
Map<String, Object> copyOfProperties = new HashMap<>(overridingProperties);
for (Map.Entry<String, Object> entry : overridingProperties.entrySet())
{
String[] parts = entry.getKey().split("\\.");
if (parts.length > 1 && !parts[parts.length - 1].equals("parameters") && !parts[parts.length - 1].equals("configurations"))
{
if (entry.getValue() instanceof Map)
{
copyOfProperties.remove(entry.getKey());
for (Map.Entry<String, Object> mapEntry : ((Map<String, Object>) entry.getValue()).entrySet())
{
String newKey = entry.getKey() + '.' + mapEntry.getKey();
Object newValue = mapEntry.getValue();
copyOfProperties.put(newKey, newValue);
}
}
}
}
return copyOfProperties;
}
private static Object getScalarOrJsonTree(String value)
{
try
{
return JsonUtils.decodeJson(value);
}
catch (Exception e)
{
return value;
}
}
@ -237,6 +344,7 @@ public class YamlConfigurationLoader implements ConfigurationLoader
T value = (T) constructor.getSingleData(klass);
if (shouldCheck)
propertiesChecker.check();
maybeAddEnvironmentVariables(value);
maybeAddSystemProperties(value);
return value;
}
@ -371,7 +479,7 @@ public class YamlConfigurationLoader implements ConfigurationLoader
{
Replacement replacement = typeReplacements.get(name);
result = replacement.toProperty(getProperty0(type, replacement.newName));
if (replacement.deprecated)
deprecationWarnings.add(replacement.oldName);
}
@ -413,7 +521,7 @@ public class YamlConfigurationLoader implements ConfigurationLoader
private Property getProperty0(Class<? extends Object> type, String name)
{
if (name.contains("."))
if (name.contains(NESTED_CONFIG_SEPARATOR))
return getNestedProperty(type, name);
return getFlatProperty(type, name);
}
@ -461,4 +569,3 @@ public class YamlConfigurationLoader implements ConfigurationLoader
return loaderOptions;
}
}

View File

@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.shared;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public final class WithEnvironment implements AutoCloseable
{
private final List<Environment> properties = new ArrayList<>();
public WithEnvironment(String... kvs)
{
with(kvs);
}
public void with(String... kvs)
{
assert kvs.length % 2 == 0 : "Input must have an even amount of inputs but given " + kvs.length;
for (int i = 0; i <= kvs.length - 2; i = i + 2)
{
with(kvs[i], kvs[i + 1]);
}
}
public void with(String key, String value)
{
try
{
Map<String, String> writableEnv = getWritableEnv();
String previous = writableEnv.put(key, value);
properties.add(new Environment(key, previous));
} catch (Exception e) {
throw new IllegalStateException("Failed to set environment variable", e);
}
}
private static Map<String, String> getWritableEnv() throws NoSuchFieldException, IllegalAccessException
{
Map<String, String> env = System.getenv(); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
Class<?> cl = env.getClass();
Field field = cl.getDeclaredField("m");
field.setAccessible(true);
return (Map<String, String>) field.get(env);
}
@Override
public void close()
{
Collections.reverse(properties);
properties.forEach(s -> {
try
{
Map<String, String> writableEnv = getWritableEnv();
if (s.value == null)
writableEnv.remove(s.key);
else
writableEnv.put(s.key, s.value);
} catch (Exception e) {
throw new IllegalStateException("Failed to set environment variable", e);
}
});
properties.clear();
}
private static final class Environment
{
private final String key;
private final String value;
private Environment(String key, String value)
{
this.key = key;
this.value = value;
}
}
}

View File

@ -27,23 +27,30 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import com.google.common.collect.ImmutableMap;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.distributed.shared.WithEnvironment;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.service.StartupChecks;
import org.apache.cassandra.repair.autorepair.AutoRepairConfig;
import org.assertj.core.api.Assertions;
import org.yaml.snakeyaml.error.YAMLException;
import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_ALLOW_ENVIRONMENT_VARIABLES;
import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_ALLOW_SYSTEM_PROPERTIES;
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.KIBIBYTES;
import static org.apache.cassandra.config.YamlConfigurationLoader.ENVIRONMENT_VARIABLE_PREFIX;
import static org.apache.cassandra.config.YamlConfigurationLoader.SYSTEM_PROPERTY_PREFIX;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -138,28 +145,187 @@ public class YamlConfigurationLoaderTest
@Test
public void withSystemProperties()
{
// for primitive types or data-types which use a String constructor, we can support these as nested
// if the type is a collection, then the string format doesn't make sense and will fail with an error such as
// Cannot create property=client_encryption_options.cipher_suites for JavaBean=org.apache.cassandra.config.Config@1f59a598
// No single argument constructor found for interface java.util.List : null
// the reason is that its not a scalar but a complex type (collection type), so the map we use needs to have a collection to match.
// It is possible that we define a common string representation for these types so they can be written to; this
// is an issue that SettingsTable may need to worry about.
try (WithProperties ignore = new WithProperties()
.set(CONFIG_ALLOW_SYSTEM_PROPERTIES, true)
.with(SYSTEM_PROPERTY_PREFIX + "storage_port", "123",
SYSTEM_PROPERTY_PREFIX + "commitlog_sync", "batch",
SYSTEM_PROPERTY_PREFIX + "seed_provider.class_name", "org.apache.cassandra.locator.SimpleSeedProvider",
SYSTEM_PROPERTY_PREFIX + "seed_provider.parameters", "{\"seeds\": \"127.0.0.1:7000,127.0.0.1:7001\"}",
SYSTEM_PROPERTY_PREFIX + "client_encryption_options.cipher_suites", "[\"FakeCipher\"]",
SYSTEM_PROPERTY_PREFIX + "client_encryption_options.optional", Boolean.FALSE.toString(),
SYSTEM_PROPERTY_PREFIX + "client_encryption_options.enabled", Boolean.TRUE.toString(),
SYSTEM_PROPERTY_PREFIX + "sai_options", "{\"prioritize_over_legacy_index\": \"true\", \"segment_write_buffer_size\": \"100MiB\"}",
SYSTEM_PROPERTY_PREFIX + "crypto_provider", "{\"class_name\": \"MyClass\", \"parameters\": {\"fail_on_missing_provider\": \"false\"}}",
SYSTEM_PROPERTY_PREFIX + "table_properties_warned", "[\"bloom_filter_fp_chance\", \"default_time_to_live\"]",
SYSTEM_PROPERTY_PREFIX + "paxos_variant", "v2",
SYSTEM_PROPERTY_PREFIX + "memtable.configurations", "{\"skiplist\": {\"class_name\": \"SkipListMemtable\", \"parameters\": {\"skip_param1\": \"skip_param1_value\"}}, \"trie\": {\"class_name\": \"TrieMemtable\", \"parameters\": {\"trie_param1\": \"trie_param1_value\"}}, \"default\": {\"inherits\": \"trie\"}}",
SYSTEM_PROPERTY_PREFIX + "client_error_reporting_exclusions.subnets", "[\"127.0.0.1\",\"127.0.0.2\"]",
SYSTEM_PROPERTY_PREFIX + "startup_checks", "{\"check_data_resurrection\": {\"enabled\": \"true\", \"heartbeat_file\": \"/var/lib/cassandra/data/cassandra-heartbeat\"}}",
SYSTEM_PROPERTY_PREFIX + "doesnotexist", Boolean.TRUE.toString()))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
assertThat(config.storage_port).isEqualTo(123);
assertThat(config.commitlog_sync).isEqualTo(Config.CommitLogSync.batch);
assertThat(config.seed_provider.class_name).isEqualTo("org.apache.cassandra.locator.SimpleSeedProvider");
assertThat(config.seed_provider.parameters.get("seeds")).isEqualTo("127.0.0.1:7000,127.0.0.1:7001");
assertThat(config.client_encryption_options.cipher_suites).isEqualTo(Collections.singletonList("FakeCipher"));
assertThat(config.client_encryption_options.optional).isFalse();
assertThat(config.client_encryption_options.enabled).isTrue();
assertThat(config.sai_options.prioritize_over_legacy_index).isTrue();
assertThat(config.sai_options.segment_write_buffer_size).isEqualTo(new DataStorageSpec.IntMebibytesBound("100MiB"));
assertThat(config.crypto_provider.class_name).isEqualTo("MyClass");
assertThat(config.crypto_provider.parameters.get("fail_on_missing_provider")).isEqualTo(Boolean.FALSE.toString());
assertThat(config.table_properties_warned).isEqualTo(Set.of("bloom_filter_fp_chance", "default_time_to_live"));
assertThat(config.paxos_variant).isEqualTo(Config.PaxosVariant.v2);
assertThat(config.client_error_reporting_exclusions).isEqualTo(new SubnetGroups(Arrays.asList("127.0.0.2", "127.0.0.1")));
assertThat(config.startup_checks).hasSize(1);
assertThat(config.startup_checks.get(StartupChecks.StartupCheckType.check_data_resurrection).get("enabled")).isEqualTo(Boolean.TRUE.toString());
assertThat(config.startup_checks.get(StartupChecks.StartupCheckType.check_data_resurrection).get("heartbeat_file")).isEqualTo("/var/lib/cassandra/data/cassandra-heartbeat");
}
try (WithProperties ignore = new WithProperties()
.set(CONFIG_ALLOW_SYSTEM_PROPERTIES, true)
.with(SYSTEM_PROPERTY_PREFIX + "memtable.configurations", "{\"skiplist\": {\"class_name\": \"SkipListMemtable\", \"parameters\": {\"skip_param1\": \"skip_param1_value\"}}, \"trie\": {\"class_name\": \"TrieMemtable\", \"parameters\": {\"trie_param1\": \"trie_param1_value\"}}, \"default\": {\"inherits\": \"trie\"}}"))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
assertThat(config.memtable.configurations).hasSize(3);
assertThat(config.memtable.configurations.get("skiplist").class_name).isEqualTo("SkipListMemtable");
assertThat(config.memtable.configurations.get("skiplist").parameters.get("skip_param1")).isEqualTo("skip_param1_value");
assertThat(config.memtable.configurations.get("trie").class_name).isEqualTo("TrieMemtable");
assertThat(config.memtable.configurations.get("trie").parameters.get("trie_param1")).isEqualTo("trie_param1_value");
assertThat(config.memtable.configurations.get("default").inherits).isEqualTo("trie");
}
try (WithProperties ignore = new WithProperties().set(CONFIG_ALLOW_SYSTEM_PROPERTIES, true)
.with(SYSTEM_PROPERTY_PREFIX + "crypto_provider.parameters", "{\"fail_on_missing_provider\": \"false\"}")
.with(SYSTEM_PROPERTY_PREFIX + "crypto_provider.class_name", "MyClass"))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
ParameterizedClass cryptoProvider = config.crypto_provider;
Assert.assertEquals("MyClass", cryptoProvider.class_name);
Assert.assertTrue(cryptoProvider.parameters.containsKey("fail_on_missing_provider"));
String failOnMissingProviderValue = cryptoProvider.parameters.get("fail_on_missing_provider");
Assert.assertNotNull(failOnMissingProviderValue);
Assert.assertEquals("false", failOnMissingProviderValue);
}
try (WithProperties ignore = new WithProperties().set(CONFIG_ALLOW_SYSTEM_PROPERTIES, true)
.with(SYSTEM_PROPERTY_PREFIX + "jmx_server_options.jmx_encryption_options",
'{' +
"\"enabled\": true," +
"\"cipher_suites\": [\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"]" +
'}'))
{
Config c = load("test/conf/cassandra-jmx-sslconfig.yaml");
Assert.assertTrue(c.jmx_server_options.enabled);
Assertions.assertThatCollection(c.jmx_server_options.jmx_encryption_options.cipher_suites).containsExactly("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
// preserved what was in yaml, overridden only what specified
Assert.assertEquals("test/conf/cassandra_ssl_test.truststore", c.jmx_server_options.jmx_encryption_options.truststore);
}
try (WithProperties ignore = new WithProperties().set(CONFIG_ALLOW_SYSTEM_PROPERTIES, true)
.with(SYSTEM_PROPERTY_PREFIX + "jmx_server_options.jmx_encryption_options.enabled", "true"))
{
Config c = load("test/conf/cassandra-jmx-sslconfig.yaml");
Assert.assertTrue(c.jmx_server_options.enabled);
}
}
@Test
public void withEnvironmentVariables()
{
try (WithProperties ignore1 = new WithProperties().set(CONFIG_ALLOW_ENVIRONMENT_VARIABLES, true);
WithEnvironment ignore2 = new WithEnvironment(ENVIRONMENT_VARIABLE_PREFIX + "STORAGE_PORT", "123",
ENVIRONMENT_VARIABLE_PREFIX + "COMMITLOG_SYNC", "batch",
ENVIRONMENT_VARIABLE_PREFIX + "SEED_PROVIDER__class_name", "org.apache.cassandra.locator.SimpleSeedProvider",
ENVIRONMENT_VARIABLE_PREFIX + "SEED_PROVIDER__parameters", "{\"seeds\": \"127.0.0.1:7000,127.0.0.1:7001\"}",
ENVIRONMENT_VARIABLE_PREFIX + "CLIENT_ENCRYPTION_OPTIONS__cipher_suites", "[\"FakeCipher\"]",
ENVIRONMENT_VARIABLE_PREFIX + "CLIENT_ENCRYPTION_OPTIONS__optional", "false",
ENVIRONMENT_VARIABLE_PREFIX + "CLIENT_ENCRYPTION_OPTIONS__enabled", "true",
ENVIRONMENT_VARIABLE_PREFIX + "SAI_OPTIONS", "{\"prioritize_over_legacy_index\": \"true\", \"segment_write_buffer_size\": \"100MiB\"}",
ENVIRONMENT_VARIABLE_PREFIX + "crypto_provider", "{\"class_name\": \"MyClass\", \"parameters\": {\"fail_on_missing_provider\": \"false\"}}",
ENVIRONMENT_VARIABLE_PREFIX + "TABLE_PROPERTIES_WARNED", "[\"bloom_filter_fp_chance\", \"default_time_to_live\"]",
ENVIRONMENT_VARIABLE_PREFIX + "PAXOS_VARIANT", "v2",
ENVIRONMENT_VARIABLE_PREFIX + "MEMTABLE__configurations", "{\"skiplist\": {\"class_name\": \"SkipListMemtable\", \"parameters\": {\"skip_param1\": \"skip_param1_value\"}}, \"trie\": {\"class_name\": \"TrieMemtable\", \"parameters\": {\"trie_param1\": \"trie_param1_value\"}}, \"default\": {\"inherits\": \"trie\"}}",
ENVIRONMENT_VARIABLE_PREFIX + "CLIENT_ERROR_REPORTING_EXCLUSIONS__subnets", "[\"127.0.0.1\",\"127.0.0.2\"]",
ENVIRONMENT_VARIABLE_PREFIX + "STARTUP_CHECKS", "{\"check_data_resurrection\": {\"enabled\": \"true\", \"heartbeat_file\": \"/var/lib/cassandra/data/cassandra-heartbeat\"}}",
ENVIRONMENT_VARIABLE_PREFIX + "doesnotexist", "true"
))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
assertThat(config.storage_port).isEqualTo(123);
assertThat(config.commitlog_sync).isEqualTo(Config.CommitLogSync.batch);
assertThat(config.seed_provider.class_name).isEqualTo("org.apache.cassandra.locator.SimpleSeedProvider");
assertThat(config.seed_provider.parameters.get("seeds")).isEqualTo("127.0.0.1:7000,127.0.0.1:7001");
assertThat(config.client_encryption_options.cipher_suites).isEqualTo(Collections.singletonList("FakeCipher"));
assertThat(config.client_encryption_options.optional).isFalse();
assertThat(config.client_encryption_options.enabled).isTrue();
assertThat(config.sai_options.prioritize_over_legacy_index).isTrue();
assertThat(config.sai_options.segment_write_buffer_size).isEqualTo(new DataStorageSpec.IntMebibytesBound("100MiB"));
assertThat(config.crypto_provider.class_name).isEqualTo("MyClass");
assertThat(config.crypto_provider.parameters.get("fail_on_missing_provider")).isEqualTo(Boolean.FALSE.toString());
assertThat(config.table_properties_warned).isEqualTo(Set.of("bloom_filter_fp_chance", "default_time_to_live"));
assertThat(config.paxos_variant).isEqualTo(Config.PaxosVariant.v2);
assertThat(config.memtable.configurations).hasSize(3);
assertThat(config.memtable.configurations.get("skiplist").class_name).isEqualTo("SkipListMemtable");
assertThat(config.memtable.configurations.get("skiplist").parameters.get("skip_param1")).isEqualTo("skip_param1_value");
assertThat(config.memtable.configurations.get("trie").class_name).isEqualTo("TrieMemtable");
assertThat(config.memtable.configurations.get("trie").parameters.get("trie_param1")).isEqualTo("trie_param1_value");
assertThat(config.memtable.configurations.get("default").inherits).isEqualTo("trie");
assertThat(config.client_error_reporting_exclusions).isEqualTo(new SubnetGroups(Arrays.asList("127.0.0.2", "127.0.0.1")));
assertThat(config.startup_checks).hasSize(1);
assertThat(config.startup_checks.get(StartupChecks.StartupCheckType.check_data_resurrection).get("enabled")).isEqualTo("true");
assertThat(config.startup_checks.get(StartupChecks.StartupCheckType.check_data_resurrection).get("heartbeat_file")).isEqualTo("/var/lib/cassandra/data/cassandra-heartbeat");
}
try (WithEnvironment ignore = new WithEnvironment(CassandraRelevantEnv.CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES.getKey(), Boolean.TRUE.toString(),
ENVIRONMENT_VARIABLE_PREFIX + "memtable.configurations", "{\"skiplist\": {\"class_name\": \"SkipListMemtable\", \"parameters\": {\"skip_param1\": \"skip_param1_value\"}}, \"trie\": {\"class_name\": \"TrieMemtable\", \"parameters\": {\"trie_param1\": \"trie_param1_value\"}}, \"default\": {\"inherits\": \"trie\"}}"))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
assertThat(config.memtable.configurations).hasSize(3);
assertThat(config.memtable.configurations.get("skiplist").class_name).isEqualTo("SkipListMemtable");
assertThat(config.memtable.configurations.get("skiplist").parameters.get("skip_param1")).isEqualTo("skip_param1_value");
assertThat(config.memtable.configurations.get("trie").class_name).isEqualTo("TrieMemtable");
assertThat(config.memtable.configurations.get("trie").parameters.get("trie_param1")).isEqualTo("trie_param1_value");
assertThat(config.memtable.configurations.get("default").inherits).isEqualTo("trie");
}
try (WithEnvironment ignore = new WithEnvironment(CassandraRelevantEnv.CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES.getKey(), Boolean.TRUE.toString(),
ENVIRONMENT_VARIABLE_PREFIX + "crypto_provider.parameters", "{\"fail_on_missing_provider\": \"false\"}",
ENVIRONMENT_VARIABLE_PREFIX + "crypto_provider.class_name", "MyClass"))
{
Config config = YamlConfigurationLoader.fromMap(Collections.emptyMap(), true, Config.class);
ParameterizedClass cryptoProvider = config.crypto_provider;
Assert.assertEquals("MyClass", cryptoProvider.class_name);
Assert.assertTrue(cryptoProvider.parameters.containsKey("fail_on_missing_provider"));
String failOnMissingProviderValue = cryptoProvider.parameters.get("fail_on_missing_provider");
Assert.assertNotNull(failOnMissingProviderValue);
Assert.assertEquals("false", failOnMissingProviderValue);
}
try (WithEnvironment ignore = new WithEnvironment(CassandraRelevantEnv.CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES.getKey(), Boolean.TRUE.toString(),
ENVIRONMENT_VARIABLE_PREFIX + "jmx_server_options.jmx_encryption_options",
'{' +
"\"enabled\": true," +
"\"cipher_suites\": [\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"]" +
'}'))
{
Config c = load("test/conf/cassandra-jmx-sslconfig.yaml");
Assert.assertTrue(c.jmx_server_options.enabled);
Assertions.assertThatCollection(c.jmx_server_options.jmx_encryption_options.cipher_suites).containsExactly("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
// preserved what was in yaml, overridden only what specified
Assert.assertEquals("test/conf/cassandra_ssl_test.truststore", c.jmx_server_options.jmx_encryption_options.truststore);
}
try (WithEnvironment ignore = new WithEnvironment(CassandraRelevantEnv.CASSANDRA_ALLOW_CONFIG_ENVIRONMENT_VARIABLES.getKey(), Boolean.TRUE.toString(),
ENVIRONMENT_VARIABLE_PREFIX + "jmx_server_options.jmx_encryption_options.enabled", "true"))
{
Config c = load("test/conf/cassandra-jmx-sslconfig.yaml");
Assert.assertTrue(c.jmx_server_options.enabled);
}
}