mirror of https://github.com/apache/cassandra
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:
parent
2de6078289
commit
b2037e473f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -476,6 +476,11 @@ public class DatabaseDescriptor
|
|||
return conf;
|
||||
}
|
||||
|
||||
public static boolean hasLoggedConfig()
|
||||
{
|
||||
return hasLoggedConfig;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static Config loadConfig() throws ConfigurationException
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue