This is an automated email from the ASF dual-hosted git repository. smiklosovic pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push: new b2037e473f Allow overriding arbitrary settings via environment variables b2037e473f is described below commit b2037e473fb6438947d6ed9c58fbea5955cb72c4 Author: Paulo Motta <pa...@apache.org> AuthorDate: Mon Jul 7 15:23:05 2025 -0400 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 --- CHANGES.txt | 1 + conf/jvm-server.options | 16 ++ .../cassandra/config/CassandraRelevantEnv.java | 10 ++ .../config/CassandraRelevantProperties.java | 1 + .../cassandra/config/DatabaseDescriptor.java | 5 + .../org/apache/cassandra/config/Properties.java | 18 +++ .../cassandra/config/YamlConfigurationLoader.java | 131 +++++++++++++-- .../distributed/shared/WithEnvironment.java | 97 +++++++++++ .../config/YamlConfigurationLoaderTest.java | 180 ++++++++++++++++++++- 9 files changed, 440 insertions(+), 19 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ba6b0ddc65..bc3fa7b424 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/conf/jvm-server.options b/conf/jvm-server.options index c63863aa3b..d850592db0 100644 --- a/conf/jvm-server.options +++ b/conf/jvm-server.options @@ -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 diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantEnv.java b/src/java/org/apache/cassandra/config/CassandraRelevantEnv.java index d970cf7fde..e563c59176 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantEnv.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantEnv.java @@ -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; } diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index a518497e8f..9492d97401 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -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"), diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index f38dc0e9d5..d605d0cd98 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -476,6 +476,11 @@ public class DatabaseDescriptor return conf; } + public static boolean hasLoggedConfig() + { + return hasLoggedConfig; + } + @VisibleForTesting public static Config loadConfig() throws ConfigurationException { diff --git a/src/java/org/apache/cassandra/config/Properties.java b/src/java/org/apache/cassandra/config/Properties.java index 79852d4af6..2e9296f257 100644 --- a/src/java/org/apache/cassandra/config/Properties.java +++ b/src/java/org/apache/cassandra/config/Properties.java @@ -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); } } diff --git a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java index f37a42e8fa..37e45b5089 100644 --- a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java +++ b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java @@ -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 (originalKey.startsWith(SYSTEM_PROPERTY_PREFIX)) + { + 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 (!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 (name.startsWith(SYSTEM_PROPERTY_PREFIX)) + if (entry.getValue() instanceof Map) { - String value = props.getProperty(name); - if (value != null) - map.put(name.replace(SYSTEM_PROPERTY_PREFIX, ""), value); + 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); + } } } - if (!map.isEmpty()) - updateFromMap(map, false, obj); + } + 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; } } - diff --git a/test/distributed/org/apache/cassandra/distributed/shared/WithEnvironment.java b/test/distributed/org/apache/cassandra/distributed/shared/WithEnvironment.java new file mode 100644 index 0000000000..03d09e3c71 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/shared/WithEnvironment.java @@ -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; + } + } +} diff --git a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java index 68d5302047..afa820d60e 100644 --- a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java +++ b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java @@ -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); } } --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org