github.com/argoproj/argo-cd/v3@v3.2.1/cmpserver/plugin/plugin_test.go (about) 1 package plugin 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "path/filepath" 11 "testing" 12 "time" 13 14 "github.com/golang/protobuf/ptypes/empty" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 "google.golang.org/grpc/metadata" 18 "gopkg.in/yaml.v2" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 21 "github.com/argoproj/argo-cd/v3/cmpserver/apiclient" 22 repoclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 23 "github.com/argoproj/argo-cd/v3/test" 24 "github.com/argoproj/argo-cd/v3/util/cmp" 25 "github.com/argoproj/argo-cd/v3/util/tgzstream" 26 ) 27 28 func newService(configFilePath string) (*Service, error) { 29 config, err := ReadPluginConfig(configFilePath) 30 if err != nil { 31 return nil, err 32 } 33 34 initConstants := CMPServerInitConstants{ 35 PluginConfig: *config, 36 } 37 38 service := &Service{ 39 initConstants: initConstants, 40 } 41 return service, nil 42 } 43 44 func (s *Service) WithGenerateCommand(command Command) *Service { 45 s.initConstants.PluginConfig.Spec.Generate = command 46 return s 47 } 48 49 type pluginOpt func(*CMPServerInitConstants) 50 51 func withDiscover(d Discover) pluginOpt { 52 return func(cic *CMPServerInitConstants) { 53 cic.PluginConfig.Spec.Discover = d 54 } 55 } 56 57 func buildPluginConfig(opts ...pluginOpt) *CMPServerInitConstants { 58 cic := &CMPServerInitConstants{ 59 PluginConfig: PluginConfig{ 60 TypeMeta: metav1.TypeMeta{ 61 Kind: "ConfigManagementPlugin", 62 APIVersion: "argoproj.io/v1alpha1", 63 }, 64 Metadata: metav1.ObjectMeta{ 65 Name: "some-plugin", 66 }, 67 Spec: PluginConfigSpec{ 68 Version: "v1.0", 69 }, 70 }, 71 } 72 for _, opt := range opts { 73 opt(cic) 74 } 75 return cic 76 } 77 78 func TestMatchRepository(t *testing.T) { 79 type fixture struct { 80 service *Service 81 path string 82 env []*apiclient.EnvEntry 83 } 84 setup := func(t *testing.T, opts ...pluginOpt) *fixture { 85 t.Helper() 86 cic := buildPluginConfig(opts...) 87 path := filepath.Join(test.GetTestDir(t), "testdata", "kustomize") 88 s := NewService(*cic) 89 return &fixture{ 90 service: s, 91 path: path, 92 env: []*apiclient.EnvEntry{{Name: "ENV_VAR", Value: "1"}}, 93 } 94 } 95 t.Run("will match plugin by filename", func(t *testing.T) { 96 // given 97 d := Discover{ 98 FileName: "kustomization.yaml", 99 } 100 f := setup(t, withDiscover(d)) 101 102 // when 103 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 104 105 // then 106 require.NoError(t, err) 107 assert.True(t, match) 108 assert.True(t, discovery) 109 }) 110 t.Run("will not match plugin by filename if file not found", func(t *testing.T) { 111 // given 112 d := Discover{ 113 FileName: "not_found.yaml", 114 } 115 f := setup(t, withDiscover(d)) 116 117 // when 118 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 119 120 // then 121 require.NoError(t, err) 122 assert.False(t, match) 123 assert.True(t, discovery) 124 }) 125 t.Run("will not match a pattern with a syntax error", func(t *testing.T) { 126 // given 127 d := Discover{ 128 FileName: "[", 129 } 130 f := setup(t, withDiscover(d)) 131 132 // when 133 _, _, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 134 135 // then 136 require.ErrorContains(t, err, "syntax error") 137 }) 138 t.Run("will match plugin by glob", func(t *testing.T) { 139 // given 140 d := Discover{ 141 Find: Find{ 142 Glob: "**/*/plugin.yaml", 143 }, 144 } 145 f := setup(t, withDiscover(d)) 146 147 // when 148 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 149 150 // then 151 require.NoError(t, err) 152 assert.True(t, match) 153 assert.True(t, discovery) 154 }) 155 t.Run("will not match plugin by glob if not found", func(t *testing.T) { 156 // given 157 d := Discover{ 158 Find: Find{ 159 Glob: "**/*/not_found.yaml", 160 }, 161 } 162 f := setup(t, withDiscover(d)) 163 164 // when 165 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 166 167 // then 168 require.NoError(t, err) 169 assert.False(t, match) 170 assert.True(t, discovery) 171 }) 172 t.Run("will throw an error for a bad pattern", func(t *testing.T) { 173 // given 174 d := Discover{ 175 Find: Find{ 176 Glob: "does-not-exist", 177 }, 178 } 179 f := setup(t, withDiscover(d)) 180 181 // when 182 _, _, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 183 184 // then 185 require.ErrorContains(t, err, "error finding glob match for pattern") 186 }) 187 t.Run("will match plugin by command when returns any output", func(t *testing.T) { 188 // given 189 d := Discover{ 190 Find: Find{ 191 Command: Command{ 192 Command: []string{"echo", "test"}, 193 }, 194 }, 195 } 196 f := setup(t, withDiscover(d)) 197 198 // when 199 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 200 201 // then 202 require.NoError(t, err) 203 assert.True(t, match) 204 assert.True(t, discovery) 205 }) 206 t.Run("will not match plugin by command when returns no output", func(t *testing.T) { 207 // given 208 d := Discover{ 209 Find: Find{ 210 Command: Command{ 211 Command: []string{"echo"}, 212 }, 213 }, 214 } 215 f := setup(t, withDiscover(d)) 216 217 // when 218 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 219 // then 220 require.NoError(t, err) 221 assert.False(t, match) 222 assert.True(t, discovery) 223 }) 224 t.Run("will match plugin because env var defined", func(t *testing.T) { 225 // given 226 d := Discover{ 227 Find: Find{ 228 Command: Command{ 229 Command: []string{"sh", "-c", "echo -n $ENV_VAR"}, 230 }, 231 }, 232 } 233 f := setup(t, withDiscover(d)) 234 235 // when 236 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 237 238 // then 239 require.NoError(t, err) 240 assert.True(t, match) 241 assert.True(t, discovery) 242 }) 243 t.Run("will not match plugin because no env var defined", func(t *testing.T) { 244 // given 245 d := Discover{ 246 Find: Find{ 247 Command: Command{ 248 // Use printf instead of echo since OSX prints the "-n" when there's no additional arg. 249 Command: []string{"sh", "-c", `printf "%s" "$ENV_NO_VAR"`}, 250 }, 251 }, 252 } 253 f := setup(t, withDiscover(d)) 254 255 // when 256 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 257 258 // then 259 require.NoError(t, err) 260 assert.False(t, match) 261 assert.True(t, discovery) 262 }) 263 t.Run("will not match plugin by command when command fails", func(t *testing.T) { 264 // given 265 d := Discover{ 266 Find: Find{ 267 Command: Command{ 268 Command: []string{"cat", "nil"}, 269 }, 270 }, 271 } 272 f := setup(t, withDiscover(d)) 273 274 // when 275 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 276 277 // then 278 require.Error(t, err) 279 assert.False(t, match) 280 assert.True(t, discovery) 281 }) 282 t.Run("will not match plugin as discovery is not set", func(t *testing.T) { 283 // given 284 d := Discover{} 285 f := setup(t, withDiscover(d)) 286 287 // when 288 match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".") 289 290 // then 291 require.NoError(t, err) 292 assert.False(t, match) 293 assert.False(t, discovery) 294 }) 295 } 296 297 func Test_Negative_ConfigFile_DoesnotExist(t *testing.T) { 298 configFilePath := "./testdata/kustomize-neg/config" 299 service, err := newService(configFilePath) 300 require.Error(t, err) 301 require.Nil(t, service) 302 } 303 304 func TestGenerateManifest(t *testing.T) { 305 configFilePath := "./testdata/kustomize/config" 306 307 t.Run("successful generate", func(t *testing.T) { 308 service, err := newService(configFilePath) 309 require.NoError(t, err) 310 311 res1, err := service.generateManifest(t.Context(), "testdata/kustomize", nil) 312 require.NoError(t, err) 313 require.NotNil(t, res1) 314 315 expectedOutput := "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}" 316 if res1 != nil { 317 require.Equal(t, expectedOutput, res1.Manifests[0]) 318 } 319 }) 320 t.Run("bad generate command", func(t *testing.T) { 321 service, err := newService(configFilePath) 322 require.NoError(t, err) 323 service.WithGenerateCommand(Command{Command: []string{"bad-command"}}) 324 325 res, err := service.generateManifest(t.Context(), "testdata/kustomize", nil) 326 require.ErrorContains(t, err, "executable file not found") 327 assert.Nil(t, res.Manifests) 328 }) 329 t.Run("bad yaml output", func(t *testing.T) { 330 service, err := newService(configFilePath) 331 require.NoError(t, err) 332 service.WithGenerateCommand(Command{Command: []string{"echo", "invalid yaml: }"}}) 333 334 res, err := service.generateManifest(t.Context(), "testdata/kustomize", nil) 335 require.ErrorContains(t, err, "failed to unmarshal manifest") 336 assert.Nil(t, res.Manifests) 337 }) 338 } 339 340 func TestGenerateManifest_deadline_exceeded(t *testing.T) { 341 configFilePath := "./testdata/kustomize/config" 342 service, err := newService(configFilePath) 343 require.NoError(t, err) 344 345 expiredCtx, cancel := context.WithTimeout(t.Context(), time.Second*0) 346 defer cancel() 347 _, err = service.generateManifest(expiredCtx, "", nil) 348 require.ErrorContains(t, err, "context deadline exceeded") 349 } 350 351 // TestRunCommandContextTimeout makes sure the command dies at timeout rather than sleeping past the timeout. 352 func TestRunCommandContextTimeout(t *testing.T) { 353 ctx, cancel := context.WithTimeout(t.Context(), 990*time.Millisecond) 354 defer cancel() 355 // Use a subshell so there's a child command. 356 command := Command{ 357 Command: []string{"sh", "-c"}, 358 Args: []string{"sleep 5"}, 359 } 360 before := time.Now() 361 _, err := runCommand(ctx, command, "", []string{}) 362 after := time.Now() 363 require.Error(t, err) // The command should time out, causing an error. 364 assert.Less(t, after.Sub(before), 1*time.Second) 365 } 366 367 func TestRunCommandEmptyCommand(t *testing.T) { 368 _, err := runCommand(t.Context(), Command{}, "", nil) 369 require.ErrorContains(t, err, "Command is empty") 370 } 371 372 // TestRunCommandContextTimeoutWithCleanup makes sure that the process is given enough time to cleanup before sending SIGKILL. 373 func TestRunCommandContextTimeoutWithCleanup(t *testing.T) { 374 ctx, cancel := context.WithTimeout(t.Context(), 900*time.Millisecond) 375 defer cancel() 376 377 // Use a subshell so there's a child command. 378 // This command sleeps for 4 seconds which is currently less than the 5 second delay between SIGTERM and SIGKILL signal and then exits successfully. 379 command := Command{ 380 Command: []string{"sh", "-c"}, 381 Args: []string{`(trap 'echo "cleanup completed"; exit' TERM; sleep 4)`}, 382 } 383 384 before := time.Now() 385 output, err := runCommand(ctx, command, "", []string{}) 386 after := time.Now() 387 388 require.Error(t, err) // The command should time out, causing an error. 389 assert.Less(t, after.Sub(before), 1*time.Second) 390 // The command should still have completed the cleanup after termination. 391 assert.Contains(t, output, "cleanup completed") 392 } 393 394 func Test_getParametersAnnouncement_empty_command(t *testing.T) { 395 staticYAML := ` 396 - name: static-a 397 - name: static-b 398 ` 399 static := &[]*repoclient.ParameterAnnouncement{} 400 err := yaml.Unmarshal([]byte(staticYAML), static) 401 require.NoError(t, err) 402 command := Command{ 403 Command: []string{"echo"}, 404 Args: []string{`[]`}, 405 } 406 res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{}) 407 require.NoError(t, err) 408 assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements) 409 } 410 411 func Test_getParametersAnnouncement_no_command(t *testing.T) { 412 staticYAML := ` 413 - name: static-a 414 - name: static-b 415 ` 416 static := &[]*repoclient.ParameterAnnouncement{} 417 err := yaml.Unmarshal([]byte(staticYAML), static) 418 require.NoError(t, err) 419 command := Command{} 420 res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{}) 421 require.NoError(t, err) 422 assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements) 423 } 424 425 func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) { 426 staticYAML := ` 427 - name: static-a 428 - name: static-b 429 ` 430 static := &[]*repoclient.ParameterAnnouncement{} 431 err := yaml.Unmarshal([]byte(staticYAML), static) 432 require.NoError(t, err) 433 command := Command{ 434 Command: []string{"echo"}, 435 Args: []string{`[{"name": "dynamic-a"}, {"name": "dynamic-b"}]`}, 436 } 437 res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{}) 438 require.NoError(t, err) 439 expected := []*repoclient.ParameterAnnouncement{ 440 {Name: "dynamic-a"}, 441 {Name: "dynamic-b"}, 442 {Name: "static-a"}, 443 {Name: "static-b"}, 444 } 445 assert.Equal(t, expected, res.ParameterAnnouncements) 446 } 447 448 func Test_getParametersAnnouncement_invalid_json(t *testing.T) { 449 command := Command{ 450 Command: []string{"echo"}, 451 Args: []string{`[`}, 452 } 453 _, err := getParametersAnnouncement(t.Context(), "", []*repoclient.ParameterAnnouncement{}, command, []*apiclient.EnvEntry{}) 454 assert.ErrorContains(t, err, "unexpected end of JSON input") 455 } 456 457 func Test_getParametersAnnouncement_bad_command(t *testing.T) { 458 command := Command{ 459 Command: []string{"exit"}, 460 Args: []string{"1"}, 461 } 462 _, err := getParametersAnnouncement(t.Context(), "", []*repoclient.ParameterAnnouncement{}, command, []*apiclient.EnvEntry{}) 463 assert.ErrorContains(t, err, "error executing dynamic parameter output command") 464 } 465 466 func Test_getTempDirMustCleanup(t *testing.T) { 467 tempDir := t.TempDir() 468 469 // Induce a directory create error to verify error handling. 470 err := os.Chmod(tempDir, 0o000) 471 require.NoError(t, err) 472 _, _, err = getTempDirMustCleanup(path.Join(tempDir, "test")) 473 require.ErrorContains(t, err, "error creating temp dir") 474 475 err = os.Chmod(tempDir, 0o700) 476 require.NoError(t, err) 477 workDir, cleanup, err := getTempDirMustCleanup(tempDir) 478 require.NoError(t, err) 479 require.DirExists(t, workDir) 480 cleanup() 481 assert.NoDirExists(t, workDir) 482 } 483 484 func TestService_Init(t *testing.T) { 485 // Set up a base directory containing a test directory and a test file. 486 tempDir := t.TempDir() 487 workDir := path.Join(tempDir, "workDir") 488 err := os.MkdirAll(workDir, 0o700) 489 require.NoError(t, err) 490 testfile := path.Join(workDir, "testfile") 491 file, err := os.Create(testfile) 492 require.NoError(t, err) 493 err = file.Close() 494 require.NoError(t, err) 495 496 // Make the base directory read-only so Init's cleanup fails. 497 err = os.Chmod(tempDir, 0o000) 498 require.NoError(t, err) 499 s := NewService(CMPServerInitConstants{PluginConfig: PluginConfig{}}) 500 err = s.Init(workDir) 501 require.ErrorContains(t, err, "error removing workdir", "Init must throw an error if it can't remove the work directory") 502 503 // Make the base directory writable so Init's cleanup succeeds. 504 err = os.Chmod(tempDir, 0o700) 505 require.NoError(t, err) 506 err = s.Init(workDir) 507 require.NoError(t, err) 508 assert.DirExists(t, workDir) 509 assert.NoFileExists(t, testfile) 510 } 511 512 func TestEnviron(t *testing.T) { 513 t.Run("empty environ", func(t *testing.T) { 514 env := environ([]*apiclient.EnvEntry{}) 515 assert.Nil(t, env) 516 }) 517 t.Run("env vars with empty names", func(t *testing.T) { 518 env := environ([]*apiclient.EnvEntry{ 519 {Value: "test"}, 520 {Name: "test"}, 521 }) 522 assert.Equal(t, []string{"test="}, env) 523 }) 524 t.Run("proper env vars", func(t *testing.T) { 525 env := environ([]*apiclient.EnvEntry{ 526 {Name: "name1", Value: "value1"}, 527 {Name: "name2", Value: "value2"}, 528 {Name: "name3", Value: ""}, 529 }) 530 assert.Equal(t, []string{"name1=value1", "name2=value2", "name3="}, env) 531 }) 532 } 533 534 func TestIsDiscoveryConfigured(t *testing.T) { 535 type fixture struct { 536 service *Service 537 } 538 setup := func(t *testing.T, opts ...pluginOpt) *fixture { 539 t.Helper() 540 cic := buildPluginConfig(opts...) 541 s := NewService(*cic) 542 return &fixture{ 543 service: s, 544 } 545 } 546 t.Run("discovery is enabled when is configured by FileName", func(t *testing.T) { 547 // given 548 d := Discover{ 549 FileName: "kustomization.yaml", 550 } 551 f := setup(t, withDiscover(d)) 552 553 // when 554 isDiscoveryConfigured := f.service.isDiscoveryConfigured() 555 556 // then 557 assert.True(t, isDiscoveryConfigured) 558 }) 559 t.Run("discovery is enabled when is configured by Glob", func(t *testing.T) { 560 // given 561 d := Discover{ 562 Find: Find{ 563 Glob: "**/*/plugin.yaml", 564 }, 565 } 566 f := setup(t, withDiscover(d)) 567 568 // when 569 isDiscoveryConfigured := f.service.isDiscoveryConfigured() 570 571 // then 572 assert.True(t, isDiscoveryConfigured) 573 }) 574 t.Run("discovery is enabled when is configured by Command", func(t *testing.T) { 575 // given 576 d := Discover{ 577 Find: Find{ 578 Command: Command{ 579 Command: []string{"echo", "test"}, 580 }, 581 }, 582 } 583 f := setup(t, withDiscover(d)) 584 585 // when 586 isDiscoveryConfigured := f.service.isDiscoveryConfigured() 587 588 // then 589 assert.True(t, isDiscoveryConfigured) 590 }) 591 t.Run("discovery is disabled when discover is not configured", func(t *testing.T) { 592 // given 593 d := Discover{} 594 f := setup(t, withDiscover(d)) 595 596 // when 597 isDiscoveryConfigured := f.service.isDiscoveryConfigured() 598 599 // then 600 assert.False(t, isDiscoveryConfigured) 601 }) 602 } 603 604 type MockGenerateManifestStream struct { 605 metadataSent bool 606 fileSent bool 607 metadataRequest *apiclient.AppStreamRequest 608 fileRequest *apiclient.AppStreamRequest 609 response *apiclient.ManifestResponse 610 } 611 612 func NewMockGenerateManifestStream(repoPath, appPath string, env []string) (*MockGenerateManifestStream, error) { 613 tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil) 614 if err != nil { 615 return nil, err 616 } 617 defer tgzstream.CloseAndDelete(tgz) 618 619 tgzBuffer := bytes.NewBuffer(nil) 620 _, err = io.Copy(tgzBuffer, tgz) 621 if err != nil { 622 return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err) 623 } 624 625 return &MockGenerateManifestStream{ 626 metadataRequest: mr, 627 fileRequest: cmp.AppFileRequest(tgzBuffer.Bytes()), 628 }, nil 629 } 630 631 func (m *MockGenerateManifestStream) SendAndClose(response *apiclient.ManifestResponse) error { 632 m.response = response 633 return nil 634 } 635 636 func (m *MockGenerateManifestStream) Recv() (*apiclient.AppStreamRequest, error) { 637 if !m.metadataSent { 638 m.metadataSent = true 639 return m.metadataRequest, nil 640 } 641 642 if !m.fileSent { 643 m.fileSent = true 644 return m.fileRequest, nil 645 } 646 return nil, io.EOF 647 } 648 649 func (m *MockGenerateManifestStream) Context() context.Context { 650 return context.Background() 651 } 652 653 func TestService_GenerateManifest(t *testing.T) { 654 configFilePath := "./testdata/kustomize/config" 655 service, err := newService(configFilePath) 656 require.NoError(t, err) 657 658 t.Run("successful generate", func(t *testing.T) { 659 s, err := NewMockGenerateManifestStream("./testdata/kustomize", "./testdata/kustomize", nil) 660 require.NoError(t, err) 661 err = service.generateManifestGeneric(s) 662 require.NoError(t, err) 663 require.NotNil(t, s.response) 664 assert.Equal(t, []string{"{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, s.response.Manifests) 665 }) 666 667 t.Run("out-of-bounds app path", func(t *testing.T) { 668 s, err := NewMockGenerateManifestStream("./testdata/kustomize", "./testdata/kustomize", nil) 669 require.NoError(t, err) 670 // set a malicious app path on the metadata 671 s.metadataRequest.Request.(*apiclient.AppStreamRequest_Metadata).Metadata.AppRelPath = "../out-of-bounds" 672 err = service.generateManifestGeneric(s) 673 require.ErrorContains(t, err, "illegal appPath") 674 assert.Nil(t, s.response) 675 }) 676 } 677 678 type MockMatchRepositoryStream struct { 679 metadataSent bool 680 fileSent bool 681 metadataRequest *apiclient.AppStreamRequest 682 fileRequest *apiclient.AppStreamRequest 683 response *apiclient.RepositoryResponse 684 } 685 686 func NewMockMatchRepositoryStream(repoPath, appPath string, env []string) (*MockMatchRepositoryStream, error) { 687 tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil) 688 if err != nil { 689 return nil, err 690 } 691 defer tgzstream.CloseAndDelete(tgz) 692 693 tgzBuffer := bytes.NewBuffer(nil) 694 _, err = io.Copy(tgzBuffer, tgz) 695 if err != nil { 696 return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err) 697 } 698 699 return &MockMatchRepositoryStream{ 700 metadataRequest: mr, 701 fileRequest: cmp.AppFileRequest(tgzBuffer.Bytes()), 702 }, nil 703 } 704 705 func (m *MockMatchRepositoryStream) SendAndClose(response *apiclient.RepositoryResponse) error { 706 m.response = response 707 return nil 708 } 709 710 func (m *MockMatchRepositoryStream) Recv() (*apiclient.AppStreamRequest, error) { 711 if !m.metadataSent { 712 m.metadataSent = true 713 return m.metadataRequest, nil 714 } 715 716 if !m.fileSent { 717 m.fileSent = true 718 return m.fileRequest, nil 719 } 720 return nil, io.EOF 721 } 722 723 func (m *MockMatchRepositoryStream) Context() context.Context { 724 return context.Background() 725 } 726 727 func TestService_MatchRepository(t *testing.T) { 728 configFilePath := "./testdata/kustomize/config" 729 service, err := newService(configFilePath) 730 require.NoError(t, err) 731 732 t.Run("supported app", func(t *testing.T) { 733 s, err := NewMockMatchRepositoryStream("./testdata/kustomize", "./testdata/kustomize", nil) 734 require.NoError(t, err) 735 err = service.matchRepositoryGeneric(s) 736 require.NoError(t, err) 737 require.NotNil(t, s.response) 738 assert.True(t, s.response.IsSupported) 739 }) 740 741 t.Run("unsupported app", func(t *testing.T) { 742 s, err := NewMockMatchRepositoryStream("./testdata/ksonnet", "./testdata/ksonnet", nil) 743 require.NoError(t, err) 744 err = service.matchRepositoryGeneric(s) 745 require.NoError(t, err) 746 require.NotNil(t, s.response) 747 assert.False(t, s.response.IsSupported) 748 }) 749 } 750 751 type MockParametersAnnouncementStream struct { 752 metadataSent bool 753 fileSent bool 754 metadataRequest *apiclient.AppStreamRequest 755 fileRequest *apiclient.AppStreamRequest 756 response *apiclient.ParametersAnnouncementResponse 757 } 758 759 func NewMockParametersAnnouncementStream(repoPath, appPath string, env []string) (*MockParametersAnnouncementStream, error) { 760 tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil) 761 if err != nil { 762 return nil, err 763 } 764 defer tgzstream.CloseAndDelete(tgz) 765 766 tgzBuffer := bytes.NewBuffer(nil) 767 _, err = io.Copy(tgzBuffer, tgz) 768 if err != nil { 769 return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err) 770 } 771 772 return &MockParametersAnnouncementStream{ 773 metadataRequest: mr, 774 fileRequest: cmp.AppFileRequest(tgzBuffer.Bytes()), 775 }, nil 776 } 777 778 func (m *MockParametersAnnouncementStream) SendAndClose(response *apiclient.ParametersAnnouncementResponse) error { 779 m.response = response 780 return nil 781 } 782 783 func (m *MockParametersAnnouncementStream) Recv() (*apiclient.AppStreamRequest, error) { 784 if !m.metadataSent { 785 m.metadataSent = true 786 return m.metadataRequest, nil 787 } 788 789 if !m.fileSent { 790 m.fileSent = true 791 return m.fileRequest, nil 792 } 793 return nil, io.EOF 794 } 795 796 func (m *MockParametersAnnouncementStream) SetHeader(metadata.MD) error { 797 return nil 798 } 799 800 func (m *MockParametersAnnouncementStream) SendHeader(metadata.MD) error { 801 return nil 802 } 803 804 func (m *MockParametersAnnouncementStream) SetTrailer(metadata.MD) {} 805 806 func (m *MockParametersAnnouncementStream) Context() context.Context { 807 return context.Background() 808 } 809 810 func (m *MockParametersAnnouncementStream) SendMsg(any) error { 811 return nil 812 } 813 814 func (m *MockParametersAnnouncementStream) RecvMsg(any) error { 815 return nil 816 } 817 818 func TestService_GetParametersAnnouncement(t *testing.T) { 819 configFilePath := "./testdata/kustomize/config" 820 service, err := newService(configFilePath) 821 require.NoError(t, err) 822 823 t.Run("successful response", func(t *testing.T) { 824 s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"MUST_BE_SET=yep"}) 825 require.NoError(t, err) 826 err = service.GetParametersAnnouncement(s) 827 require.NoError(t, err) 828 require.NotNil(t, s.response) 829 require.Len(t, s.response.ParameterAnnouncements, 2) 830 assert.Equal(t, repoclient.ParameterAnnouncement{Name: "dynamic-test-param", String_: "yep"}, *s.response.ParameterAnnouncements[0]) 831 assert.Equal(t, repoclient.ParameterAnnouncement{Name: "test-param", String_: "test-value"}, *s.response.ParameterAnnouncements[1]) 832 }) 833 t.Run("out of bounds app", func(t *testing.T) { 834 s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"MUST_BE_SET=yep"}) 835 require.NoError(t, err) 836 // set a malicious app path on the metadata 837 s.metadataRequest.Request.(*apiclient.AppStreamRequest_Metadata).Metadata.AppRelPath = "../out-of-bounds" 838 err = service.GetParametersAnnouncement(s) 839 require.ErrorContains(t, err, "illegal appPath") 840 require.Nil(t, s.response) 841 }) 842 t.Run("fails when script fails", func(t *testing.T) { 843 s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"WRONG_ENV_VAR=oops"}) 844 require.NoError(t, err) 845 err = service.GetParametersAnnouncement(s) 846 require.ErrorContains(t, err, "error executing dynamic parameter output command") 847 require.Nil(t, s.response) 848 }) 849 } 850 851 func TestService_CheckPluginConfiguration(t *testing.T) { 852 type fixture struct { 853 service *Service 854 } 855 setup := func(t *testing.T, opts ...pluginOpt) *fixture { 856 t.Helper() 857 cic := buildPluginConfig(opts...) 858 s := NewService(*cic) 859 return &fixture{ 860 service: s, 861 } 862 } 863 t.Run("discovery is enabled when is configured", func(t *testing.T) { 864 // given 865 d := Discover{ 866 FileName: "kustomization.yaml", 867 } 868 f := setup(t, withDiscover(d)) 869 870 // when 871 resp, err := f.service.CheckPluginConfiguration(t.Context(), &empty.Empty{}) 872 873 // then 874 require.NoError(t, err) 875 assert.True(t, resp.IsDiscoveryConfigured) 876 }) 877 878 t.Run("discovery is disabled when is not configured", func(t *testing.T) { 879 // given 880 d := Discover{} 881 f := setup(t, withDiscover(d)) 882 883 // when 884 resp, err := f.service.CheckPluginConfiguration(t.Context(), &empty.Empty{}) 885 886 // then 887 require.NoError(t, err) 888 assert.False(t, resp.IsDiscoveryConfigured) 889 }) 890 }