diff --git a/core/src/main/java/org/apache/druid/metadata/DynamicConfigProvider.java b/core/src/main/java/org/apache/druid/metadata/DynamicConfigProvider.java index 52f032a0a19..166437c5cc5 100644 --- a/core/src/main/java/org/apache/druid/metadata/DynamicConfigProvider.java +++ b/core/src/main/java/org/apache/druid/metadata/DynamicConfigProvider.java @@ -32,6 +32,7 @@ import java.util.Map; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = MapStringDynamicConfigProvider.class) @JsonSubTypes(value = { @JsonSubTypes.Type(name = "mapString", value = MapStringDynamicConfigProvider.class), + @JsonSubTypes.Type(name = "environment", value = EnvironmentVariableDynamicConfigProvider.class) }) public interface DynamicConfigProvider { diff --git a/core/src/main/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProvider.java b/core/src/main/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProvider.java new file mode 100644 index 00000000000..629c748cf88 --- /dev/null +++ b/core/src/main/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProvider.java @@ -0,0 +1,89 @@ +/* + * 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.druid.metadata; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class EnvironmentVariableDynamicConfigProvider implements DynamicConfigProvider +{ + private final ImmutableMap variables; + + @JsonCreator + public EnvironmentVariableDynamicConfigProvider( + @JsonProperty("variables") Map config + ) + { + this.variables = ImmutableMap.copyOf(Preconditions.checkNotNull(config, "config")); + } + + @JsonProperty("variables") + public Map getVariables() + { + return variables; + } + + @Override + public Map getConfig() + { + HashMap map = new HashMap<>(); + for (Map.Entry entry : variables.entrySet()) { + map.put(entry.getKey(), System.getenv(entry.getValue())); + } + return map; + } + + @Override + public String toString() + { + return "EnvironmentVariablePasswordProvider{" + + "variable='" + variables + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EnvironmentVariableDynamicConfigProvider that = (EnvironmentVariableDynamicConfigProvider) o; + + return Objects.equals(variables, that.variables); + + } + + @Override + public int hashCode() + { + return variables.hashCode(); + } + +} diff --git a/core/src/test/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProviderTest.java b/core/src/test/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProviderTest.java new file mode 100644 index 00000000000..94f408d530e --- /dev/null +++ b/core/src/test/java/org/apache/druid/metadata/EnvironmentVariableDynamicConfigProviderTest.java @@ -0,0 +1,109 @@ +/* + * 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.druid.metadata; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class EnvironmentVariableDynamicConfigProviderTest +{ + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + private static final Map CHANGED_ENV_MAP = new HashMap<>(); + + @BeforeClass + public static void setupTest() throws Exception + { + Map oldEnvMap = getENVMap(); + Map addEnvMap = ImmutableMap.of("DRUID_USER", "druid", "DRUID_PASSWORD", "123"); + for (Map.Entry entry : addEnvMap.entrySet()) { + CHANGED_ENV_MAP.put(entry.getKey(), oldEnvMap.get(entry.getKey())); + oldEnvMap.put(entry.getKey(), entry.getValue()); + } + } + + @AfterClass + public static void tearDownTest() throws Exception + { + Map oldEnvMap = getENVMap(); + for (Map.Entry entry : CHANGED_ENV_MAP.entrySet()) { + if (entry.getValue() == null) { + oldEnvMap.remove(entry.getKey()); + } else { + oldEnvMap.put(entry.getKey(), entry.getValue()); + } + } + } + + @Test + public void testSerde() throws IOException + { + String providerString = "{\"type\": \"environment\", \"variables\" : {\"testKey\":\"testValue\"}}"; + DynamicConfigProvider provider = JSON_MAPPER.readValue(providerString, DynamicConfigProvider.class); + Assert.assertTrue(provider instanceof EnvironmentVariableDynamicConfigProvider); + Assert.assertEquals("testValue", ((EnvironmentVariableDynamicConfigProvider) provider).getVariables().get("testKey")); + DynamicConfigProvider serde = JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(provider), DynamicConfigProvider.class); + Assert.assertEquals(provider, serde); + } + + @Test + public void testGetConfig() throws Exception + { + String providerString = "{\"type\": \"environment\", \"variables\" : {\"user\":\"DRUID_USER\",\"password\":\"DRUID_PASSWORD\"}}"; + DynamicConfigProvider provider = JSON_MAPPER.readValue(providerString, DynamicConfigProvider.class); + Assert.assertTrue(provider instanceof EnvironmentVariableDynamicConfigProvider); + Assert.assertEquals("druid", ((EnvironmentVariableDynamicConfigProvider) provider).getConfig().get("user")); + Assert.assertEquals("123", ((EnvironmentVariableDynamicConfigProvider) provider).getConfig().get("password")); + } + + /** + * This method use reflection to get system environment variables map in runtime JVM + * which can be changed. + * + * @return system environment variables map. + */ + private static Map getENVMap() throws Exception + { + Map envMap = null; + Class[] classes = Collections.class.getDeclaredClasses(); + Map systemEnv = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object object = field.get(systemEnv); + envMap = (Map) object; + } + } + if (envMap == null) { + throw new RuntimeException("Failed to get environment map."); + } + return envMap; + } +} diff --git a/docs/operations/dynamic-config-provider.md b/docs/operations/dynamic-config-provider.md index 33bf9663932..45b61d54b94 100644 --- a/docs/operations/dynamic-config-provider.md +++ b/docs/operations/dynamic-config-provider.md @@ -31,3 +31,16 @@ Users can create custom extension of the `DynamicConfigProvider` interface that For more information, see [Adding a new DynamicConfigProvider implementation](../development/modules.md#adding-a-new-dynamicconfigprovider-implementation). +## Environment variable dynamic config provider + +`EnvironmentVariableDynamicConfigProvider` can be used to avoid exposing credentials or other secret information in the configuration files using environment variables. An example to use this `configProvider` is: +```json +druid.some.config.dynamicConfigProvider={"type": "environment","variables":{"secret1": "SECRET1_VAR","secret2": "SECRET2_VAR"}} +``` +The values are described below. + +|Field|Type|Description|Required| +|-----|----|-----------|--------| +|`type`|String|dynamic config provider type|Yes: `environment`| +|`variables`|Map|environment variables to get information from|Yes| +