github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/configuration/config_manager/config_handler_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package configmanager 21 22 import ( 23 "context" 24 "io/fs" 25 "os" 26 "os/signal" 27 "path/filepath" 28 "syscall" 29 "time" 30 31 . "github.com/onsi/ginkgo/v2" 32 . "github.com/onsi/gomega" 33 34 "github.com/fsnotify/fsnotify" 35 36 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 37 "github.com/1aal/kubeblocks/pkg/configuration/util" 38 testutil "github.com/1aal/kubeblocks/pkg/testutil/k8s" 39 ) 40 41 var _ = Describe("Config Handler Test", func() { 42 43 var tmpWorkDir string 44 var mockK8sCli *testutil.K8sClientMockHelper 45 46 const ( 47 oldVersion = "[test]\na = 1\nb = 2\n" 48 newVersion = "[test]\na = 2\nb = 2\n\nc = 100" 49 ) 50 51 BeforeEach(func() { 52 // Add any setup steps that needs to be executed before each test 53 mockK8sCli = testutil.NewK8sMockClient() 54 tmpWorkDir, _ = os.MkdirTemp(os.TempDir(), "test-handle-") 55 }) 56 57 AfterEach(func() { 58 os.RemoveAll(tmpWorkDir) 59 DeferCleanup(mockK8sCli.Finish) 60 }) 61 62 newConfigSpec := func() appsv1alpha1.ComponentConfigSpec { 63 return appsv1alpha1.ComponentConfigSpec{ 64 ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ 65 Name: "config", 66 TemplateRef: "config-template", 67 VolumeName: "/opt/config", 68 Namespace: "default", 69 }, 70 ConfigConstraintRef: "config-constraint", 71 } 72 } 73 74 newFormatter := func() appsv1alpha1.FormatterConfig { 75 return appsv1alpha1.FormatterConfig{ 76 FormatterOptions: appsv1alpha1.FormatterOptions{ 77 IniConfig: &appsv1alpha1.IniConfig{ 78 SectionName: "test", 79 }, 80 }, 81 Format: appsv1alpha1.Ini, 82 } 83 } 84 85 newUnixSignalConfig := func() ConfigSpecInfo { 86 return ConfigSpecInfo{ 87 ReloadOptions: &appsv1alpha1.ReloadOptions{ 88 UnixSignalTrigger: &appsv1alpha1.UnixSignalTrigger{ 89 ProcessName: findCurrProcName(), 90 Signal: appsv1alpha1.SIGHUP, 91 }}, 92 ReloadType: appsv1alpha1.UnixSignalType, 93 MountPoint: "/tmp/test", 94 ConfigSpec: newConfigSpec(), 95 } 96 } 97 98 newShellConfig := func(mountPoint string) ConfigSpecInfo { 99 return ConfigSpecInfo{ 100 ReloadOptions: &appsv1alpha1.ReloadOptions{ 101 ShellTrigger: &appsv1alpha1.ShellTrigger{ 102 Command: []string{"sh", "-c", `echo "hello world" "$@"`}, 103 }}, 104 ReloadType: appsv1alpha1.ShellType, 105 MountPoint: mountPoint, 106 ConfigSpec: newConfigSpec(), 107 FormatterConfig: newFormatter(), 108 } 109 } 110 111 newDownwardAPIOptions := func() []appsv1alpha1.DownwardAPIOption { 112 return []appsv1alpha1.DownwardAPIOption{ 113 { 114 Name: "labels", 115 MountPoint: filepath.Join(tmpWorkDir, "labels"), 116 Command: []string{"sh", "-c", `echo "labels trigger"`}, 117 }, 118 { 119 Name: "annotations", 120 MountPoint: filepath.Join(tmpWorkDir, "annotations"), 121 Command: []string{"sh", "-c", `echo "annotation trigger"`}, 122 }, 123 } 124 } 125 126 newDownwardAPIConfig := func() ConfigSpecInfo { 127 return ConfigSpecInfo{ 128 ReloadOptions: &appsv1alpha1.ReloadOptions{ 129 ShellTrigger: &appsv1alpha1.ShellTrigger{ 130 Command: []string{"sh", "-c", `echo "hello world" "$@"`}, 131 }, 132 }, 133 ReloadType: appsv1alpha1.ShellType, 134 MountPoint: tmpWorkDir, 135 ConfigSpec: newConfigSpec(), 136 FormatterConfig: newFormatter(), 137 DownwardAPIOptions: newDownwardAPIOptions(), 138 } 139 } 140 141 newTPLScriptsConfig := func(configPath string) ConfigSpecInfo { 142 return ConfigSpecInfo{ 143 ReloadOptions: &appsv1alpha1.ReloadOptions{ 144 TPLScriptTrigger: &appsv1alpha1.TPLScriptTrigger{}, 145 }, 146 ReloadType: appsv1alpha1.TPLScriptType, 147 MountPoint: "/tmp/test", 148 ConfigSpec: newConfigSpec(), 149 FormatterConfig: newFormatter(), 150 TPLConfig: configPath, 151 } 152 } 153 154 prepareTestConfig := func(configPath string, config string) { 155 fileInfo, err := os.Stat(configPath) 156 if err != nil { 157 Expect(os.IsNotExist(err)).To(BeTrue()) 158 } 159 if fileInfo == nil { 160 Expect(os.MkdirAll(configPath, fs.ModePerm)).Should(Succeed()) 161 } 162 Expect(os.WriteFile(filepath.Join(configPath, "my.cnf"), []byte(config), fs.ModePerm)).Should(Succeed()) 163 } 164 165 toJSONString := func(v ConfigSpecInfo) string { 166 b, err := util.ToYamlConfig([]ConfigSpecInfo{v}) 167 Expect(err).Should(Succeed()) 168 configFile := filepath.Join(tmpWorkDir, configManagerConfig) 169 Expect(os.WriteFile(configFile, b, fs.ModePerm)).Should(Succeed()) 170 return configFile 171 } 172 173 Context("TestSimpleHandler", func() { 174 It("CreateSignalHandler", func() { 175 _, err := CreateSignalHandler(appsv1alpha1.SIGALRM, "test", "") 176 Expect(err).Should(Succeed()) 177 _, err = CreateSignalHandler("NOSIGNAL", "test", "") 178 Expect(err.Error()).To(ContainSubstring("not supported unix signal: NOSIGNAL")) 179 }) 180 181 It("CreateShellHandler", func() { 182 _, err := CreateExecHandler(nil, "", nil, "") 183 Expect(err.Error()).To(ContainSubstring("invalid command")) 184 _, err = CreateExecHandler([]string{}, "", nil, "") 185 Expect(err.Error()).To(ContainSubstring("invalid command")) 186 c, err := CreateExecHandler([]string{"go", "version"}, "", &ConfigSpecInfo{ 187 ConfigSpec: appsv1alpha1.ComponentConfigSpec{ 188 ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ 189 Name: "for_test", 190 }}}, 191 "") 192 Expect(err).Should(Succeed()) 193 Expect(c.VolumeHandle(context.Background(), fsnotify.Event{})).Should(Succeed()) 194 }) 195 196 It("CreateTPLScriptHandler", func() { 197 mockK8sTestConfigureDirectory(filepath.Join(tmpWorkDir, "config"), "my.cnf", "xxxx") 198 tplFile := filepath.Join(tmpWorkDir, "test.tpl") 199 configFile := filepath.Join(tmpWorkDir, "config.yaml") 200 Expect(os.WriteFile(tplFile, []byte(``), fs.ModePerm)).Should(Succeed()) 201 202 tplConfig := TPLScriptConfig{Scripts: "test.tpl"} 203 b, _ := util.ToYamlConfig(tplConfig) 204 Expect(os.WriteFile(configFile, b, fs.ModePerm)).Should(Succeed()) 205 206 _, err := CreateTPLScriptHandler("", configFile, []string{filepath.Join(tmpWorkDir, "config")}, "") 207 Expect(err).Should(Succeed()) 208 }) 209 210 }) 211 212 Context("TestConfigHandler", func() { 213 It("SignalHandler", func() { 214 config := newUnixSignalConfig() 215 handler, err := CreateCombinedHandler(toJSONString(config), filepath.Join(tmpWorkDir, "backup")) 216 Expect(err).Should(Succeed()) 217 Expect(handler.MountPoint()).Should(ContainElement(config.MountPoint)) 218 219 // process unix signal 220 trigger := make(chan bool) 221 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGHUP) 222 defer stop() 223 224 go func() { 225 select { 226 case <-time.After(5 * time.Second): 227 // not walk here 228 Expect(true).Should(BeFalse()) 229 case <-ctx.Done(): 230 stop() 231 trigger <- true 232 } 233 }() 234 By("process unix signal") 235 Expect(handler.VolumeHandle(ctx, fsnotify.Event{Name: config.MountPoint})).Should(Succeed()) 236 237 select { 238 case <-time.After(10 * time.Second): 239 logger.Info("failed to watch volume.") 240 Expect(true).Should(BeFalse()) 241 case <-trigger: 242 logger.Info("success to watch volume.") 243 Expect(true).To(BeTrue()) 244 } 245 246 By("not support handler") 247 Expect(handler.OnlineUpdate(ctx, "", nil).Error()).Should(ContainSubstring("not found handler for config name")) 248 Expect(handler.OnlineUpdate(ctx, config.ConfigSpec.Name, nil).Error()).Should(ContainSubstring("not support online update")) 249 By("not match mount point") 250 Expect(handler.VolumeHandle(ctx, fsnotify.Event{Name: "not_exist_mount_point"})).Should(Succeed()) 251 }) 252 253 It("ShellHandler", func() { 254 configPath := filepath.Join(tmpWorkDir, "config") 255 prepareTestConfig(configPath, oldVersion) 256 config := newShellConfig(configPath) 257 handler, err := CreateCombinedHandler(toJSONString(config), filepath.Join(tmpWorkDir, "backup")) 258 Expect(err).Should(Succeed()) 259 Expect(handler.MountPoint()).Should(ContainElement(configPath)) 260 261 // mock modify config 262 prepareTestConfig(configPath, newVersion) 263 By("change config") 264 Expect(handler.VolumeHandle(context.TODO(), fsnotify.Event{Name: configPath})).Should(Succeed()) 265 By("not change config") 266 Expect(handler.VolumeHandle(context.TODO(), fsnotify.Event{Name: configPath})).Should(Succeed()) 267 By("not support onlineUpdate") 268 Expect(handler.OnlineUpdate(context.TODO(), config.ConfigSpec.Name, nil)).Should(Succeed()) 269 }) 270 271 It("TplScriptsHandler", func() { 272 By("mock command channel") 273 newCommandChannel = func(ctx context.Context, dataType, dsn string) (DynamicParamUpdater, error) { 274 return mockCChannel, nil 275 } 276 277 tplFile := filepath.Join(tmpWorkDir, "test.tpl") 278 configFile := filepath.Join(tmpWorkDir, "config.yaml") 279 Expect(os.WriteFile(tplFile, []byte(``), fs.ModePerm)).Should(Succeed()) 280 281 tplConfig := TPLScriptConfig{ 282 Scripts: "test.tpl", 283 FormatterConfig: newFormatter(), 284 } 285 b, _ := util.ToYamlConfig(tplConfig) 286 Expect(os.WriteFile(configFile, b, fs.ModePerm)).Should(Succeed()) 287 288 config := newTPLScriptsConfig(configFile) 289 handler, err := CreateCombinedHandler(toJSONString(config), "") 290 Expect(err).Should(Succeed()) 291 Expect(handler.OnlineUpdate(context.TODO(), config.ConfigSpec.Name, map[string]string{ 292 "param_a": "a", 293 "param_b": "b", 294 })).Should(Succeed()) 295 }) 296 297 It("TplScriptsHandler Volume Event", func() { 298 By("mock command channel") 299 newCommandChannel = func(ctx context.Context, dataType, dsn string) (DynamicParamUpdater, error) { 300 return mockCChannel, nil 301 } 302 303 By("prepare config data") 304 configPath := filepath.Join(tmpWorkDir, "config") 305 prepareTestConfig(configPath, oldVersion) 306 307 tplFile := filepath.Join(tmpWorkDir, "test.tpl") 308 configFile := filepath.Join(tmpWorkDir, "config.yaml") 309 Expect(os.WriteFile(tplFile, []byte(``), fs.ModePerm)).Should(Succeed()) 310 311 tplConfig := TPLScriptConfig{ 312 Scripts: "test.tpl", 313 FormatterConfig: newFormatter(), 314 } 315 b, _ := util.ToYamlConfig(tplConfig) 316 Expect(os.WriteFile(configFile, b, fs.ModePerm)).Should(Succeed()) 317 318 config := newTPLScriptsConfig(configFile) 319 config.MountPoint = configPath 320 handler, err := CreateCombinedHandler(toJSONString(config), filepath.Join(tmpWorkDir, "backup")) 321 Expect(err).Should(Succeed()) 322 323 By("change config") 324 prepareTestConfig(configPath, newVersion) 325 Expect(handler.VolumeHandle(context.TODO(), fsnotify.Event{Name: configPath})).Should(Succeed()) 326 327 }) 328 329 It("DownwardAPIsHandler", func() { 330 config := newDownwardAPIConfig() 331 handler, err := CreateCombinedHandler(toJSONString(config), filepath.Join(tmpWorkDir, "backup")) 332 Expect(err).Should(Succeed()) 333 Expect(handler.MountPoint()).Should(ContainElement(config.MountPoint)) 334 Expect(handler.VolumeHandle(context.TODO(), fsnotify.Event{Name: config.DownwardAPIOptions[0].MountPoint})).Should(Succeed()) 335 Expect(handler.VolumeHandle(context.TODO(), fsnotify.Event{Name: config.DownwardAPIOptions[1].MountPoint})).Should(Succeed()) 336 }) 337 }) 338 }) 339 340 func mockK8sTestConfigureDirectory(mockDirectory string, cfgFile, content string) { 341 var ( 342 tmpVolumeDir = filepath.Join(mockDirectory, "..2023_06_16_06_06_06.1234567") 343 configFilePath = filepath.Join(tmpVolumeDir, cfgFile) 344 tmpDataDir = filepath.Join(mockDirectory, "..data_tmp") 345 watchedDataDir = filepath.Join(mockDirectory, "..data") 346 ) 347 348 // wait inotify ready 349 Expect(os.MkdirAll(tmpVolumeDir, fs.ModePerm)).Should(Succeed()) 350 Expect(os.WriteFile(configFilePath, []byte(content), fs.ModePerm)).Should(Succeed()) 351 Expect(os.Chmod(configFilePath, fs.ModePerm)).Should(Succeed()) 352 353 pwd, err := os.Getwd() 354 Expect(err).Should(Succeed()) 355 defer func() { 356 _ = os.Chdir(pwd) 357 }() 358 359 Expect(os.Chdir(mockDirectory)) 360 Expect(os.Symlink(filepath.Base(tmpVolumeDir), filepath.Base(tmpDataDir))).Should(Succeed()) 361 Expect(os.Rename(tmpDataDir, watchedDataDir)).Should(Succeed()) 362 Expect(os.Symlink(filepath.Join(filepath.Base(watchedDataDir), cfgFile), cfgFile)).Should(Succeed()) 363 } 364 365 type mockCommandChannel struct { 366 } 367 368 func (m *mockCommandChannel) ExecCommand(ctx context.Context, command string, args ...string) (string, error) { 369 return "", nil 370 } 371 372 func (m *mockCommandChannel) Close() { 373 } 374 375 var mockCChannel = &mockCommandChannel{}