github.com/cloudberrydb/gpbackup@v1.0.3-0.20240118031043-5410fd45eed6/utils/plugin_test.go (about) 1 package utils_test 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "regexp" 9 "strconv" 10 "strings" 11 12 "github.com/blang/semver" 13 "github.com/cloudberrydb/gp-common-go-libs/cluster" 14 "github.com/cloudberrydb/gp-common-go-libs/iohelper" 15 "github.com/cloudberrydb/gp-common-go-libs/operating" 16 "github.com/cloudberrydb/gp-common-go-libs/testhelper" 17 "github.com/cloudberrydb/gpbackup/testutils" 18 "github.com/cloudberrydb/gpbackup/utils" 19 "github.com/pkg/errors" 20 21 . "github.com/onsi/ginkgo/v2" 22 . "github.com/onsi/gomega" 23 ) 24 25 var _ = Describe("utils/plugin tests", func() { 26 var testCluster *cluster.Cluster 27 var executor testutils.TestExecutorMultiple 28 var subject utils.PluginConfig 29 var tempDir string 30 31 BeforeEach(func() { 32 operating.InitializeSystemFunctions() 33 tempDir, _ = ioutil.TempDir("", "temp") 34 operating.System.Stdout = stdout 35 subject = utils.PluginConfig{ 36 ExecutablePath: "/a/b/myPlugin", 37 ConfigPath: "/tmp/my_plugin_config.yaml", 38 Options: make(map[string]string), 39 } 40 subject.Options = make(map[string]string) 41 executor = testutils.TestExecutorMultiple{ 42 ClusterOutputs: make([]*cluster.RemoteOutput, 2), 43 } 44 executor.ClusterOutputs[0] = &cluster.RemoteOutput{ 45 Commands: []cluster.ShellCommand{ 46 cluster.ShellCommand{Content: -1, Stdout: utils.RequiredPluginVersion}, 47 cluster.ShellCommand{Content: 0, Stdout: utils.RequiredPluginVersion}, 48 cluster.ShellCommand{Content: 1, Stdout: utils.RequiredPluginVersion}, 49 }, 50 } 51 executor.ClusterOutputs[1] = &cluster.RemoteOutput{ 52 Commands: []cluster.ShellCommand{ 53 cluster.ShellCommand{Content: -1, Stdout: "myPlugin version 1.2.3"}, 54 cluster.ShellCommand{Content: 0, Stdout: "myPlugin version 1.2.3"}, 55 cluster.ShellCommand{Content: 1, Stdout: "myPlugin version 1.2.3"}, 56 }, 57 } 58 testCluster = cluster.NewCluster([]cluster.SegConfig{ 59 {ContentID: -1, DataDir: filepath.Join(tempDir, "seg-1"), Hostname: "coordinator", Port: 100}, 60 {ContentID: 0, DataDir: filepath.Join(tempDir, "seg0"), Hostname: "segment1", Port: 101}, 61 {ContentID: 1, DataDir: filepath.Join(tempDir, "seg1"), Hostname: "segment2", Port: 102}, 62 }) 63 testCluster.Executor = &executor 64 }) 65 AfterEach(func() { 66 err := os.RemoveAll(tempDir) 67 Expect(err).To(Not(HaveOccurred())) 68 _ = os.Remove(subject.ConfigPath) 69 confDir := filepath.Dir(subject.ConfigPath) 70 confFileName := filepath.Base(subject.ConfigPath) 71 files, _ := ioutil.ReadDir(confDir) 72 for _, f := range files { 73 match, _ := filepath.Match(confFileName+"*", f.Name()) 74 if match { 75 _ = os.Remove(confDir + "/" + f.Name()) 76 } 77 } 78 }) 79 Describe("plugin versions via CheckPluginExistsOnAllHosts()", func() { 80 It(" generates the correct commands", func() { 81 operating.System.Getenv = func(key string) string { 82 return "my/install/dir" 83 } 84 85 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 86 87 apiVersionCommands := executor.ClusterCommands[0] 88 expectedCommand := "source my/install/dir/greenplum_path.sh && /a/b/myPlugin plugin_api_version" 89 for _, shellCommands := range apiVersionCommands { 90 Expect(shellCommands.CommandString).To(ContainSubstring(expectedCommand)) 91 } 92 nativeVersionCommands := executor.ClusterCommands[1] 93 expectedCommand = "source my/install/dir/greenplum_path.sh && /a/b/myPlugin --version" 94 // for _, contentID := range testCluster.ContentIDs { 95 // cmd := nativeVersionCommands[contentID] 96 // Expect(cmd[len(cmd)-1]).To(Equal(expectedCommand)) 97 // } 98 for _, shellCommands := range nativeVersionCommands { 99 Expect(shellCommands.CommandString).To(ContainSubstring(expectedCommand)) 100 } 101 }) 102 }) 103 Describe("creates segment-specific plugin config and copies it to all hosts", func() { 104 It("appends PGPORT and the --version of the plugin", func() { 105 testConfigPath := "/tmp/my_plugin_config.yaml" 106 testConfigContents := ` 107 executablepath: /tmp/fake_path 108 options: 109 field1: 12 110 field2: hello 111 field3: 567 112 ` 113 err := ioutil.WriteFile(testConfigPath, []byte(testConfigContents), 0777) 114 Expect(err).To(Not(HaveOccurred())) 115 subject.SetBackupPluginVersion("myTimestamp", "my.test.version") 116 subject.CopyPluginConfigToAllHosts(testCluster) 117 118 Expect(executor.NumRemoteExecutions).To(Equal(1)) 119 cc := executor.ClusterCommands[0] 120 Expect(len(cc)).To(Equal(3)) 121 Expect(cc[0].Content).To(Equal(-1)) 122 Expect(cc[0].CommandString).To(MatchRegexp(`rsync -e ssh .*-1 coordinator:\/tmp\/my_plugin_config\.yaml; rm .*-1`)) 123 Expect(cc[1].Content).To(Equal(0)) 124 Expect(cc[1].CommandString).To(MatchRegexp(`rsync -e ssh .*0 segment1:\/tmp\/my_plugin_config\.yaml; rm .*0`)) 125 Expect(cc[2].Content).To(Equal(1)) 126 Expect(cc[2].CommandString).To(MatchRegexp(`rsync -e ssh .*1 segment2:\/tmp\/my_plugin_config\.yaml; rm .*1`)) 127 128 rgx := regexp.MustCompile(`rsync -e ssh (.*-1) coordinator:\/tmp\/my_plugin_config\.yaml; rm .*-1`) 129 rs := rgx.FindStringSubmatch(cc[0].CommandString) 130 coordinatorConfigPath := rs[1] 131 rgx = regexp.MustCompile(`rsync -e ssh (.*0) segment1:\/tmp\/my_plugin_config\.yaml; rm .*0`) 132 rs = rgx.FindStringSubmatch(cc[1].CommandString) 133 segmentOneConfigPath := rs[1] 134 rgx = regexp.MustCompile(`rsync -e ssh (.*1) segment2:\/tmp\/my_plugin_config\.yaml; rm .*1`) 135 rs = rgx.FindStringSubmatch(cc[2].CommandString) 136 segmentTwoConfigPath := rs[1] 137 138 // check contents 139 contents := strings.Join(iohelper.MustReadLinesFromFile(coordinatorConfigPath), "\n") 140 Expect(contents).To(ContainSubstring("\n pgport: \"100\"")) 141 Expect(contents).To(ContainSubstring("\n backup_plugin_version: my.test.version")) 142 contents = strings.Join(iohelper.MustReadLinesFromFile(segmentOneConfigPath), "\n") 143 Expect(contents).To(ContainSubstring("\n pgport: \"101\"")) 144 Expect(contents).To(ContainSubstring("\n backup_plugin_version: my.test.version")) 145 contents = strings.Join(iohelper.MustReadLinesFromFile(segmentTwoConfigPath), "\n") 146 Expect(contents).To(ContainSubstring("\n pgport: \"102\"")) 147 Expect(contents).To(ContainSubstring("\n backup_plugin_version: my.test.version")) 148 }) 149 When("copying for a plugin with encryption", func() { 150 It("copies the encryption key", func() { 151 executor.LocalOutput = "gpbackup_fake_plugin version 1.0.1+dev.28.g00c877e" 152 testConfigPath := "/tmp/my_plugin_config.yaml" 153 testConfigContents := ` 154 executablepath: /tmp/foobar 155 options: 156 field1: 12 157 field2: hello 158 field3: 567 159 ` 160 err := ioutil.WriteFile(testConfigPath, []byte(testConfigContents), 0777) 161 subject.Options["password_encryption"] = "on" 162 mdd := testCluster.GetDirForContent(-1) 163 _ = os.MkdirAll(mdd, 0777) 164 secretFilePath := filepath.Join(mdd, utils.SecretKeyFile) 165 secretFile := iohelper.MustOpenFileForWriting(secretFilePath) 166 _, err = secretFile.Write([]byte(`gpbackup_fake_plugin: 0123456789`)) 167 Expect(err).To(Not(HaveOccurred())) 168 169 subject.CopyPluginConfigToAllHosts(testCluster) 170 171 // check contents 172 cc := executor.ClusterCommands[0] 173 rgx := regexp.MustCompile(`rsync -e ssh (.*-1) coordinator:\/tmp\/my_plugin_config\.yaml; rm .*-1`) 174 rs := rgx.FindStringSubmatch(cc[0].CommandString) 175 coordinatorConfigPath := rs[1] 176 rgx = regexp.MustCompile(`rsync -e ssh (.*0) segment1:\/tmp\/my_plugin_config\.yaml; rm .*0`) 177 rs = rgx.FindStringSubmatch(cc[1].CommandString) 178 segmentOneConfigPath := rs[1] 179 rgx = regexp.MustCompile(`rsync -e ssh (.*1) segment2:\/tmp\/my_plugin_config\.yaml; rm .*1`) 180 rs = rgx.FindStringSubmatch(cc[2].CommandString) 181 segmentTwoConfigPath := rs[1] 182 183 // check contents 184 contents := strings.Join(iohelper.MustReadLinesFromFile(coordinatorConfigPath), "\n") 185 Expect(contents).To(ContainSubstring("\n gpbackup_fake_plugin: \"0123456789\"")) 186 contents = strings.Join(iohelper.MustReadLinesFromFile(segmentOneConfigPath), "\n") 187 Expect(contents).To(ContainSubstring("\n gpbackup_fake_plugin: \"0123456789\"")) 188 contents = strings.Join(iohelper.MustReadLinesFromFile(segmentTwoConfigPath), "\n") 189 Expect(contents).To(ContainSubstring("\n gpbackup_fake_plugin: \"0123456789\"")) 190 }) 191 It("writes a stdout message when encrypt key is not found", func() { 192 subject.Options["password_encryption"] = "on" 193 executor.LocalOutput = "gpbackup_fake_plugin version 1.0.1+dev.28.g00c877e" 194 pluginName, err := subject.GetPluginName(testCluster) 195 Expect(err).To(Not(HaveOccurred())) 196 errMsg := fmt.Sprintf("Cannot find encryption key for plugin %s. Please re-encrypt password(s) so that key becomes available.", pluginName) 197 defer testhelper.ShouldPanicWithMessage(errMsg) 198 subject.CopyPluginConfigToAllHosts(testCluster) 199 200 Expect(string(stdout.Contents())).To(ContainSubstring(errMsg)) 201 Expect(string(stdout.Contents())).To(ContainSubstring(errMsg)) 202 }) 203 }) 204 }) 205 Describe("version validation", func() { 206 When("version is equal to requirement", func() { 207 It("succeeds", func() { 208 subject.CheckPluginExistsOnAllHosts(testCluster) 209 }) 210 }) 211 When("version is greater than requirement", func() { 212 It("succeeds", func() { 213 // add one to whatever the current required version might be 214 version, _ := semver.Make(utils.RequiredPluginVersion) 215 greater, _ := semver.Make(strconv.Itoa(int(version.Major)+1) + ".0.0") 216 co := executor.ClusterOutputs[0].Commands 217 Expect(co[0].Content).To(Equal(-1)) 218 co[0].Stdout = greater.String() 219 Expect(co[1].Content).To(Equal(0)) 220 co[1].Stdout = greater.String() 221 Expect(co[2].Content).To(Equal(1)) 222 co[2].Stdout = greater.String() 223 224 co[1].Stdout = greater.String() 225 co[2].Stdout = greater.String() 226 227 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 228 }) 229 }) 230 When("version is too low", func() { 231 It("panics with message", func() { 232 co := executor.ClusterOutputs[0].Commands 233 co[0].Stdout = "0.2.0" 234 co[1].Stdout = "0.2.0" 235 co[2].Stdout = "0.2.0" 236 defer testhelper.ShouldPanicWithMessage("Plugin API version incorrect") 237 238 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 239 }) 240 }) 241 When("version cannot be parsed", func() { 242 It("panics with message", func() { 243 co := executor.ClusterOutputs[0].Commands 244 co[0].Stdout = "foo" 245 co[1].Stdout = "foo" 246 co[2].Stdout = "foo" 247 defer testhelper.ShouldPanicWithMessage("Unable to parse plugin API version") 248 249 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 250 }) 251 }) 252 When("version command fails", func() { 253 It("panics with message", func() { 254 subject.ExecutablePath = "myFailingPlugin" 255 executor.ClusterOutputs[0].NumErrors = 1 256 defer testhelper.ShouldPanicWithMessage("Unable to execute plugin myFailingPlugin") 257 258 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 259 }) 260 }) 261 When("version inconsistent", func() { 262 It("panics with message", func() { 263 executor.ClusterOutputs[0].Commands[0].Stdout = "99.99.9999" 264 defer testhelper.ShouldPanicWithMessage("Plugin API version is inconsistent across segments") 265 266 _ = subject.CheckPluginExistsOnAllHosts(testCluster) 267 }) 268 }) 269 }) 270 Describe("UsesEncryption", func() { 271 It("returns false when there is no encryption in config", func() { 272 Expect(subject.UsesEncryption()).To(BeFalse()) 273 }) 274 It("returns true when there is local encryption in config", func() { 275 subject.Options["password_encryption"] = "on" 276 Expect(subject.UsesEncryption()).To(BeTrue()) 277 }) 278 It("returns true when there is remote encryption in config", func() { 279 subject.Options["replication"] = "on" 280 subject.Options["remote_password_encryption"] = "on" 281 Expect(subject.UsesEncryption()).To(BeTrue()) 282 }) 283 }) 284 Describe("GetSecretKey", func() { 285 It("returns a secret key when one exists for the given name", func() { 286 mdd := testCluster.GetDirForContent(-1) 287 _ = os.MkdirAll(mdd, 0777) 288 secretFilePath := filepath.Join(mdd, utils.SecretKeyFile) 289 err := ioutil.WriteFile(secretFilePath, []byte(`gpbackup_fake_plugin: 0123456789`), 0777) 290 Expect(err).To(Not(HaveOccurred())) 291 292 key, err := utils.GetSecretKey("gpbackup_fake_plugin", mdd) 293 294 Expect(err).To(Not(HaveOccurred())) 295 Expect(key).To(Equal("0123456789")) 296 }) 297 It("returns an error when no encrypt file exists for the given name", func() { 298 mdd := testCluster.GetDirForContent(-1) 299 300 pluginName := "gpbackup_fake_plugin" 301 _, err := utils.GetSecretKey(pluginName, mdd) 302 303 Expect(err).To(HaveOccurred()) 304 Expect(err.Error()).To(Equal(fmt.Sprintf("Cannot find encryption key for plugin %s. Please re-encrypt password(s) so that key becomes available.", pluginName))) 305 }) 306 It("returns an error when no key exists for the given name", func() { 307 mdd := testCluster.GetDirForContent(-1) 308 _ = os.MkdirAll(mdd, 0777) 309 secretFilePath := filepath.Join(mdd, utils.SecretKeyFile) 310 err := ioutil.WriteFile(secretFilePath, []byte(""), 0777) 311 Expect(err).To(Not(HaveOccurred())) 312 313 pluginName := "gpbackup_fake_plugin" 314 _, err = utils.GetSecretKey(pluginName, mdd) 315 316 Expect(err).To(HaveOccurred()) 317 Expect(err.Error()).To(Equal(fmt.Sprintf("Cannot find encryption key for plugin %s. Please re-encrypt password(s) so that key becomes available.", pluginName))) 318 }) 319 It("returns an error when encrypt file cannot be parsed", func() { 320 mdd := testCluster.GetDirForContent(-1) 321 _ = os.MkdirAll(mdd, 0777) 322 secretFilePath := filepath.Join(mdd, utils.SecretKeyFile) 323 err := ioutil.WriteFile(secretFilePath, []byte("improperlyFormattedYaml"), 0777) 324 Expect(err).To(Not(HaveOccurred())) 325 326 pluginName := "gpbackup_fake_plugin" 327 _, err = utils.GetSecretKey(pluginName, mdd) 328 329 Expect(err).To(HaveOccurred()) 330 Expect(err.Error()).To(Equal(fmt.Sprintf("Cannot find encryption key for plugin %s. Please re-encrypt password(s) so that key becomes available.", pluginName))) 331 }) 332 }) 333 Describe("DeleteConfigFileOnSegments", func() { 334 When("config has encryption", func() { 335 It("sends the correct cluster command to delete config file", func() { 336 subject.Options["password_encryption"] = "on" 337 338 subject.DeletePluginConfigWhenEncrypting(testCluster) 339 340 Expect(executor.NumRemoteExecutions).To(Equal(1)) 341 cc := executor.ClusterCommands[0] 342 Expect(len(cc)).To(Equal(3)) 343 Expect(cc[0].Content).To(Equal(-1)) 344 Expect(cc[0].CommandString).To(ContainSubstring("rm -f /tmp/my_plugin_config.yaml")) 345 Expect(cc[1].Content).To(Equal(0)) 346 Expect(cc[1].CommandString).To(ContainSubstring("rm -f /tmp/my_plugin_config.yaml")) 347 Expect(cc[2].Content).To(Equal(1)) 348 Expect(cc[2].CommandString).To(ContainSubstring("rm -f /tmp/my_plugin_config.yaml")) 349 }) 350 }) 351 When("config does not have encryption", func() { 352 It("does not send a cluster command to delete config file", func() { 353 subject.DeletePluginConfigWhenEncrypting(testCluster) 354 355 Expect(executor.NumLocalExecutions).To(Equal(0)) 356 }) 357 }) 358 }) 359 Describe("GetPluginName", func() { 360 It("make the correct plugin call, parses out plugin name correctly, and returns it", func() { 361 executor.LocalOutput = "gpbackup_fake_plugin version 1.0.1+dev.28.g00c877e" 362 pluginName, err := subject.GetPluginName(testCluster) 363 364 Expect(err).To(Not(HaveOccurred())) 365 Expect(executor.LocalCommands[0]).To(Equal("/a/b/myPlugin --version")) 366 Expect(pluginName).To(Equal("gpbackup_fake_plugin")) 367 }) 368 It("encountered an error running plugin command", func() { 369 executor.LocalError = errors.New("error executing plugin") 370 pluginName, err := subject.GetPluginName(testCluster) 371 372 Expect(pluginName).To(Equal("")) 373 Expect(err).To(HaveOccurred()) 374 Expect(err.Error()).To(Equal("ERROR: Failed to get plugin name. Failed with error: error executing plugin")) 375 }) 376 It("did not recieve expected information from plugin", func() { 377 executor.LocalOutput = "bad output" 378 pluginName, err := subject.GetPluginName(testCluster) 379 380 Expect(pluginName).To(Equal("")) 381 Expect(err).To(HaveOccurred()) 382 Expect(err.Error()).To(Equal("Unexpected plugin version format: \"bad output\"\nExpected: \"[plugin_name] version [git_version]\"")) 383 }) 384 }) 385 Describe("ReadPluginConfig", func() { 386 It("returns an error if executablepath is not specified", func() { 387 operating.System.ReadFile = func(string) ([]byte, error) { 388 return []byte(`options: 389 hostname: "myhostname"`), nil 390 } 391 392 _, err := utils.ReadPluginConfig("myconfigpath") 393 Expect(err).To(HaveOccurred()) 394 Expect(err.Error()).To(Equal("executablepath is required in config file")) 395 }) 396 It("returns an error if additional fields are present on the root level", func() { 397 operating.System.ReadFile = func(string) ([]byte, error) { 398 return []byte(`executablepath: "/usr/local/gpdb/bin/gpbackup_ddboost_plugin" 399 options: 400 hostname: "myhostname"`), nil 401 } 402 403 _, err := utils.ReadPluginConfig("myconfigpath") 404 Expect(err).To(HaveOccurred()) 405 Expect(err.Error()).To(Equal("plugin config file is formatted incorrectly")) 406 }) 407 }) 408 })