Coverage Summary for Class: ConfigLoader (co.rsk.config)
Class |
Method, %
|
Line, %
|
ConfigLoader |
18.2%
(2/11)
|
3.7%
(4/109)
|
ConfigLoader$1 |
0%
(0/1)
|
0%
(0/1)
|
Total |
16.7%
(2/12)
|
3.6%
(4/110)
|
1 /*
2 * This file is part of RskJ
3 * Copyright (C) 2018 RSK Labs Ltd.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package co.rsk.config;
19
20 import co.rsk.cli.CliArgs;
21 import com.typesafe.config.*;
22 import org.ethereum.config.SystemProperties;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 import javax.annotation.Nonnull;
27 import java.io.File;
28 import java.util.Map;
29 import java.util.Objects;
30
31 /**
32 * Class that encapsulates config loading strategy.
33 */
34 public class ConfigLoader {
35
36 private static final Logger logger = LoggerFactory.getLogger("config");
37
38 private static final String MAINNET_RESOURCE_PATH = "config/main";
39 private static final String TESTNET_RESOURCE_PATH = "config/testnet";
40 private static final String REGTEST_RESOURCE_PATH = "config/regtest";
41 private static final String DEVNET_RESOURCE_PATH = "config/devnet";
42 private static final String EXPECTED_RESOURCE_PATH = "expected";
43 private static final String YES = "yes";
44 private static final String NO = "no";
45
46 private final CliArgs<NodeCliOptions, NodeCliFlags> cliArgs;
47
48 public ConfigLoader(CliArgs<NodeCliOptions, NodeCliFlags> cliArgs) {
49 this.cliArgs = Objects.requireNonNull(cliArgs);
50 }
51
52 /**
53 * Loads configurations from different sources with the following precedence:
54 * 1. Command line arguments
55 * 2. Environment variables
56 * 3. System properties
57 * 4. User configuration file
58 * 5. Installer configuration file
59 * 6. Default settings per network in resources/[network].conf
60 * 7. Default settings for all networks in resources/reference.conf
61 *
62 * <p>
63 * During the verification process the unified configuration is being tested against the setting names
64 * defined in the expected.conf config file. The process is silent by default which means that in a case of any problems
65 * with the config settings only error logs will be generated and the node will continue its running.
66 *
67 * <p>
68 * If the <b><blockchain.config.verify/b> setting is {@code true} (either set in a .conf file or via <b>--verify-config</b> command line flag),
69 * then in a case of any problems an exception will be thrown.
70 *
71 * Note:
72 * 1. The <b><blockchain.config.verify/b> setting is {@code false} by default.
73 * 2. Config verification process of matching actual and expected config settings is a recursive process and takes into
74 * account appropriate setting names. Scalar values are not tested for matching, e.g. if we have settingKey="some value"
75 * in the expected.conf file and settingKey=100 in a user config file, then it will pass the verification process.
76 *
77 * @throws RskConfigurationException on configuration errors
78 */
79 public Config getConfig() {
80 Config cliConfig = getConfigFromCliArgs();
81 Config systemPropsConfig = ConfigFactory.systemProperties();
82 Config systemEnvConfig = ConfigFactory.systemEnvironment();
83 Config userCustomConfig = getUserCustomConfig();
84 Config installerConfig = getInstallerConfig();
85
86 Config userConfig = ConfigFactory.empty()
87 .withFallback(cliConfig)
88 .withFallback(systemPropsConfig)
89 .withFallback(systemEnvConfig)
90 .withFallback(userCustomConfig)
91 .withFallback(installerConfig);
92 Config networkBaseConfig = getNetworkDefaultConfig(userConfig);
93 Config unifiedConfig = userConfig.withFallback(networkBaseConfig);
94
95 Config expectedConfig = ConfigFactory.parseResourcesAnySyntax(EXPECTED_RESOURCE_PATH)
96 .withFallback(systemPropsConfig)
97 .withFallback(systemEnvConfig);
98 boolean valid = isActualObjectValid("", expectedConfig.root(), unifiedConfig.root());
99
100 if (unifiedConfig.getBoolean(SystemProperties.PROPERTY_BC_VERIFY) && !valid) {
101 throw new RskConfigurationException("Verification of node config settings has failed. See the previous error logs for details.");
102 }
103
104 return unifiedConfig;
105 }
106
107 private Config getConfigFromCliArgs() {
108 Config config = ConfigFactory.empty();
109
110 for (NodeCliFlags flag : cliArgs.getFlags()) {
111 config = flag.withConfig(config);
112 }
113
114 for (Map.Entry<NodeCliOptions, String> entry : cliArgs.getOptions().entrySet()) {
115 config = entry.getKey().withConfig(config, entry.getValue());
116 }
117
118 return config;
119 }
120
121 private Config getUserCustomConfig() {
122 String file = System.getProperty("rsk.conf.file");
123 Config cmdLineConfigFile = file != null ? ConfigFactory.parseFile(new File(file)) : ConfigFactory.empty();
124 logger.info(
125 "Config ( {} ): user properties from -Drsk.conf.file file '{}'",
126 cmdLineConfigFile.entrySet().isEmpty() ? NO : YES,
127 file
128 );
129 return cmdLineConfigFile;
130 }
131
132 private Config getInstallerConfig() {
133 File installerFile = new File("/etc/rsk/node.conf");
134 Config installerConfig = installerFile.exists() ? ConfigFactory.parseFile(installerFile) : ConfigFactory.empty();
135 logger.info(
136 "Config ( {} ): default properties from installer '/etc/rsk/node.conf'",
137 installerConfig.entrySet().isEmpty() ? NO : YES
138 );
139 return installerConfig;
140 }
141
142 /**
143 * @return the network-specific configuration based on the user config, or mainnet if no configuration is specified.
144 */
145 private Config getNetworkDefaultConfig(Config userConfig) {
146 if (userConfig.hasPath(SystemProperties.PROPERTY_BC_CONFIG_NAME)) {
147 String network = userConfig.getString(SystemProperties.PROPERTY_BC_CONFIG_NAME);
148 if (NodeCliFlags.NETWORK_TESTNET.getName().equals(network)) {
149 return ConfigFactory.load(TESTNET_RESOURCE_PATH);
150 } else if (NodeCliFlags.NETWORK_REGTEST.getName().equals(network)) {
151 return ConfigFactory.load(REGTEST_RESOURCE_PATH);
152 } else if (NodeCliFlags.NETWORK_DEVNET.getName().equals(network)) {
153 return ConfigFactory.load(DEVNET_RESOURCE_PATH);
154 } else if (NodeCliFlags.NETWORK_MAINNET.getName().equals(network)) {
155 return ConfigFactory.load(MAINNET_RESOURCE_PATH);
156 } else {
157 String exceptionMessage = String.format(
158 "%s is not a valid network name (%s property)",
159 network,
160 SystemProperties.PROPERTY_BC_CONFIG_NAME
161 );
162 logger.warn(exceptionMessage);
163 throw new IllegalArgumentException(exceptionMessage);
164 }
165 }
166
167 logger.info("Network not set, using mainnet by default");
168 return ConfigFactory.load(MAINNET_RESOURCE_PATH);
169 }
170
171 private static boolean isActualObjectValid(@Nonnull String keyPath, @Nonnull ConfigObject expectedObject, @Nonnull ConfigObject actualObject) {
172 boolean valid = true;
173 String prefix = keyPath.isEmpty() ? "" : keyPath + ".";
174 for (Map.Entry<String, ConfigValue> actualEntry : actualObject.entrySet()) {
175 String actualEntryKey = actualEntry.getKey();
176 ConfigValue actualEntryValue = actualEntry.getValue();
177 if (expectedObject.isEmpty()) {
178 // if expected object is empty, then the actual object should contain only scalar items
179 if (isCollectionType(actualEntryValue.valueType())) {
180 String entryKeyPath = prefix + actualEntryKey;
181 logger.error("Expected scalar config value for key path `{}`. Actual value is {}. See expected.conf for the expected settings",
182 entryKeyPath, actualEntryValue);
183 valid = false;
184 }
185 } else {
186 ConfigValue expectedEntryValue = expectedObject.get(actualEntryKey);
187 String entryKeyPath = prefix + actualEntryKey;
188 if (expectedEntryValue == null) {
189 logger.error("Unexpected config value {} for key path `{}`. See expected.conf for the expected settings", actualEntryValue, entryKeyPath);
190 valid = false;
191 } else {
192 valid &= isActualValueValid(entryKeyPath, expectedEntryValue, actualEntryValue);
193 }
194 }
195 }
196 return valid;
197 }
198
199 private static boolean isActualListValid(@Nonnull String keyPath, @Nonnull ConfigList expectedList, @Nonnull ConfigList actualList) {
200 if (expectedList.size() > 1) {
201 throw new RuntimeException("An array in expected.conf should either be empty or contain one template item.");
202 }
203
204 boolean valid = true;
205 int index = 0;
206 for (ConfigValue actualItem : actualList) {
207 if (expectedList.isEmpty()) {
208 // if expected list is empty, then the actual list should contain only scalar items
209 if (isCollectionType(actualItem.valueType())) {
210 String itemKeyPath = keyPath + "[" + index + "]";
211 logger.error("Expected scalar config value for key path `{}`. Actual value is {}. See expected.conf for the expected settings",
212 itemKeyPath, actualItem);
213 valid = false;
214 }
215 } else {
216 // Assuming that all items in the list should have the same configuration structure.
217 String itemKeyPath = keyPath + "[" + index + "]";
218 ConfigValue expectedItem = expectedList.get(0);
219 valid &= isActualValueValid(itemKeyPath, expectedItem, actualItem);
220 }
221 index++;
222 }
223 return valid;
224 }
225
226 private static boolean isActualValueValid(@Nonnull String keyPath, @Nonnull ConfigValue expectedValue, @Nonnull ConfigValue actualValue) {
227 ConfigValueType actualValueType = Objects.requireNonNull(actualValue.valueType());
228 ConfigValueType expectedValueType = Objects.requireNonNull(expectedValue.valueType());
229
230 if (!isCollectionType(expectedValueType) && !isCollectionType(actualValueType)) {
231 return true; // We don't verify non-collection types
232 }
233
234 if (expectedValueType != actualValueType) {
235 logger.error("Config value type mismatch. `{}` has type {}, but should have {}. See expected.conf for the expected settings",
236 keyPath, actualValueType, expectedValueType);
237 return false;
238 }
239
240 switch (actualValueType) {
241 case OBJECT:
242 ConfigObject actualObject = (ConfigObject) actualValue;
243 ConfigObject expectedObject = (ConfigObject) expectedValue;
244 return isActualObjectValid(keyPath, expectedObject, actualObject);
245 case LIST:
246 ConfigList actualList = (ConfigList) actualValue;
247 ConfigList expectedList = (ConfigList) expectedValue;
248 return isActualListValid(keyPath, expectedList, actualList);
249 default:
250 return true;
251 }
252 }
253
254 /**
255 * Checks whether the value type is a collection of other values.
256 *
257 * @return {@code true} if the value type is either {@link ConfigValueType#OBJECT} or {@link ConfigValueType#LIST}.
258 */
259 public static boolean isCollectionType(ConfigValueType valueType) {
260 return valueType == ConfigValueType.OBJECT || valueType == ConfigValueType.LIST;
261 }
262
263 }