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{}