-
Notifications
You must be signed in to change notification settings - Fork 468
/
Copy pathconfig_manager.go
449 lines (376 loc) · 14.6 KB
/
config_manager.go
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved.
//
// Licensed 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 chop
import (
"context"
"errors"
"fmt"
"github.com/altinity/clickhouse-operator/pkg/apis/deployment"
"os"
"os/user"
"path/filepath"
"sort"
"github.com/kubernetes-sigs/yaml"
kube "k8s.io/client-go/kubernetes"
log "github.com/altinity/clickhouse-operator/pkg/announcer"
api "github.com/altinity/clickhouse-operator/pkg/apis/clickhouse.altinity.com/v1"
chopClientSet "github.com/altinity/clickhouse-operator/pkg/client/clientset/versioned"
"github.com/altinity/clickhouse-operator/pkg/controller"
)
// ConfigManager specifies configuration manager in charge of operator's configuration
type ConfigManager struct {
// kubeClient is a k8s client
kubeClient *kube.Clientset
// chopClient is a k8s client able to communicate with operator's Custom Resources
chopClient *chopClientSet.Clientset
// chopConfigList is a list of available operator configurations
chopConfigList *api.ClickHouseOperatorConfigurationList
// initConfigFilePath is a path to the configuration file, which will be used as initial/seed
// to build final config, which will be used/consumed by users
initConfigFilePath string
// fileConfig is a prepared file-based config
fileConfig *api.OperatorConfig
// crConfigs is a slice of prepared Custom Resource based configs
crConfigs []*api.OperatorConfig
// config is the final config, built as merge of all available configs.
// This config is ready to use/be consumed by users
config *api.OperatorConfig
// runtimeParams is set/map of runtime params, influencing configuration
runtimeParams map[string]string
}
// NewConfigManager creates new ConfigManager
func NewConfigManager(
kubeClient *kube.Clientset,
chopClient *chopClientSet.Clientset,
initConfigFilePath string,
) *ConfigManager {
return &ConfigManager{
kubeClient: kubeClient,
chopClient: chopClient,
initConfigFilePath: initConfigFilePath,
}
}
// Init reads config from all sources
func (cm *ConfigManager) Init() error {
var err error
// Get ENV vars
cm.runtimeParams = cm.getEnvVarParams()
cm.logEnvVarParams()
// Get initial config from the file
cm.fileConfig, err = cm.getFileBasedConfig(cm.initConfigFilePath)
if err != nil {
return err
}
log.V(1).Info("File-based CHOP config:")
log.V(1).Info("\n" + cm.fileConfig.String(true))
// Get configs from all config Custom Resources that are located in the namespace where Operator is running
if namespace, ok := cm.GetRuntimeParam(deployment.OPERATOR_POD_NAMESPACE); ok {
cm.getAllCRBasedConfigs(namespace)
cm.logAllCRBasedConfigs()
}
// Prepare one unified config from all available config pieces
cm.buildUnifiedConfig()
cm.fetchSecretCredentials()
// From now on we have one unified CHOP config
log.V(1).Info("Unified CHOP config - with secret data fetched (but not post-processed yet):")
log.V(1).Info("\n" + cm.config.String(true))
// Finalize config by post-processing
cm.Postprocess()
// OperatorConfig is ready
log.V(1).Info("Final CHOP config:")
log.V(1).Info("\n" + cm.config.String(true))
return nil
}
// Config is an access wrapper
func (cm *ConfigManager) Config() *api.OperatorConfig {
return cm.config
}
// getAllCRBasedConfigs reads all ClickHouseOperatorConfiguration objects in specified namespace
func (cm *ConfigManager) getAllCRBasedConfigs(namespace string) {
// We need to have chop kube client available in order to fetch ClickHouseOperatorConfiguration objects
if cm.chopClient == nil {
return
}
log.V(1).F().Info("Looking for ClickHouseOperatorConfigurations in namespace '%s'.", namespace)
// Get list of ClickHouseOperatorConfiguration objects
var err error
if cm.chopConfigList, err = cm.chopClient.ClickhouseV1().ClickHouseOperatorConfigurations(namespace).List(context.TODO(), controller.NewListOptions()); err != nil {
log.V(1).F().Error("Error read ClickHouseOperatorConfigurations in namespace '%s'. Err: %v", namespace, err)
return
}
if cm.chopConfigList == nil {
return
}
// Get sorted list of names of ClickHouseOperatorConfiguration objects located in specified namespace
var names []string
for i := range cm.chopConfigList.Items {
chOperatorConfiguration := &cm.chopConfigList.Items[i]
names = append(names, chOperatorConfiguration.Name)
}
sort.Strings(names)
// Build sorted list of configs
for _, name := range names {
for i := range cm.chopConfigList.Items {
// Convenience wrapper
chOperatorConfiguration := &cm.chopConfigList.Items[i]
if chOperatorConfiguration.Name == name {
// Save location info into OperatorConfig itself
chOperatorConfiguration.Spec.Runtime.ConfigCRNamespace = namespace
chOperatorConfiguration.Spec.Runtime.ConfigCRName = name
cm.crConfigs = append(cm.crConfigs, &chOperatorConfiguration.Spec)
log.V(1).F().Error("Append ClickHouseOperatorConfigurations '%s/%s'.", namespace, name)
continue
}
}
}
}
// logAllCRBasedConfigs writes all ClickHouseOperatorConfiguration objects into log
func (cm *ConfigManager) logAllCRBasedConfigs() {
for _, chOperatorConfiguration := range cm.crConfigs {
log.V(1).Info("CR-based chop config: %s/%s :", chOperatorConfiguration.Runtime.ConfigCRNamespace, chOperatorConfiguration.Runtime.ConfigCRName)
log.V(1).Info("\n" + chOperatorConfiguration.String(true))
}
}
// buildUnifiedConfig prepares one config from all accumulated parts
func (cm *ConfigManager) buildUnifiedConfig() {
// Start with file config as a base
cm.config = cm.fileConfig
cm.fileConfig = nil
// Merge all the rest CR-based configs into base config
for _, chOperatorConfiguration := range cm.crConfigs {
_ = cm.config.MergeFrom(chOperatorConfiguration, api.MergeTypeOverrideByNonEmptyValues)
cm.config.Runtime.ConfigCRSources = append(cm.config.Runtime.ConfigCRSources, api.ConfigCRSource{
Namespace: chOperatorConfiguration.Runtime.ConfigCRNamespace,
Name: chOperatorConfiguration.Runtime.ConfigCRName,
})
}
}
// IsConfigListed checks whether specified ClickHouseOperatorConfiguration is listed in list of ClickHouseOperatorConfiguration(s)
func (cm *ConfigManager) IsConfigListed(config *api.ClickHouseOperatorConfiguration) bool {
for i := range cm.chopConfigList.Items {
chOperatorConfiguration := &cm.chopConfigList.Items[i]
if config.Namespace == chOperatorConfiguration.Namespace &&
config.Name == chOperatorConfiguration.Name &&
config.ResourceVersion == chOperatorConfiguration.ResourceVersion {
// Yes, this config already listed with the same resource version
return true
}
}
return false
}
const (
fileIsOptional = true
fileIsMandatory = false
)
// getFileBasedConfig creates one OperatorConfig object based on the first available configuration file
func (cm *ConfigManager) getFileBasedConfig(configFilePath string) (*api.OperatorConfig, error) {
// Check config files availability in the following order:
// 1. Explicitly specified config file as CLI option
// 2. Explicitly specified config file as ENV var
// 3. Well-known config file in home dir
// 4. Well-known config file in /etc
// In case no file found fallback to the default config
// 1. Check config file as explicitly specified CLI option
if len(configFilePath) > 0 {
// Config file is explicitly specified as a CLI flag, has to have this file.
// Absence of the file is an error.
conf, err := cm.buildConfigFromFile(configFilePath, fileIsMandatory)
if err != nil {
return nil, err
}
if conf != nil {
return conf, nil
}
// Since this file is a mandatory one - has to fail
return nil, fmt.Errorf("welcome Schrodinger: no conf, no err")
}
// 2. Check config file as explicitly specified ENV var
configFilePath = os.Getenv(deployment.CHOP_CONFIG)
if len(configFilePath) > 0 {
// Config file is explicitly specified as an ENV var, has to have this file.
// Absence of the file is an error.
conf, err := cm.buildConfigFromFile(configFilePath, fileIsMandatory)
if err != nil {
return nil, err
}
if conf != nil {
return conf, nil
}
// Since this file is a mandatory one - has to fail
return nil, fmt.Errorf("welcome Schrodinger: no conf, no err")
}
// 3. Check config file as well-known config file in home dir
// Try to find ~/.clickhouse-operator/config.yaml
if usr, err := user.Current(); err == nil {
// OS user found. Parse ~/.clickhouse-operator/config.yaml file
// File is optional, absence of the file is not an error.
configFilePath = filepath.Join(usr.HomeDir, ".clickhouse-operator", "config.yaml")
conf, err := cm.buildConfigFromFile(configFilePath, fileIsOptional)
if err != nil {
return nil, err
}
if conf != nil {
return conf, nil
}
// Since this file is an optional one - no return, continue to the next option
}
// 3. Check config file as well-known config file in /etc
// Try to find /etc/clickhouse-operator/config.yaml
{
configFilePath = "/etc/clickhouse-operator/config.yaml"
// File is optional, absence of the file is not an error
conf, err := cm.buildConfigFromFile(configFilePath, fileIsOptional)
if err != nil {
return nil, err
}
if conf != nil {
return conf, nil
}
// Since this file is an optional one - no return, continue to the next option
}
// No any config file found, fallback to default configuration
return cm.buildDefaultConfig()
}
// buildConfigFromFile returns OperatorConfig struct built out of specified file path
func (cm *ConfigManager) buildConfigFromFile(configFilePath string, optional bool) (*api.OperatorConfig, error) {
if _, err := os.Stat(configFilePath); errors.Is(err, os.ErrNotExist) && optional {
// File does not exist, but it is optional. so there is not error per se
return nil, nil
}
// Read config file content
yamlText, err := os.ReadFile(filepath.Clean(configFilePath))
if err != nil {
return nil, err
}
// Parse config file content into OperatorConfig struct
config := new(api.OperatorConfig)
err = yaml.Unmarshal(yamlText, config)
if err != nil {
return nil, err
}
// Fill OperatorConfig's paths
config.Runtime.ConfigFilePath, _ = filepath.Abs(configFilePath)
config.Runtime.ConfigFolderPath = filepath.Dir(config.Runtime.ConfigFilePath)
return config, nil
}
// buildDefaultConfig returns default OperatorConfig
func (cm *ConfigManager) buildDefaultConfig() (*api.OperatorConfig, error) {
config := api.OperatorConfig{}
return &config, nil
}
// listSupportedEnvVarNames return list of ENV vars that the operator supports
func (cm *ConfigManager) listSupportedEnvVarNames() []string {
// This list of ENV VARS is specified in operator .yaml manifest, section "kind: Deployment"
return []string{
deployment.OPERATOR_POD_NODE_NAME,
deployment.OPERATOR_POD_NAME,
deployment.OPERATOR_POD_NAMESPACE,
deployment.OPERATOR_POD_IP,
deployment.OPERATOR_POD_SERVICE_ACCOUNT,
deployment.OPERATOR_CONTAINER_CPU_REQUEST,
deployment.OPERATOR_CONTAINER_CPU_LIMIT,
deployment.OPERATOR_CONTAINER_MEM_REQUEST,
deployment.OPERATOR_CONTAINER_MEM_LIMIT,
deployment.WATCH_NAMESPACE,
deployment.WATCH_NAMESPACES,
}
}
// getEnvVarParams returns base set of runtime parameters filled by ENV vars
func (cm *ConfigManager) getEnvVarParams() map[string]string {
params := make(map[string]string)
// Extract parameters from ENV VARS
for _, varName := range cm.listSupportedEnvVarNames() {
params[varName] = os.Getenv(varName)
}
return params
}
// logEnvVarParams writes runtime parameters into log
func (cm *ConfigManager) logEnvVarParams() {
// Log params according to sorted names
// So we need to
// 1. Extract and sort names aka keys
// 2. Walk over keys and log params
// Sort names aka keys
var keys []string
for k := range cm.runtimeParams {
keys = append(keys, k)
}
sort.Strings(keys)
// Walk over sorted names aka keys
log.V(1).Info("Parameters num: %d", len(cm.runtimeParams))
for _, k := range keys {
log.V(1).Info("%s=%s", k, cm.runtimeParams[k])
}
}
// HasRuntimeParam checks whether specified runtime param exists
func (cm *ConfigManager) HasRuntimeParam(name string) bool {
_map := cm.getEnvVarParams()
_, ok := _map[name]
return ok
}
// GetRuntimeParam gets specified runtime param
func (cm *ConfigManager) GetRuntimeParam(name string) (string, bool) {
_map := cm.getEnvVarParams()
value, ok := _map[name]
return value, ok
}
// fetchSecretCredentials
func (cm *ConfigManager) fetchSecretCredentials() {
// Secret name where to look for ClickHouse access credentials
name := cm.config.ClickHouse.Access.Secret.Name
// Do we need to fetch credentials from the secret?
if name == "" {
// No secret name specified, no need to read it
return
}
// We have secret name specified, let's move on and read credentials
// Figure out namespace where to look for the secret
namespace := cm.config.ClickHouse.Access.Secret.Namespace
if namespace == "" {
// No namespace explicitly specified, let's look into namespace where pod is running
if cm.HasRuntimeParam(deployment.OPERATOR_POD_NAMESPACE) {
namespace, _ = cm.GetRuntimeParam(deployment.OPERATOR_POD_NAMESPACE)
}
}
log.V(1).Info("Going to search for username/password in the secret '%s/%s'", namespace, name)
// Sanity check
if namespace == "" {
// We've already checked that name is not empty
cm.config.ClickHouse.Access.Secret.Runtime.Error = fmt.Sprintf("Still empty namespace for secret '%s'", name)
return
}
secret, err := cm.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), name, controller.NewGetOptions())
if err != nil {
cm.config.ClickHouse.Access.Secret.Runtime.Error = err.Error()
log.V(1).Warning("Unable to fetch secret: '%s/%s'", namespace, name)
return
}
cm.config.ClickHouse.Access.Secret.Runtime.Fetched = true
log.V(1).Info("Secret fetched: '%s/%s'", namespace, name)
// Find username and password from credentials
for key, value := range secret.Data {
switch key {
case "username":
cm.config.ClickHouse.Access.Secret.Runtime.Username = string(value)
log.V(1).Info("Username read from the secret: '%s/%s'", namespace, name)
case "password":
cm.config.ClickHouse.Access.Secret.Runtime.Password = string(value)
log.V(1).Info("Password read from the secret: '%s/%s'", namespace, name)
}
}
}
// Postprocess performs postprocessing of the configuration
func (cm *ConfigManager) Postprocess() {
cm.config.Postprocess()
}