-
Notifications
You must be signed in to change notification settings - Fork 2
/
ConfigService.groovy
230 lines (194 loc) · 8.29 KB
/
ConfigService.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
*
* Copyright © 2023 Extremely Heavy Industries Inc.
*/
package io.xh.hoist.config
import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileDynamic
import io.xh.hoist.BaseService
import static io.xh.hoist.json.JSONSerializer.serializePretty
/**
* Service to provide soft-configured `AppConfig` values to both server and client.
*
* Configs are managed via the Hoist Admin Console and persisted to the application database with
* metadata to specify their type and client-side visibility. They are intended to be used for
* any values that might need to differ across environments, be adjusted at runtime, or that are
* generally unsuitable for hard-coding in the application source code.
*
* Note that the effective value of a config can be overridden by an "instance config" with the
* same name. See {@link io.xh.hoist.util.InstanceConfigUtils} for more details on that system.
* Also note that instance configs (and therefore AppConfig overrides) can be sourced from a
* predefined yaml file, directory, and/or environment variables.
*
* Fires an `xhConfigChanged` event when a config value is updated.
*/
@GrailsCompileStatic
class ConfigService extends BaseService {
String getString(String name, String notFoundValue = null) {
return (String) getInternalByName(name, 'string', notFoundValue)
}
Integer getInt(String name, Integer notFoundValue = null) {
return (Integer) getInternalByName(name, 'int', notFoundValue)
}
Long getLong(String name, Long notFoundValue = null) {
return (Long) getInternalByName(name, 'long', notFoundValue)
}
Double getDouble(String name, Double notFoundValue = null) {
return (Double) getInternalByName(name, 'double', notFoundValue)
}
Boolean getBool(String name, Boolean notFoundValue = null) {
return (Boolean) getInternalByName(name, 'bool', notFoundValue)
}
Map getMap(String name, Map notFoundValue = null) {
return (Map) getInternalByName(name, 'json', notFoundValue)
}
List getList(String name, List notFoundValue = null) {
return (List) getInternalByName(name, 'json', notFoundValue)
}
String getPwd(String name, String notFoundValue = null) {
return (String) getInternalByName(name, 'pwd', notFoundValue)
}
/**
* Return a map of all config values needed by client.
* All passwords will be obscured.
*/
@ReadOnly
boolean hasConfig(String name) {
return AppConfig.findByName(name, [cache: true]) != null
}
@ReadOnly
Map getClientConfig() {
def ret = [:]
AppConfig.findAllByClientVisible(true, [cache: true]).each {
AppConfig config = (AppConfig) it
def name = config.name
try {
ret[name] = config.externalValue(obscurePassword: true, jsonAsObject: true)
} catch (Exception e) {
logError("Exception while getting client config: '$name'", e)
}
}
return ret
}
/**
* Return a map of specified config values, appropriate for display in admin client.
* Note this may include configs that are not typically sent to clients
* as specified by 'clientVisible'. All passwords will be obscured, however.
*/
@ReadOnly
Map getForAdminStats(String... names) {
return names.toList().collectEntries {
def config = AppConfig.findByName(it, [cache: true])
[it, config?.externalValue(obscurePassword: true, jsonAsObject: true)]
}
}
/**
* Parse a config which may contain a string or comma delimited list
* into a List of split and trimmed strings.
*
* Contains special support for nested configs of the form '[configName]'
*/
@CompileDynamic
List<String> getStringList(String configName) {
def rawConfig = getString(configName, ''),
tokens = rawConfig.split(',')*.trim(),
ret = []
def groupPattern = /\[([\w-]+)\]/
for (String token : tokens) {
def matcher = (token =~ /$groupPattern/)
if (matcher.size() == 1) {
ret.addAll(getStringList(matcher[0][1]))
} else {
ret.add(token)
}
}
ret
}
/** Update the value of an existing config. */
@Transactional
AppConfig setValue(String name, Object value, String lastUpdatedBy = authUsername ?: 'hoist-config-service') {
def currConfig = AppConfig.findByName(name, [cache: true])
if (currConfig == null) {
throw new RuntimeException("No config found with name: [$name]")
}
if (currConfig.valueType == 'json' && !(value instanceof String)) value = serializePretty(value)
currConfig.value = value as String
currConfig.lastUpdatedBy = lastUpdatedBy
currConfig.save(flush: true)
}
/**
* Check a list of core configurations required for Hoist/application operation - ensuring that these configs are
* present and that their valueTypes and clientVisible flags are are as expected. Will create missing configs with
* supplied default values if not found.
*
* @param reqConfigs - map of configName to map of [valueType, defaultValue, clientVisible, groupName]
*/
@Transactional
void ensureRequiredConfigsCreated(Map<String, Map> reqConfigs) {
def currConfigs = AppConfig.list(),
created = 0
reqConfigs.each { confName, confDefaults ->
def currConfig = currConfigs.find { it.name == confName },
valType = confDefaults.valueType,
defaultVal = confDefaults.defaultValue,
clientVisible = confDefaults.clientVisible ?: false,
note = confDefaults.note ?: ''
if (!currConfig) {
if (valType == 'json') defaultVal = serializePretty(defaultVal)
new AppConfig(
name: confName,
valueType: valType,
value: defaultVal,
groupName: confDefaults.groupName ?: 'Default',
clientVisible: clientVisible,
lastUpdatedBy: 'hoist-bootstrap',
note: note
).save()
logWarn(
"Required config $confName missing and created with default value",
'verify default is appropriate for this application'
)
created++
} else {
if (currConfig.valueType != valType) {
logError(
"Unexpected value type for required config $confName",
"expected $valType got ${currConfig.valueType}",
'review and fix!'
)
}
if (currConfig.clientVisible != clientVisible) {
logError(
"Unexpected clientVisible for required config $confName",
"expected $clientVisible got ${currConfig.clientVisible}",
'review and fix!'
)
}
}
}
logDebug("Validated presense of ${reqConfigs.size()} required configs", "created ${created}")
}
void fireConfigChanged(AppConfig obj) {
def topic = clusterService.getTopic('xhConfigChanged')
topic.publishAsync([key: obj.name, value: obj.externalValue()])
}
//-------------------
// Implementation
//-------------------
@ReadOnly
private Object getInternalByName(String name, String valueType, Object notFoundValue) {
AppConfig c = AppConfig.findByName(name, [cache: true])
if (c == null) {
if (notFoundValue != null) return notFoundValue
throw new RuntimeException("No config found with name: [$name]")
}
if (valueType != c.valueType) {
throw new RuntimeException("Unexpected type for config: [$name] | config is ${c.valueType} | expected ${valueType}")
}
return c.externalValue(decryptPassword: true, jsonAsObject: true)
}
}