github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/configuration/config_manager/builder_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  	"reflect"
    25  	"testing"
    26  
    27  	. "github.com/onsi/ginkgo/v2"
    28  	. "github.com/onsi/gomega"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  	corev1 "k8s.io/api/core/v1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/util/yaml"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  
    36  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    37  	testutil "github.com/1aal/kubeblocks/pkg/testutil/k8s"
    38  )
    39  
    40  var _ = Describe("Config Builder Test", func() {
    41  
    42  	const (
    43  		scriptsName = "script_cm"
    44  		scriptsNS   = "default"
    45  
    46  		lazyRenderedTemplateName = "lazy-rendered-template"
    47  	)
    48  
    49  	var mockK8sCli *testutil.K8sClientMockHelper
    50  
    51  	BeforeEach(func() {
    52  		// Add any setup steps that needs to be executed before each test
    53  		mockK8sCli = testutil.NewK8sMockClient()
    54  	})
    55  
    56  	AfterEach(func() {
    57  		DeferCleanup(mockK8sCli.Finish)
    58  	})
    59  
    60  	syncFn := func(sync bool) *bool { r := sync; return &r }
    61  
    62  	newVolumeMounts := func() []corev1.VolumeMount {
    63  		return []corev1.VolumeMount{
    64  			{
    65  				MountPath: "/postgresql/conf",
    66  				Name:      "pg_config",
    67  			}}
    68  	}
    69  	newVolumeMounts2 := func() []corev1.VolumeMount {
    70  		return []corev1.VolumeMount{
    71  			{
    72  				MountPath: "/postgresql/conf",
    73  				Name:      "pg_config",
    74  			},
    75  			{
    76  				MountPath: "/postgresql/conf2",
    77  				Name:      "pg_config",
    78  			}}
    79  	}
    80  	newReloadOptions := func(t appsv1alpha1.CfgReloadType, sync *bool) *appsv1alpha1.ReloadOptions {
    81  		signalHandle := &appsv1alpha1.UnixSignalTrigger{
    82  			ProcessName: "postgres",
    83  			Signal:      appsv1alpha1.SIGHUP,
    84  		}
    85  		shellHandle := &appsv1alpha1.ShellTrigger{
    86  			Command: []string{"pwd"},
    87  		}
    88  		scriptHandle := &appsv1alpha1.TPLScriptTrigger{
    89  			Sync: sync,
    90  			ScriptConfig: appsv1alpha1.ScriptConfig{
    91  				ScriptConfigMapRef: "reload-script",
    92  				Namespace:          scriptsNS,
    93  			},
    94  		}
    95  
    96  		switch t {
    97  		default:
    98  			return nil
    99  		case appsv1alpha1.UnixSignalType:
   100  			return &appsv1alpha1.ReloadOptions{
   101  				UnixSignalTrigger: signalHandle}
   102  		case appsv1alpha1.ShellType:
   103  			return &appsv1alpha1.ReloadOptions{
   104  				ShellTrigger: shellHandle}
   105  		case appsv1alpha1.TPLScriptType:
   106  			return &appsv1alpha1.ReloadOptions{
   107  				TPLScriptTrigger: scriptHandle}
   108  		}
   109  	}
   110  	newConfigSpecMeta := func() []ConfigSpecMeta {
   111  		return []ConfigSpecMeta{
   112  			{
   113  				ConfigSpecInfo: ConfigSpecInfo{
   114  					ConfigSpec: appsv1alpha1.ComponentConfigSpec{
   115  						ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   116  							Name:       "pg_config",
   117  							VolumeName: "pg_config",
   118  						},
   119  					},
   120  				},
   121  			},
   122  		}
   123  	}
   124  
   125  	newCMBuildParams := func(hasScripts bool) *CfgManagerBuildParams {
   126  		param := &CfgManagerBuildParams{
   127  			Cluster: &appsv1alpha1.Cluster{
   128  				ObjectMeta: metav1.ObjectMeta{
   129  					Name:      "abcd",
   130  					Namespace: "default",
   131  				},
   132  			},
   133  			ComponentName:             "test",
   134  			Volumes:                   newVolumeMounts(),
   135  			ConfigSpecsBuildParams:    newConfigSpecMeta(),
   136  			ConfigLazyRenderedVolumes: make(map[string]corev1.VolumeMount),
   137  			DownwardAPIVolumes:        make([]corev1.VolumeMount, 0),
   138  		}
   139  		if hasScripts {
   140  			param.ConfigSpecsBuildParams[0].ScriptConfig = []appsv1alpha1.ScriptConfig{
   141  				{
   142  					Namespace:          scriptsNS,
   143  					ScriptConfigMapRef: scriptsName,
   144  				},
   145  			}
   146  		}
   147  		return param
   148  	}
   149  
   150  	mockTplScriptCM := func() {
   151  		mockK8sCli.MockGetMethod(testutil.WithGetReturned(testutil.WithConstructSimpleGetResult([]client.Object{
   152  			&corev1.ConfigMap{
   153  				ObjectMeta: metav1.ObjectMeta{
   154  					Name:      "reload-script",
   155  					Namespace: scriptsNS,
   156  				},
   157  				Data: map[string]string{
   158  					"reload.yaml": `
   159  scripts: reload.tpl
   160  fileRegex: my.cnf
   161  formatterConfig:
   162    format: ini
   163  `,
   164  				}},
   165  			&corev1.ConfigMap{
   166  				ObjectMeta: metav1.ObjectMeta{
   167  					Name:      lazyRenderedTemplateName,
   168  					Namespace: scriptsNS,
   169  				},
   170  				Data: map[string]string{
   171  					"my.cnf": "",
   172  				}},
   173  		}), testutil.WithAnyTimes()))
   174  		mockK8sCli.MockCreateMethod(testutil.WithCreateReturned(testutil.WithCreatedSucceedResult(), testutil.WithAnyTimes()))
   175  	}
   176  
   177  	newDownwardAPIVolumes := func() []appsv1alpha1.DownwardAPIOption {
   178  		return []appsv1alpha1.DownwardAPIOption{
   179  			{
   180  				Name:       "downward-api",
   181  				MountPoint: "/etc/podinfo",
   182  				Command:    []string{"/bin/true"},
   183  				Items: []corev1.DownwardAPIVolumeFile{
   184  					{
   185  						Path: "labels/role",
   186  						FieldRef: &corev1.ObjectFieldSelector{
   187  							FieldPath: `metadata.labels['kubeblocks.io/role']`,
   188  						},
   189  					},
   190  				},
   191  			},
   192  		}
   193  	}
   194  
   195  	Context("TestBuildConfigManagerContainer", func() {
   196  		It("builds unixSignal reloader correctly", func() {
   197  			param := newCMBuildParams(false)
   198  			mockTplScriptCM()
   199  			reloadOptions := newReloadOptions(appsv1alpha1.UnixSignalType, nil)
   200  			for i := range param.ConfigSpecsBuildParams {
   201  				buildParam := &param.ConfigSpecsBuildParams[i]
   202  				buildParam.ReloadOptions = reloadOptions
   203  				buildParam.ReloadType = appsv1alpha1.UnixSignalType
   204  			}
   205  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), ctx, param, newVolumeMounts2())).Should(Succeed())
   206  			for _, arg := range []string{`--volume-dir`, `/postgresql/conf`, `--volume-dir`, `/postgresql/conf2`} {
   207  				Expect(param.Args).Should(ContainElement(arg))
   208  			}
   209  		})
   210  
   211  		It("builds shellTrigger reloader correctly", func() {
   212  			mockK8sCli.MockGetMethod(testutil.WithGetReturned(testutil.WithConstructSimpleGetResult([]client.Object{
   213  				&corev1.ConfigMap{
   214  					ObjectMeta: metav1.ObjectMeta{
   215  						Name:      scriptsName,
   216  						Namespace: scriptsNS,
   217  					},
   218  				},
   219  			}), testutil.WithTimes(3)))
   220  			mockK8sCli.MockCreateMethod(testutil.WithCreateReturned(testutil.WithCreatedSucceedResult(), testutil.WithTimes(2)))
   221  
   222  			param := newCMBuildParams(true)
   223  			reloadOptions := newReloadOptions(appsv1alpha1.ShellType, nil)
   224  			for i := range param.ConfigSpecsBuildParams {
   225  				buildParam := &param.ConfigSpecsBuildParams[i]
   226  				buildParam.ReloadOptions = reloadOptions
   227  				buildParam.ReloadType = appsv1alpha1.ShellType
   228  			}
   229  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), context.TODO(), param, newVolumeMounts())).Should(Succeed())
   230  			for _, arg := range []string{`--volume-dir`, `/postgresql/conf`} {
   231  				Expect(param.Args).Should(ContainElement(arg))
   232  			}
   233  		})
   234  
   235  		It("builds tplScriptsTrigger reloader correctly", func() {
   236  			mockTplScriptCM()
   237  			param := newCMBuildParams(false)
   238  			reloadOptions := newReloadOptions(appsv1alpha1.TPLScriptType, syncFn(true))
   239  			for i := range param.ConfigSpecsBuildParams {
   240  				buildParam := &param.ConfigSpecsBuildParams[i]
   241  				buildParam.ReloadOptions = reloadOptions
   242  				buildParam.ReloadType = appsv1alpha1.TPLScriptType
   243  			}
   244  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), context.TODO(), param, newVolumeMounts())).Should(Succeed())
   245  			for _, arg := range []string{`--operator-update-enable`} {
   246  				Expect(param.Args).Should(ContainElement(arg))
   247  			}
   248  		})
   249  
   250  		It("builds tplScriptsTrigger reloader correctly with sync", func() {
   251  			mockTplScriptCM()
   252  			param := newCMBuildParams(false)
   253  			reloadOptions := newReloadOptions(appsv1alpha1.TPLScriptType, syncFn(false))
   254  			for i := range param.ConfigSpecsBuildParams {
   255  				buildParam := &param.ConfigSpecsBuildParams[i]
   256  				buildParam.ReloadOptions = reloadOptions
   257  				buildParam.ReloadType = appsv1alpha1.TPLScriptType
   258  			}
   259  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), context.TODO(), param, newVolumeMounts())).Should(Succeed())
   260  			for _, arg := range []string{`--volume-dir`, `/postgresql/conf`} {
   261  				Expect(param.Args).Should(ContainElement(arg))
   262  			}
   263  		})
   264  
   265  		It("builds secondary render correctly", func() {
   266  			mockTplScriptCM()
   267  			param := newCMBuildParams(false)
   268  			reloadOptions := newReloadOptions(appsv1alpha1.TPLScriptType, syncFn(false))
   269  			for i := range param.ConfigSpecsBuildParams {
   270  				buildParam := &param.ConfigSpecsBuildParams[i]
   271  				buildParam.ReloadOptions = reloadOptions
   272  				buildParam.ReloadType = appsv1alpha1.TPLScriptType
   273  				buildParam.ConfigSpec.LegacyRenderedConfigSpec = &appsv1alpha1.LegacyRenderedTemplateSpec{
   274  					ConfigTemplateExtension: appsv1alpha1.ConfigTemplateExtension{
   275  						Namespace:   scriptsNS,
   276  						TemplateRef: lazyRenderedTemplateName,
   277  					},
   278  				}
   279  			}
   280  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), context.TODO(), param, newVolumeMounts())).Should(Succeed())
   281  			for _, buildParam := range param.ConfigSpecsBuildParams {
   282  				Expect(FindVolumeMount(param.Volumes, GetConfigVolumeName(buildParam.ConfigSpec))).ShouldNot(BeNil())
   283  			}
   284  		})
   285  
   286  		It("builds downwardAPI correctly", func() {
   287  			mockTplScriptCM()
   288  			param := newCMBuildParams(false)
   289  			buildParam := &param.ConfigSpecsBuildParams[0]
   290  			buildParam.DownwardAPIOptions = newDownwardAPIVolumes()
   291  			buildParam.ReloadOptions = newReloadOptions(appsv1alpha1.TPLScriptType, syncFn(true))
   292  			Expect(BuildConfigManagerContainerParams(mockK8sCli.Client(), context.TODO(), param, newVolumeMounts())).Should(Succeed())
   293  			Expect(FindVolumeMount(param.DownwardAPIVolumes, buildParam.DownwardAPIOptions[0].Name)).ShouldNot(BeNil())
   294  		})
   295  	})
   296  
   297  })
   298  
   299  func TestCheckAndUpdateReloadYaml(t *testing.T) {
   300  	customEqual := func(l, r map[string]string) bool {
   301  		if len(l) != len(r) {
   302  			return false
   303  		}
   304  		var err error
   305  		for k, v := range l {
   306  			var lv any
   307  			var rv any
   308  			err = yaml.Unmarshal([]byte(v), &lv)
   309  			assert.Nil(t, err)
   310  			err = yaml.Unmarshal([]byte(r[k]), &rv)
   311  			assert.Nil(t, err)
   312  			if !reflect.DeepEqual(lv, rv) {
   313  				return false
   314  			}
   315  		}
   316  		return true
   317  	}
   318  
   319  	type args struct {
   320  		data            map[string]string
   321  		reloadConfig    string
   322  		formatterConfig *appsv1alpha1.FormatterConfig
   323  	}
   324  	tests := []struct {
   325  		name    string
   326  		args    args
   327  		want    map[string]string
   328  		wantErr bool
   329  	}{{
   330  		name: "testCheckAndUpdateReloadYaml",
   331  		args: args{
   332  			data: map[string]string{"reload.yaml": `
   333  fileRegex: my.cnf
   334  scripts: reload.tpl
   335  `},
   336  			reloadConfig: "reload.yaml",
   337  			formatterConfig: &appsv1alpha1.FormatterConfig{
   338  				Format: appsv1alpha1.Ini,
   339  			},
   340  		},
   341  		wantErr: false,
   342  		want: map[string]string{"reload.yaml": `
   343  scripts: reload.tpl
   344  fileRegex: my.cnf
   345  formatterConfig:
   346    format: ini
   347  `,
   348  		},
   349  	}, {
   350  		name: "testCheckAndUpdateReloadYaml",
   351  		args: args{
   352  			data:            map[string]string{},
   353  			reloadConfig:    "reload.yaml",
   354  			formatterConfig: &appsv1alpha1.FormatterConfig{Format: appsv1alpha1.Ini},
   355  		},
   356  		wantErr: true,
   357  		want:    map[string]string{},
   358  	}}
   359  	for _, tt := range tests {
   360  		t.Run(tt.name, func(t *testing.T) {
   361  			got, err := checkAndUpdateReloadYaml(tt.args.data, tt.args.reloadConfig, *tt.args.formatterConfig)
   362  			if (err != nil) != tt.wantErr {
   363  				t.Errorf("checkAndUpdateReloadYaml() error = %v, wantErr %v", err, tt.wantErr)
   364  				return
   365  			}
   366  			if !customEqual(got, tt.want) {
   367  				t.Errorf("checkAndUpdateReloadYaml() got = %v, want %v", got, tt.want)
   368  			}
   369  		})
   370  	}
   371  }