Skip to content

Commit 436336e

Browse files
Watcher accounts constructed lazily (#36656)
This fixes two bugs about watcher notifications: * registering accounts that had only secure settings was not possible before; these accounts are very much practical for Slack and PagerDuty integrations. * removes the limitation that, for an account with both secure and cluster settings, the admin had to first change/add the secure settings and only then add the dependent dynamic cluster settings. The reverse order would trigger a SettingsException for an incomplete account. The workaround is to lazily instantiate account objects, hoping that when accounts are instantiated all the required settings are in place. Previously, the approach was to greedily validate all the account settings by constructing the account objects, even if they would not ever be used by actions. This made sense in a world where all the settings were set by a single API. But given that accounts have dependent settings (that must be used together) that have to be changed using different APIs (POST _nodes/reload_secure_settings and PUT _cluster/settings), the settings group would technically be in an invalid state in between the calls. This fix builds account objects, and validates the settings, when they are needed by actions.
1 parent 1f88b93 commit 436336e

File tree

3 files changed

+180
-19
lines changed

3 files changed

+180
-19
lines changed

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/NotificationService.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.common.settings.Setting;
1414
import org.elasticsearch.common.settings.Settings;
1515
import org.elasticsearch.common.settings.SettingsException;
16+
import org.elasticsearch.common.util.LazyInitializable;
1617

1718
import java.io.IOException;
1819
import java.io.InputStream;
@@ -33,8 +34,8 @@ public abstract class NotificationService<Account> extends AbstractComponent {
3334
private final Settings bootSettings;
3435
private final List<Setting<?>> pluginSecureSettings;
3536
// all are guarded by this
36-
private volatile Map<String, Account> accounts;
37-
private volatile Account defaultAccount;
37+
private volatile Map<String, LazyInitializable<Account, SettingsException>> accounts;
38+
private volatile LazyInitializable<Account, SettingsException> defaultAccount;
3839
// cached cluster setting, required when recreating the notification clients
3940
// using the new "reloaded" secure settings
4041
private volatile Settings cachedClusterSettings;
@@ -56,7 +57,7 @@ public NotificationService(String type, Settings settings, ClusterSettings clust
5657
this.pluginSecureSettings = pluginSecureSettings;
5758
}
5859

59-
private synchronized void clusterSettingsConsumer(Settings settings) {
60+
protected synchronized void clusterSettingsConsumer(Settings settings) {
6061
// update cached cluster settings
6162
this.cachedClusterSettings = settings;
6263
// use these new dynamic cluster settings together with the previously cached
@@ -99,49 +100,49 @@ private void buildAccounts() {
99100
public Account getAccount(String name) {
100101
// note this is not final since we mock it in tests and that causes
101102
// trouble since final methods can't be mocked...
102-
final Map<String, Account> accounts;
103-
final Account defaultAccount;
103+
final Map<String, LazyInitializable<Account, SettingsException>> accounts;
104+
final LazyInitializable<Account, SettingsException> defaultAccount;
104105
synchronized (this) { // must read under sync block otherwise it might be inconsistent
105106
accounts = this.accounts;
106107
defaultAccount = this.defaultAccount;
107108
}
108-
Account theAccount = accounts.getOrDefault(name, defaultAccount);
109+
LazyInitializable<Account, SettingsException> theAccount = accounts.getOrDefault(name, defaultAccount);
109110
if (theAccount == null && name == null) {
110111
throw new IllegalArgumentException("no accounts of type [" + type + "] configured. " +
111112
"Please set up an account using the [xpack.notification." + type +"] settings");
112113
}
113114
if (theAccount == null) {
114115
throw new IllegalArgumentException("no account found for name: [" + name + "]");
115116
}
116-
return theAccount;
117+
return theAccount.getOrCompute();
117118
}
118119

119120
private String getNotificationsAccountPrefix() {
120121
return "xpack.notification." + type + ".account.";
121122
}
122123

123124
private Set<String> getAccountNames(Settings settings) {
124-
// secure settings are not responsible for the client names
125-
final Settings noSecureSettings = Settings.builder().put(settings, false).build();
126-
return noSecureSettings.getByPrefix(getNotificationsAccountPrefix()).names();
125+
return settings.getByPrefix(getNotificationsAccountPrefix()).names();
127126
}
128127

129128
private @Nullable String getDefaultAccountName(Settings settings) {
130129
return settings.get("xpack.notification." + type + ".default_account");
131130
}
132131

133-
private Map<String, Account> createAccounts(Settings settings, Set<String> accountNames,
132+
private Map<String, LazyInitializable<Account, SettingsException>> createAccounts(Settings settings, Set<String> accountNames,
134133
BiFunction<String, Settings, Account> accountFactory) {
135-
final Map<String, Account> accounts = new HashMap<>();
134+
final Map<String, LazyInitializable<Account, SettingsException>> accounts = new HashMap<>();
136135
for (final String accountName : accountNames) {
137136
final Settings accountSettings = settings.getAsSettings(getNotificationsAccountPrefix() + accountName);
138-
final Account account = accountFactory.apply(accountName, accountSettings);
139-
accounts.put(accountName, account);
137+
accounts.put(accountName, new LazyInitializable<>(() -> {
138+
return accountFactory.apply(accountName, accountSettings);
139+
}));
140140
}
141141
return Collections.unmodifiableMap(accounts);
142142
}
143143

144-
private @Nullable Account findDefaultAccountOrNull(Settings settings, Map<String, Account> accounts) {
144+
private @Nullable LazyInitializable<Account, SettingsException> findDefaultAccountOrNull(Settings settings,
145+
Map<String, LazyInitializable<Account, SettingsException>> accounts) {
145146
final String defaultAccountName = getDefaultAccountName(settings);
146147
if (defaultAccountName == null) {
147148
if (accounts.isEmpty()) {
@@ -150,7 +151,7 @@ private Map<String, Account> createAccounts(Settings settings, Set<String> accou
150151
return accounts.values().iterator().next();
151152
}
152153
} else {
153-
final Account account = accounts.get(defaultAccountName);
154+
final LazyInitializable<Account, SettingsException> account = accounts.get(defaultAccountName);
154155
if (account == null) {
155156
throw new SettingsException("could not find default account [" + defaultAccountName + "]");
156157
}

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,27 @@
55
*/
66
package org.elasticsearch.xpack.watcher.notification;
77

8+
import org.elasticsearch.common.settings.SecureSetting;
9+
import org.elasticsearch.common.settings.SecureSettings;
10+
import org.elasticsearch.common.settings.SecureString;
11+
import org.elasticsearch.common.settings.Setting;
812
import org.elasticsearch.common.settings.Settings;
913
import org.elasticsearch.common.settings.SettingsException;
1014
import org.elasticsearch.test.ESTestCase;
1115
import org.elasticsearch.xpack.watcher.notification.NotificationService;
1216

17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.security.GeneralSecurityException;
20+
import java.util.Arrays;
1321
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
import java.util.function.BiConsumer;
1429

1530
import static org.hamcrest.Matchers.anyOf;
1631
import static org.hamcrest.Matchers.is;
@@ -25,6 +40,7 @@ public void testSingleAccount() {
2540
assertThat(service.getAccount(accountName), is(accountName));
2641
// single account, this will also be the default
2742
assertThat(service.getAccount("non-existing"), is(accountName));
43+
assertThat(service.getAccount(null), is(accountName));
2844
}
2945

3046
public void testMultipleAccountsWithExistingDefault() {
@@ -80,16 +96,160 @@ public void testAccountDoesNotExist() throws Exception{
8096
is("no accounts of type [test] configured. Please set up an account using the [xpack.notification.test] settings"));
8197
}
8298

99+
public void testAccountWithSecureSettings() throws Exception {
100+
final Setting<SecureString> secureSetting1 = SecureSetting.secureString("xpack.notification.test.account.secure_only", null);
101+
final Setting<SecureString> secureSetting2 = SecureSetting.secureString("xpack.notification.test.account.mixed.secure", null);
102+
final Map<String, char[]> secureSettingsMap = new HashMap<>();
103+
secureSettingsMap.put(secureSetting1.getKey(), "secure_only".toCharArray());
104+
secureSettingsMap.put(secureSetting2.getKey(), "mixed_secure".toCharArray());
105+
Settings settings = Settings.builder()
106+
.put("xpack.notification.test.account.unsecure_only", "bar")
107+
.put("xpack.notification.test.account.mixed.unsecure", "mixed_unsecure")
108+
.setSecureSettings(secureSettingsFromMap(secureSettingsMap))
109+
.build();
110+
TestNotificationService service = new TestNotificationService(settings, Arrays.asList(secureSetting1, secureSetting2));
111+
assertThat(service.getAccount("secure_only"), is("secure_only"));
112+
assertThat(service.getAccount("unsecure_only"), is("unsecure_only"));
113+
assertThat(service.getAccount("mixed"), is("mixed"));
114+
assertThat(service.getAccount(null), anyOf(is("secure_only"), is("unsecure_only"), is("mixed")));
115+
}
116+
117+
public void testAccountCreationCached() {
118+
String accountName = randomAlphaOfLength(10);
119+
Settings settings = Settings.builder().put("xpack.notification.test.account." + accountName, "bar").build();
120+
final AtomicInteger validationInvocationCount = new AtomicInteger(0);
121+
122+
TestNotificationService service = new TestNotificationService(settings, (String name, Settings accountSettings) -> {
123+
validationInvocationCount.incrementAndGet();
124+
});
125+
assertThat(validationInvocationCount.get(), is(0));
126+
assertThat(service.getAccount(accountName), is(accountName));
127+
assertThat(validationInvocationCount.get(), is(1));
128+
if (randomBoolean()) {
129+
assertThat(service.getAccount(accountName), is(accountName));
130+
} else {
131+
assertThat(service.getAccount(null), is(accountName));
132+
}
133+
// counter is still 1 because the account is cached
134+
assertThat(validationInvocationCount.get(), is(1));
135+
}
136+
137+
public void testAccountUpdateSettings() throws Exception {
138+
final Setting<SecureString> secureSetting = SecureSetting.secureString("xpack.notification.test.account.x.secure", null);
139+
final Setting<String> setting = Setting.simpleString("xpack.notification.test.account.x.dynamic", Setting.Property.Dynamic,
140+
Setting.Property.NodeScope);
141+
final AtomicReference<String> secureSettingValue = new AtomicReference<String>(randomAlphaOfLength(4));
142+
final AtomicReference<String> settingValue = new AtomicReference<String>(randomAlphaOfLength(4));
143+
final Map<String, char[]> secureSettingsMap = new HashMap<>();
144+
final AtomicInteger validationInvocationCount = new AtomicInteger(0);
145+
secureSettingsMap.put(secureSetting.getKey(), secureSettingValue.get().toCharArray());
146+
final Settings.Builder settingsBuilder = Settings.builder()
147+
.put(setting.getKey(), settingValue.get())
148+
.setSecureSettings(secureSettingsFromMap(secureSettingsMap));
149+
final TestNotificationService service = new TestNotificationService(settingsBuilder.build(), Arrays.asList(secureSetting),
150+
(String name, Settings accountSettings) -> {
151+
assertThat(accountSettings.get("dynamic"), is(settingValue.get()));
152+
assertThat(SecureSetting.secureString("secure", null).get(accountSettings), is(secureSettingValue.get()));
153+
validationInvocationCount.incrementAndGet();
154+
});
155+
assertThat(validationInvocationCount.get(), is(0));
156+
service.getAccount(null);
157+
assertThat(validationInvocationCount.get(), is(1));
158+
// update secure setting only
159+
updateSecureSetting(secureSettingValue, secureSetting, secureSettingsMap, settingsBuilder, service);
160+
assertThat(validationInvocationCount.get(), is(1));
161+
service.getAccount(null);
162+
assertThat(validationInvocationCount.get(), is(2));
163+
updateDynamicClusterSetting(settingValue, setting, settingsBuilder, service);
164+
assertThat(validationInvocationCount.get(), is(2));
165+
service.getAccount(null);
166+
assertThat(validationInvocationCount.get(), is(3));
167+
// update both
168+
if (randomBoolean()) {
169+
// update secure first
170+
updateSecureSetting(secureSettingValue, secureSetting, secureSettingsMap, settingsBuilder, service);
171+
// update cluster second
172+
updateDynamicClusterSetting(settingValue, setting, settingsBuilder, service);
173+
} else {
174+
// update cluster first
175+
updateDynamicClusterSetting(settingValue, setting, settingsBuilder, service);
176+
// update secure second
177+
updateSecureSetting(secureSettingValue, secureSetting, secureSettingsMap, settingsBuilder, service);
178+
}
179+
assertThat(validationInvocationCount.get(), is(3));
180+
service.getAccount(null);
181+
assertThat(validationInvocationCount.get(), is(4));
182+
}
183+
184+
private static void updateDynamicClusterSetting(AtomicReference<String> settingValue, Setting<String> setting,
185+
Settings.Builder settingsBuilder, TestNotificationService service) {
186+
settingValue.set(randomAlphaOfLength(4));
187+
settingsBuilder.put(setting.getKey(), settingValue.get());
188+
service.clusterSettingsConsumer(settingsBuilder.build());
189+
}
190+
191+
private static void updateSecureSetting(AtomicReference<String> secureSettingValue, Setting<SecureString> secureSetting,
192+
Map<String, char[]> secureSettingsMap, Settings.Builder settingsBuilder, TestNotificationService service) {
193+
secureSettingValue.set(randomAlphaOfLength(4));
194+
secureSettingsMap.put(secureSetting.getKey(), secureSettingValue.get().toCharArray());
195+
service.reload(settingsBuilder.build());
196+
}
197+
83198
private static class TestNotificationService extends NotificationService<String> {
84199

85-
TestNotificationService(Settings settings) {
86-
super("test", settings, Collections.emptyList());
200+
private final BiConsumer<String, Settings> validator;
201+
202+
TestNotificationService(Settings settings, List<Setting<?>> secureSettings, BiConsumer<String, Settings> validator) {
203+
super("test", settings, secureSettings);
204+
this.validator = validator;
87205
reload(settings);
88206
}
89207

208+
TestNotificationService(Settings settings, List<Setting<?>> secureSettings) {
209+
this(settings, secureSettings, (x, y) -> {});
210+
}
211+
212+
TestNotificationService(Settings settings) {
213+
this(settings, Collections.emptyList(), (x, y) -> {});
214+
}
215+
216+
TestNotificationService(Settings settings, BiConsumer<String, Settings> validator) {
217+
this(settings, Collections.emptyList(), validator);
218+
}
219+
90220
@Override
91221
protected String createAccount(String name, Settings accountSettings) {
222+
validator.accept(name, accountSettings);
92223
return name;
93224
}
94225
}
226+
227+
private static SecureSettings secureSettingsFromMap(Map<String, char[]> secureSettingsMap) {
228+
return new SecureSettings() {
229+
230+
@Override
231+
public boolean isLoaded() {
232+
return true;
233+
}
234+
235+
@Override
236+
public SecureString getString(String setting) throws GeneralSecurityException {
237+
return new SecureString(secureSettingsMap.get(setting));
238+
}
239+
240+
@Override
241+
public Set<String> getSettingNames() {
242+
return secureSettingsMap.keySet();
243+
}
244+
245+
@Override
246+
public InputStream getFile(String setting) throws GeneralSecurityException {
247+
return null;
248+
}
249+
250+
@Override
251+
public void close() throws IOException {
252+
}
253+
};
254+
}
95255
}

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatServiceTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public void testSingleAccountIntegrationNoRoomSetting() throws Exception {
128128
.put("xpack.notification.hipchat.account." + accountName + ".auth_token", "_token");
129129
SettingsException e = expectThrows(SettingsException.class, () ->
130130
new HipChatService(settingsBuilder.build(), httpClient,
131-
new ClusterSettings(settingsBuilder.build(), new HashSet<>(HipChatService.getSettings()))));
131+
new ClusterSettings(settingsBuilder.build(), new HashSet<>(HipChatService.getSettings()))).getAccount(null));
132132
assertThat(e.getMessage(), containsString("missing required [room] setting for [integration] account profile"));
133133
}
134134

0 commit comments

Comments
 (0)