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


Reply via email to