github.com/oam-dev/kubevela@v1.9.11/pkg/addon/addon_test.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package addon 18 19 import ( 20 "context" 21 "encoding/json" 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "net/http" 26 "net/http/httptest" 27 "os" 28 "path" 29 "path/filepath" 30 "strings" 31 "testing" 32 33 "github.com/crossplane/crossplane-runtime/pkg/test" 34 "github.com/google/go-github/v32/github" 35 "github.com/stretchr/testify/assert" 36 "go.uber.org/multierr" 37 appsv1 "k8s.io/api/apps/v1" 38 corev1 "k8s.io/api/core/v1" 39 kerrors "k8s.io/apimachinery/pkg/api/errors" 40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 "k8s.io/apimachinery/pkg/runtime/schema" 42 "sigs.k8s.io/controller-runtime/pkg/client" 43 "sigs.k8s.io/controller-runtime/pkg/client/fake" 44 45 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 46 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 47 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 48 "github.com/oam-dev/kubevela/apis/types" 49 "github.com/oam-dev/kubevela/pkg/oam" 50 addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" 51 version2 "github.com/oam-dev/kubevela/version" 52 ) 53 54 var paths = []string{ 55 "example/metadata.yaml", 56 "example/readme.md", 57 "example/template.cue", 58 "example/definitions/helm.yaml", 59 "example/resources/configmap.cue", 60 "example/parameter.cue", 61 "example/resources/service/source-controller.yaml", 62 63 "example-legacy/metadata.yaml", 64 "example-legacy/readme.md", 65 "example-legacy/template.yaml", 66 "example-legacy/definitions/helm.yaml", 67 "example-legacy/resources/configmap.cue", 68 "example-legacy/resources/parameter.cue", 69 "example-legacy/resources/service/source-controller.yaml", 70 71 "terraform/metadata.yaml", 72 "terraform-alibaba/metadata.yaml", 73 74 "test-error-addon/metadata.yaml", 75 "test-error-addon/resources/parameter.cue", 76 77 "test-disable-addon/metadata.yaml", 78 "test-disable-addon/definitions/compDef.yaml", 79 "test-disable-addon/definitions/traitDef.cue", 80 } 81 82 var ossHandler http.HandlerFunc = func(rw http.ResponseWriter, req *http.Request) { 83 queryPath := strings.TrimPrefix(req.URL.Path, "/") 84 85 if strings.Contains(req.URL.RawQuery, "prefix") { 86 prefix := req.URL.Query().Get("prefix") 87 res := ListBucketResult{ 88 Files: []File{}, 89 Count: 0, 90 } 91 for _, p := range paths { 92 if strings.HasPrefix(p, prefix) { 93 // Size 100 is for mock a file 94 res.Files = append(res.Files, File{Name: p, Size: 100}) 95 res.Count += 1 96 } 97 } 98 data, err := xml.Marshal(res) 99 if err != nil { 100 rw.Write([]byte(err.Error())) 101 } 102 rw.Write(data) 103 } else { 104 found := false 105 for _, p := range paths { 106 if queryPath == p { 107 file, err := os.ReadFile(path.Join("testdata", queryPath)) 108 if err != nil { 109 rw.Write([]byte(err.Error())) 110 } 111 found = true 112 rw.Write(file) 113 break 114 } 115 } 116 if !found { 117 rw.Write([]byte("not found")) 118 } 119 } 120 } 121 122 var helmHandler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) { 123 switch { 124 case strings.Contains(request.URL.Path, "index.yaml"): 125 files, err := os.ReadFile("./testdata/multiversion-helm-repo/index.yaml") 126 if err != nil { 127 _, _ = writer.Write([]byte(err.Error())) 128 } 129 writer.Write(files) 130 case strings.Contains(request.URL.Path, "fluxcd-1.0.0.tgz"): 131 files, err := os.ReadFile("./testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz") 132 if err != nil { 133 _, _ = writer.Write([]byte(err.Error())) 134 } 135 writer.Write(files) 136 case strings.Contains(request.URL.Path, "fluxcd-2.0.0.tgz"): 137 files, err := os.ReadFile("./testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz") 138 if err != nil { 139 _, _ = writer.Write([]byte(err.Error())) 140 } 141 writer.Write(files) 142 } 143 } 144 145 var ctx = context.Background() 146 147 func testReaderFunc(t *testing.T, reader AsyncReader) { 148 registryMeta, err := reader.ListAddonMeta() 149 assert.NoError(t, err) 150 151 testAddonName := "example" 152 var testAddonMeta SourceMeta 153 for _, m := range registryMeta { 154 if m.Name == testAddonName { 155 testAddonMeta = m 156 break 157 } 158 } 159 assert.NoError(t, err) 160 uiData, err := GetUIDataFromReader(reader, &testAddonMeta, UIMetaOptions) 161 assert.NoError(t, err) 162 assert.Equal(t, uiData.Name, testAddonName) 163 assert.True(t, uiData.Parameters != "") 164 assert.Equal(t, len(uiData.APISchema.Properties), 1) 165 assert.Equal(t, uiData.APISchema.Properties["example"].Value.Description, "the example field") 166 assert.True(t, len(uiData.Definitions) > 0) 167 168 testAddonName = "example-legacy" 169 for _, m := range registryMeta { 170 if m.Name == testAddonName { 171 testAddonMeta = m 172 break 173 } 174 } 175 assert.NoError(t, err) 176 uiData, err = GetUIDataFromReader(reader, &testAddonMeta, UIMetaOptions) 177 assert.NoError(t, err) 178 assert.Equal(t, uiData.Name, testAddonName) 179 assert.True(t, uiData.Parameters != "") 180 assert.True(t, len(uiData.Definitions) > 0) 181 182 // test get ui data 183 rName := "KubeVela" 184 uiDataList, err := ListAddonUIDataFromReader(reader, registryMeta, rName, UIMetaOptions) 185 fmt.Println(err.Error()) 186 assert.True(t, strings.Contains(err.Error(), "preference mark not allowed at this position")) 187 assert.Equal(t, 5, len(uiDataList)) 188 assert.Equal(t, uiDataList[0].RegistryName, rName) 189 190 // test get install package 191 installPkg, err := GetInstallPackageFromReader(reader, &testAddonMeta, uiData) 192 assert.NoError(t, err) 193 assert.NotNil(t, installPkg, "should get install package") 194 assert.Equal(t, len(installPkg.CUETemplates), 1) 195 } 196 197 func TestGetAddonData(t *testing.T) { 198 server := httptest.NewServer(ossHandler) 199 defer server.Close() 200 201 reader, err := NewAsyncReader(server.URL, "", "", "", "", ossType) 202 assert.NoError(t, err) 203 testReaderFunc(t, reader) 204 } 205 206 func TestRenderApp(t *testing.T) { 207 addon := baseAddon 208 app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{}) 209 assert.NoError(t, err, "render app fail") 210 // definition should not be rendered 211 assert.Equal(t, len(app.Spec.Components), 1) 212 } 213 214 func TestRenderAppWithNeedNamespace(t *testing.T) { 215 addon := baseAddon 216 addon.NeedNamespace = append(addon.NeedNamespace, types.DefaultKubeVelaNS, "test-ns2") 217 app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{}) 218 assert.NoError(t, err, "render app fail") 219 assert.Equal(t, len(app.Spec.Components), 2) 220 for _, c := range app.Spec.Components { 221 assert.NotEqual(t, types.DefaultKubeVelaNS+"-namespace", c.Name, "namespace vela-system should not be rendered") 222 } 223 } 224 225 func TestRenderDeploy2RuntimeAddon(t *testing.T) { 226 addonDeployToRuntime := baseAddon 227 addonDeployToRuntime.Meta.DeployTo = &DeployTo{ 228 DisableControlPlane: false, 229 RuntimeCluster: true, 230 } 231 defs, err := RenderDefinitions(&addonDeployToRuntime, nil) 232 assert.NoError(t, err) 233 assert.Equal(t, len(defs), 1) 234 def := defs[0] 235 assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1") 236 assert.Equal(t, def.GetKind(), "TraitDefinition") 237 238 app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{}) 239 assert.NoError(t, err) 240 policies := app.Spec.Policies 241 assert.True(t, len(policies) == 1) 242 assert.Equal(t, policies[0].Type, v1alpha1.TopologyPolicyType) 243 } 244 245 func TestRenderDefinitions(t *testing.T) { 246 addonDeployToRuntime := baseAddon 247 addonDeployToRuntime.Meta.DeployTo = &DeployTo{ 248 DisableControlPlane: false, 249 RuntimeCluster: false, 250 } 251 defs, err := RenderDefinitions(&addonDeployToRuntime, nil) 252 assert.NoError(t, err) 253 assert.Equal(t, len(defs), 1) 254 def := defs[0] 255 assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1") 256 assert.Equal(t, def.GetKind(), "TraitDefinition") 257 258 app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{}) 259 assert.NoError(t, err) 260 // addon which app work on no-runtime-cluster mode workflow is nil 261 assert.Nil(t, app.Spec.Workflow) 262 } 263 264 func TestRenderViews(t *testing.T) { 265 addonDeployToRuntime := viewAddon 266 addonDeployToRuntime.Meta.DeployTo = &DeployTo{ 267 DisableControlPlane: false, 268 RuntimeCluster: false, 269 } 270 views, err := RenderViews(&addonDeployToRuntime) 271 assert.NoError(t, err) 272 assert.Equal(t, len(views), 2) 273 274 view := views[0] 275 assert.Equal(t, view.GetKind(), "ConfigMap") 276 assert.Equal(t, view.GetAPIVersion(), "v1") 277 assert.Equal(t, view.GetNamespace(), types.DefaultKubeVelaNS) 278 assert.Equal(t, view.GetName(), "cloud-resource-view") 279 280 view = views[1] 281 assert.Equal(t, view.GetKind(), "ConfigMap") 282 assert.Equal(t, view.GetAPIVersion(), "v1") 283 assert.Equal(t, view.GetNamespace(), types.DefaultKubeVelaNS) 284 assert.Equal(t, view.GetName(), "pod-view") 285 286 } 287 288 func TestRenderK8sObjects(t *testing.T) { 289 addonMultiYaml := multiYamlAddon 290 addonMultiYaml.Meta.DeployTo = &DeployTo{ 291 DisableControlPlane: false, 292 RuntimeCluster: false, 293 } 294 295 app, _, err := RenderApp(ctx, &addonMultiYaml, nil, map[string]interface{}{}) 296 assert.NoError(t, err) 297 assert.Equal(t, len(app.Spec.Components), 1) 298 comp := app.Spec.Components[0] 299 assert.Equal(t, comp.Type, "k8s-objects") 300 } 301 302 func TestGetClusters(t *testing.T) { 303 // string array test 304 args := map[string]interface{}{ 305 types.ClustersArg: []string{ 306 "cluster1", "cluster2", 307 }, 308 } 309 clusters := getClusters(args) 310 assert.Equal(t, clusters, []string{ 311 "cluster1", "cluster2", 312 }) 313 // interface array test 314 args1 := map[string]interface{}{ 315 types.ClustersArg: []interface{}{ 316 "cluster3", "cluster4", 317 }, 318 } 319 clusters1 := getClusters(args1) 320 assert.Equal(t, clusters1, []string{ 321 "cluster3", "cluster4", 322 }) 323 // no cluster arg test 324 args2 := map[string]interface{}{ 325 "anyargkey": "anyargvalue", 326 } 327 clusters2 := getClusters(args2) 328 assert.Nil(t, clusters2) 329 // other type test 330 args3 := map[string]interface{}{ 331 types.ClustersArg: "cluster5", 332 } 333 clusters3 := getClusters(args3) 334 assert.Nil(t, clusters3) 335 } 336 337 func TestGetAddonStatus(t *testing.T) { 338 getFunc := test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object) error { 339 switch key.Name { 340 case "addon-disabled", "disabled": 341 return kerrors.NewNotFound(schema.GroupResource{Group: "apiVersion: core.oam.dev/v1beta1", Resource: "app"}, key.Name) 342 case "addon-suspend": 343 o := obj.(*v1beta1.Application) 344 app := &v1beta1.Application{} 345 app.Status.Workflow = &common.WorkflowStatus{ 346 Suspend: true, 347 } 348 *o = *app 349 case "addon-enabled": 350 o := obj.(*v1beta1.Application) 351 app := &v1beta1.Application{} 352 app.Status.Phase = common.ApplicationRunning 353 *o = *app 354 case "addon-disabling": 355 o := obj.(*v1beta1.Application) 356 app := &v1beta1.Application{} 357 app.Status.Phase = common.ApplicationDeleting 358 *o = *app 359 case "addon-secret-enabled": 360 o := obj.(*corev1.Secret) 361 secret := &corev1.Secret{} 362 secret.Data = map[string][]byte{ 363 "some-key": []byte("some-value"), 364 } 365 *o = *secret 366 case "addon-secret-disabling", "addon-secret-enabling": 367 o := obj.(*corev1.Secret) 368 secret := &corev1.Secret{} 369 secret.Data = map[string][]byte{} 370 *o = *secret 371 default: 372 o := obj.(*v1beta1.Application) 373 app := &v1beta1.Application{} 374 app.Status.Phase = common.ApplicationRendering 375 *o = *app 376 } 377 return nil 378 }) 379 380 cli := test.MockClient{ 381 MockGet: getFunc, 382 } 383 384 cases := []struct { 385 name string 386 expectStatus string 387 expectedParameters map[string]interface{} 388 }{ 389 { 390 name: "disabled", expectStatus: "disabled", 391 }, 392 { 393 name: "suspend", expectStatus: "suspend", 394 }, 395 { 396 name: "enabled", expectStatus: "enabled", 397 }, 398 { 399 name: "disabling", expectStatus: "disabling", 400 }, 401 { 402 name: "enabling", expectStatus: "enabling", 403 }, 404 } 405 406 for _, s := range cases { 407 addonStatus, err := GetAddonStatus(context.Background(), &cli, s.name) 408 assert.NoError(t, err) 409 assert.Equal(t, addonStatus.AddonPhase, s.expectStatus) 410 } 411 } 412 413 func TestGetAddonVersionMeetSystemRequirement(t *testing.T) { 414 server := httptest.NewServer(helmHandler) 415 defer server.Close() 416 i := &Installer{ 417 r: &Registry{ 418 Helm: &HelmSource{ 419 URL: server.URL, 420 }, 421 }, 422 } 423 version := i.getAddonVersionMeetSystemRequirement("fluxcd-no-requirements") 424 assert.Equal(t, version, "1.0.0") 425 version = i.getAddonVersionMeetSystemRequirement("not-exist") 426 assert.Equal(t, version, "") 427 } 428 429 func TestHasNotCoveredClusters(t *testing.T) { 430 // case1: clusterArgValue can cover addonClusters 431 cav := []interface{}{"local"} 432 addonClusters := []string{"local"} 433 notCovered, mergedClusters := hasNotCoveredClusters(cav, addonClusters) 434 assert.False(t, notCovered) 435 assert.Equal(t, []string{"local"}, mergedClusters) 436 437 // case2: clusterArgValue can not cover addonClusters 438 addonClusters = []string{"local", "c1"} 439 notCovered1, mergedClusters1 := hasNotCoveredClusters(cav, addonClusters) 440 assert.True(t, notCovered1) 441 assert.Equal(t, addonClusters, mergedClusters1) 442 } 443 444 var baseAddon = InstallPackage{ 445 Meta: Meta{ 446 Name: "test-render-cue-definition-addon", 447 NeedNamespace: []string{"test-ns"}, 448 DeployTo: &DeployTo{RuntimeCluster: true}, 449 }, 450 CUEDefinitions: []ElementFile{ 451 { 452 Data: testCueDef, 453 Name: "test-def", 454 }, 455 }, 456 } 457 458 var multiYamlAddon = InstallPackage{ 459 Meta: Meta{ 460 Name: "test-render-multi-yaml-addon", 461 }, 462 YAMLTemplates: []ElementFile{ 463 { 464 Data: testYamlObject1, 465 Name: "test-object-1", 466 }, 467 { 468 Data: testYamlObject2, 469 Name: "test-object-2", 470 }, 471 }, 472 } 473 474 var viewAddon = InstallPackage{ 475 Meta: Meta{ 476 Name: "test-render-view-addon", 477 }, 478 YAMLViews: []ElementFile{ 479 { 480 Data: testYAMLView, 481 Name: "cloud-resource-view", 482 }, 483 }, 484 CUEViews: []ElementFile{ 485 { 486 Data: testCUEView, 487 Name: "pod-view", 488 }, 489 }, 490 } 491 492 var testCueDef = `annotations: { 493 type: "trait" 494 annotations: {} 495 labels: { 496 "ui-hidden": "true" 497 } 498 description: "Add annotations on K8s pod for your workload which follows the pod spec in path 'spec.template'." 499 attributes: { 500 podDisruptive: true 501 appliesToWorkloads: ["*"] 502 } 503 } 504 template: { 505 patch: { 506 metadata: { 507 annotations: { 508 for k, v in parameter { 509 "\(k)": v 510 } 511 } 512 } 513 spec: template: metadata: annotations: { 514 for k, v in parameter { 515 "\(k)": v 516 } 517 } 518 } 519 parameter: [string]: string 520 } 521 ` 522 523 var testYamlObject1 = ` 524 apiVersion: apps/v1 525 kind: Deployment 526 metadata: 527 name: nginx-deployment 528 labels: 529 app: nginx 530 spec: 531 replicas: 3 532 selector: 533 matchLabels: 534 app: nginx 535 template: 536 metadata: 537 labels: 538 app: nginx 539 spec: 540 containers: 541 - name: nginx 542 image: nginx:1.14.2 543 ports: 544 - containerPort: 80 545 ` 546 var testYamlObject2 = ` 547 apiVersion: apps/v1 548 kind: Deployment 549 metadata: 550 name: nginx-deployment-2 551 labels: 552 app: nginx 553 spec: 554 replicas: 3 555 selector: 556 matchLabels: 557 app: nginx 558 template: 559 metadata: 560 labels: 561 app: nginx 562 spec: 563 containers: 564 - name: nginx 565 image: nginx:1.14.2 566 ports: 567 - containerPort: 80 568 ` 569 570 var testYAMLView = ` 571 apiVersion: "v1" 572 kind: "ConfigMap" 573 metadata: 574 name: "cloud-resource-view" 575 namespace: "vela-system" 576 data: 577 template: | 578 import ( 579 "vela/ql" 580 ) 581 582 parameter: { 583 appName: string 584 appNs: string 585 } 586 resources: ql.#ListResourcesInApp & { 587 app: { 588 name: parameter.appName 589 namespace: parameter.appNs 590 filter: { 591 "apiVersion": "terraform.core.oam.dev/v1beta1" 592 "kind": "Configuration" 593 } 594 withStatus: true 595 } 596 } 597 status: { 598 if resources.err == _|_ { 599 "cloud-resources": [ for i, resource in resources.list { 600 resource.object 601 }] 602 } 603 if resources.err != _|_ { 604 error: resources.err 605 } 606 } 607 608 609 ` 610 var testCUEView = ` 611 import ( 612 "vela/ql" 613 ) 614 615 parameter: { 616 name: string 617 namespace: string 618 cluster: *"" | string 619 } 620 pod: ql.#Read & { 621 value: { 622 apiVersion: "v1" 623 kind: "Pod" 624 metadata: { 625 name: parameter.name 626 namespace: parameter.namespace 627 } 628 } 629 cluster: parameter.cluster 630 } 631 eventList: ql.#SearchEvents & { 632 value: { 633 apiVersion: "v1" 634 kind: "Pod" 635 metadata: pod.value.metadata 636 } 637 cluster: parameter.cluster 638 } 639 podMetrics: ql.#Read & { 640 cluster: parameter.cluster 641 value: { 642 apiVersion: "metrics.k8s.io/v1beta1" 643 kind: "PodMetrics" 644 metadata: { 645 name: parameter.name 646 namespace: parameter.namespace 647 } 648 } 649 } 650 status: { 651 if pod.err == _|_ { 652 containers: [ for container in pod.value.spec.containers { 653 name: container.name 654 image: container.image 655 resources: { 656 if container.resources.limits != _|_ { 657 limits: container.resources.limits 658 } 659 if container.resources.requests != _|_ { 660 requests: container.resources.requests 661 } 662 if podMetrics.err == _|_ { 663 usage: {for containerUsage in podMetrics.value.containers { 664 if containerUsage.name == container.name { 665 cpu: containerUsage.usage.cpu 666 memory: containerUsage.usage.memory 667 } 668 }} 669 } 670 } 671 if pod.value.status.containerStatuses != _|_ { 672 status: {for containerStatus in pod.value.status.containerStatuses if containerStatus.name == container.name { 673 state: containerStatus.state 674 restartCount: containerStatus.restartCount 675 }} 676 } 677 }] 678 if eventList.err == _|_ { 679 events: eventList.list 680 } 681 } 682 if pod.err != _|_ { 683 error: pod.err 684 } 685 } 686 687 ` 688 689 func TestGetPatternFromItem(t *testing.T) { 690 ossR, err := NewAsyncReader("http://ep.beijing", "some-bucket", "", "some-sub-path", "", ossType) 691 assert.NoError(t, err) 692 gitR, err := NewAsyncReader("https://github.com/oam-dev/catalog", "", "", "addons", "", gitType) 693 assert.NoError(t, err) 694 gitItemName := "parameter.cue" 695 gitItemType := FileType 696 gitItemPath := "addons/terraform/resources/parameter.cue" 697 698 viewOSSR := localReader{ 699 dir: "./testdata/test-view", 700 name: "test-view", 701 } 702 viewPath := filepath.Join("./testdata/test-view/views/pod-view.cue", "pod-view.cue") 703 704 testCases := []struct { 705 caseName string 706 item Item 707 root string 708 meetPattern string 709 r AsyncReader 710 }{ 711 { 712 caseName: "OSS case", 713 item: OSSItem{ 714 tp: FileType, 715 path: "terraform/resources/parameter.cue", 716 name: "parameter.cue", 717 }, 718 root: "terraform", 719 meetPattern: "resources/parameter.cue", 720 r: ossR, 721 }, 722 { 723 caseName: "git case", 724 item: &github.RepositoryContent{Name: &gitItemName, Type: &gitItemType, Path: &gitItemPath}, 725 root: "terraform", 726 meetPattern: "resources/parameter.cue", 727 r: gitR, 728 }, 729 { 730 caseName: "views case", 731 item: OSSItem{ 732 tp: FileType, 733 path: viewPath, 734 name: "pod-view.cue", 735 }, 736 root: "test-view", 737 meetPattern: "views", 738 r: viewOSSR, 739 }, 740 } 741 for _, tc := range testCases { 742 res := GetPatternFromItem(tc.item, tc.r, tc.root) 743 assert.Equal(t, res, tc.meetPattern, tc.caseName) 744 } 745 } 746 747 func TestGitLabReaderNotPanic(t *testing.T) { 748 _, err := NewAsyncReader("https://gitlab.com/test/catalog", "", "", "addons", "", gitType) 749 assert.EqualError(t, err, "git type repository only support github for now") 750 } 751 752 func TestCheckSemVer(t *testing.T) { 753 testCases := []struct { 754 actual string 755 require string 756 nilError bool 757 res bool 758 }{ 759 { 760 actual: "v1.2.1", 761 require: "<=v1.2.1", 762 res: true, 763 }, 764 { 765 actual: "v1.2.1", 766 require: ">v1.2.1", 767 res: false, 768 }, 769 { 770 actual: "v1.2.1", 771 require: "<=v1.2.3", 772 res: true, 773 }, 774 { 775 actual: "v1.2", 776 require: "<=v1.2.3", 777 res: true, 778 }, 779 { 780 actual: "v1.2.1", 781 require: ">v1.2.3", 782 res: false, 783 }, 784 { 785 actual: "v1.2.1", 786 require: "=v1.2.1", 787 res: true, 788 }, 789 { 790 actual: "1.2.1", 791 require: "=v1.2.1", 792 res: true, 793 }, 794 { 795 actual: "1.2.1", 796 require: "", 797 res: true, 798 }, 799 { 800 actual: "v1.2.2", 801 require: "<=v1.2.3, >=v1.2.1", 802 res: true, 803 }, 804 { 805 actual: "v1.2.0", 806 require: "v1.2.0, <=v1.2.3", 807 res: true, 808 }, 809 { 810 actual: "1.2.2", 811 require: "v1.2.2", 812 res: true, 813 }, 814 { 815 actual: "1.2.02", 816 require: "v1.2.2", 817 res: true, 818 }, 819 { 820 actual: "1.3.0-beta.1", 821 require: ">=v1.3.0-alpha.1", 822 res: true, 823 }, 824 { 825 actual: "1.3.0-alpha.2", 826 require: ">=v1.3.0-alpha.1", 827 res: true, 828 }, 829 { 830 actual: "1.2.3", 831 require: ">=v1.3.0-alpha.1", 832 res: false, 833 }, 834 { 835 actual: "v1.4.0-alpha.3", 836 require: ">=v1.3.0-beta.2", 837 res: true, 838 }, 839 { 840 actual: "v1.4.0-beta.1", 841 require: ">=v1.3.0", 842 res: true, 843 }, 844 { 845 actual: "v1.4.0", 846 require: ">=v1.3.0-beta.2", 847 res: true, 848 }, 849 { 850 actual: "1.2.4-beta.2", 851 require: ">=v1.2.4-beta.3", 852 res: false, 853 }, 854 { 855 actual: "1.5.0-beta.2", 856 require: ">=1.5.0", 857 res: false, 858 }, 859 { 860 actual: "1.5.0-alpha.2", 861 require: ">=1.5.0", 862 res: false, 863 }, 864 { 865 actual: "1.5.0-rc.2", 866 require: ">=1.5.0-beta.1", 867 res: true, 868 }, 869 { 870 actual: "1.5.0-rc.2", 871 require: ">=1.5.0-rc.1", 872 res: true, 873 }, 874 { 875 actual: "1.5.0-rc.1", 876 require: ">=1.5.0-alpha.1", 877 res: true, 878 }, 879 { 880 actual: "1.5.0-rc.2", 881 require: ">=1.5.0", 882 res: false, 883 }, 884 { 885 actual: "1.5.0-rc.2", 886 require: ">=1.4.0", 887 res: true, 888 }, 889 } 890 for _, testCase := range testCases { 891 result, err := checkSemVer(testCase.actual, testCase.require) 892 assert.NoError(t, err) 893 assert.Equal(t, result, testCase.res) 894 } 895 } 896 897 func TestCheckAddonVersionMeetRequired(t *testing.T) { 898 k8sClient := &test.MockClient{ 899 MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { 900 return nil 901 }), 902 MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { 903 robj := obj.(*appsv1.DeploymentList) 904 list := &appsv1.DeploymentList{ 905 Items: []appsv1.Deployment{ 906 { 907 ObjectMeta: metav1.ObjectMeta{ 908 Labels: map[string]string{ 909 oam.LabelControllerName: oam.ApplicationControllerName, 910 }, 911 }, 912 Spec: appsv1.DeploymentSpec{ 913 Template: corev1.PodTemplateSpec{ 914 Spec: corev1.PodSpec{ 915 Containers: []corev1.Container{ 916 { 917 Image: "vela-core:v1.2.5", 918 }, 919 }, 920 }, 921 }, 922 }, 923 }, 924 }, 925 } 926 list.DeepCopyInto(robj) 927 return nil 928 }), 929 } 930 ctx := context.Background() 931 assert.NoError(t, checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil)) 932 933 version2.VelaVersion = "v1.2.3" 934 if err := checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil); err == nil { 935 assert.Error(t, fmt.Errorf("should meet error")) 936 } 937 938 version2.VelaVersion = "v1.2.4" 939 assert.NoError(t, checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil)) 940 } 941 942 var testUnmarshalToContent1 = ` 943 { 944 "type": "file", 945 "encoding": "", 946 "size": 651, 947 "name": "metadata.yaml", 948 "path": "example/metadata.yaml", 949 "content": "name: example\r\nversion: 1.0.0\r\ndescription: Extended workload to do continuous and progressive delivery\r\nicon: https://raw.githubusercontent.com/fluxcd/flux/master/docs/_files/weave-flux.png\r\nurl: https://fluxcd.io\r\n\r\ntags:\r\n - extended_workload\r\n - gitops\r\n - only_example\r\n\r\ndeployTo:\r\n control_plane: true\r\n runtime_cluster: false\r\n\r\ndependencies: []\r\n#- name: addon_name\r\n\r\n# set invisible means this won't be list and will be enabled when depended on\r\n# for example, terraform-alibaba depends on terraform which is invisible,\r\n# when terraform-alibaba is enabled, terraform will be enabled automatically\r\n# default: false\r\ninvisible: false\r\n" 950 }` 951 var testUnmarshalToContent2 = ` 952 [ 953 { 954 "type": "dir", 955 "name": "example", 956 "path": "example" 957 }, 958 { 959 "type": "dir", 960 "name": "local", 961 "path": "local" 962 }, 963 { 964 "type": "dir", 965 "name": "terraform", 966 "path": "terraform" 967 }, 968 { 969 "type": "dir", 970 "name": "terraform-alibaba", 971 "path": "terraform-alibaba" 972 }, 973 { 974 "type": "dir", 975 "name": "test-error-addon", 976 "path": "test-error-addon" 977 } 978 ]` 979 var testUnmarshalToContent3 = ` 980 [ 981 { 982 "type": "dir", 983 "name": "example", 984 }, 985 { 986 "type": "dir", 987 "name": "local", 988 "path": "local" 989 } 990 ]` 991 var testUnmarshalToContent4 = `` 992 993 func TestUnmarshalToContent(t *testing.T) { 994 _, _, err1 := unmarshalToContent([]byte(testUnmarshalToContent1)) 995 assert.NoError(t, err1) 996 _, _, err2 := unmarshalToContent([]byte(testUnmarshalToContent2)) 997 assert.NoError(t, err2) 998 _, _, err3 := unmarshalToContent([]byte(testUnmarshalToContent3)) 999 assert.Error(t, err3, "unmarshalling failed for both file and directory content: invalid character '}' looking for beginnin") 1000 _, _, err4 := unmarshalToContent([]byte(testUnmarshalToContent4)) 1001 assert.Error(t, err4, "unmarshalling failed for both file and directory content: unexpected end of JSON input and unexpecte") 1002 } 1003 1004 // Test readResFile, only accept .cue and .yaml/.yml 1005 func TestReadResFile(t *testing.T) { 1006 1007 // setup test data 1008 testAddonName := "example" 1009 testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) 1010 reader := localReader{dir: testAddonDir, name: testAddonName} 1011 metas, err := reader.ListAddonMeta() 1012 testAddonMeta := metas[testAddonName] 1013 assert.NoError(t, err) 1014 1015 // run test 1016 var addon = &InstallPackage{} 1017 ptItems := ClassifyItemByPattern(&testAddonMeta, reader) 1018 items := ptItems[ResourcesDirName] 1019 for _, it := range items { 1020 err := readResFile(addon, reader, reader.RelativePath(it)) 1021 assert.NoError(t, err) 1022 } 1023 1024 // verify 1025 assert.True(t, len(addon.YAMLTemplates) == 1) 1026 } 1027 1028 // Test readDefFile only accept .cue and .yaml/.yml 1029 func TestReadDefFile(t *testing.T) { 1030 1031 // setup test data 1032 testAddonName := "example" 1033 testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) 1034 reader := localReader{dir: testAddonDir, name: testAddonName} 1035 metas, err := reader.ListAddonMeta() 1036 testAddonMeta := metas[testAddonName] 1037 assert.NoError(t, err) 1038 1039 // run test 1040 var uiData = &UIData{} 1041 ptItems := ClassifyItemByPattern(&testAddonMeta, reader) 1042 items := ptItems[DefinitionsDirName] 1043 1044 for _, it := range items { 1045 err := readDefFile(uiData, reader, reader.RelativePath(it)) 1046 if err != nil { 1047 assert.Error(t, fmt.Errorf("Something wrong.")) 1048 } 1049 } 1050 1051 // verify 1052 assert.True(t, len(uiData.Definitions) == 1) 1053 } 1054 1055 // Test readDefFile only accept .cue 1056 func TestReadViewFile(t *testing.T) { 1057 1058 // setup test data 1059 testAddonName := "test-view" 1060 testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) 1061 reader := localReader{dir: testAddonDir, name: testAddonName} 1062 metas, err := reader.ListAddonMeta() 1063 testAddonMeta := metas[testAddonName] 1064 assert.NoError(t, err) 1065 1066 // run test 1067 var addon = &InstallPackage{} 1068 ptItems := ClassifyItemByPattern(&testAddonMeta, reader) 1069 items := ptItems[ViewDirName] 1070 1071 for _, it := range items { 1072 err := readViewFile(addon, reader, reader.RelativePath(it)) 1073 if err != nil { 1074 assert.NoError(t, err) 1075 } 1076 } 1077 notExistErr := readViewFile(addon, reader, "not-exist.cue") 1078 assert.Error(t, notExistErr) 1079 1080 // verify 1081 assert.True(t, len(addon.CUEViews) == 1) 1082 assert.True(t, len(addon.YAMLViews) == 1) 1083 } 1084 1085 func TestRenderCUETemplate(t *testing.T) { 1086 fileDate, err := os.ReadFile("./testdata/example/resources/configmap.cue") 1087 assert.NoError(t, err) 1088 addon := &InstallPackage{ 1089 Meta: Meta{ 1090 Version: "1.0.1", 1091 }, 1092 Parameters: "{\"example\": \"\"}", 1093 } 1094 component, err := renderCompAccordingCUETemplate(ElementFile{Data: string(fileDate), Name: "configmap.cue"}, addon, map[string]interface{}{ 1095 "example": "render", 1096 }) 1097 assert.NoError(t, err) 1098 assert.True(t, component.Type == "raw") 1099 var config = make(map[string]interface{}) 1100 err = json.Unmarshal(component.Properties.Raw, &config) 1101 assert.NoError(t, err) 1102 assert.True(t, component.Type == "raw") 1103 assert.True(t, config["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["version"] == "1.0.1") 1104 } 1105 1106 func TestCheckEnableAddonErrorWhenMissMatch(t *testing.T) { 1107 version2.VelaVersion = "v1.3.0" 1108 i := InstallPackage{Meta: Meta{SystemRequirements: &SystemRequirements{VelaVersion: ">=1.4.0"}}} 1109 installer := &Installer{} 1110 _, err := installer.enableAddon(&i) 1111 assert.Equal(t, errors.As(err, &VersionUnMatchError{}), true) 1112 } 1113 1114 func TestPackageAddon(t *testing.T) { 1115 pwd, _ := os.Getwd() 1116 1117 validAddonDict := "./testdata/example-legacy" 1118 archiver, err := PackageAddon(validAddonDict) 1119 assert.NoError(t, err) 1120 assert.Equal(t, filepath.Join(pwd, "example-legacy-1.0.1.tgz"), archiver) 1121 // Remove generated package after tests 1122 defer func() { 1123 _ = os.RemoveAll(filepath.Join(pwd, "example-legacy-1.0.1.tgz")) 1124 }() 1125 1126 invalidAddonDict := "./testdata" 1127 archiver, err = PackageAddon(invalidAddonDict) 1128 assert.NotNil(t, err) 1129 assert.Equal(t, "", archiver) 1130 1131 invalidAddonMetadata := "./testdata/invalid-metadata" 1132 archiver, err = PackageAddon(invalidAddonMetadata) 1133 assert.NotNil(t, err) 1134 assert.Equal(t, "", archiver) 1135 } 1136 1137 func TestGenerateAnnotation(t *testing.T) { 1138 meta := Meta{ 1139 Name: "test-addon", 1140 SystemRequirements: &SystemRequirements{ 1141 VelaVersion: ">1.4.0", 1142 KubernetesVersion: ">1.20.0", 1143 }} 1144 res := generateAnnotation(&meta) 1145 assert.Equal(t, res[velaSystemRequirement], ">1.4.0") 1146 assert.Equal(t, res[kubernetesSystemRequirement], ">1.20.0") 1147 assert.Equal(t, res[addonSystemRequirement], meta.Name) 1148 1149 meta = Meta{} 1150 meta.SystemRequirements = &SystemRequirements{KubernetesVersion: ">=1.20.1"} 1151 res = generateAnnotation(&meta) 1152 assert.Equal(t, res[velaSystemRequirement], "") 1153 assert.Equal(t, res[kubernetesSystemRequirement], ">=1.20.1") 1154 } 1155 1156 func TestMergeAddonInstallArgs(t *testing.T) { 1157 k8sClient := fake.NewClientBuilder().Build() 1158 ctx := context.Background() 1159 1160 testcases := []struct { 1161 name string 1162 legacyArgs string 1163 args map[string]interface{} 1164 mergedArgs string 1165 application string 1166 err error 1167 }{ 1168 { 1169 name: "addon1", 1170 legacyArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", 1171 args: map[string]interface{}{ 1172 "serviceType": "NodePort", 1173 }, 1174 mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", 1175 }, 1176 { 1177 name: "addon2", 1178 legacyArgs: "{\"clusters\":[\"\"]}", 1179 args: map[string]interface{}{ 1180 "repo": "hub.vela.com", 1181 "serviceType": "NodePort", 1182 "imagePullSecrets": []string{"test-hub"}, 1183 }, 1184 mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", 1185 }, 1186 { 1187 name: "addon3", 1188 legacyArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", 1189 args: map[string]interface{}{ 1190 "imagePullSecrets": []string{"test-hub-2"}, 1191 }, 1192 mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub-2\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", 1193 }, 1194 { 1195 // merge nested parameters 1196 name: "addon4", 1197 legacyArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", 1198 args: map[string]interface{}{ 1199 "p1": map[string]interface{}{ 1200 "p12": "p12-v2", 1201 "p13": "p13-v1", 1202 }, 1203 }, 1204 mergedArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v2\",\"p13\":\"p13-v1\"}}", 1205 }, 1206 { 1207 // there is not legacyArgs 1208 name: "addon5", 1209 legacyArgs: "", 1210 args: map[string]interface{}{ 1211 "p1": map[string]interface{}{ 1212 "p12": "p12-v2", 1213 "p13": "p13-v1", 1214 }, 1215 }, 1216 mergedArgs: "{\"p1\":{\"p12\":\"p12-v2\",\"p13\":\"p13-v1\"}}", 1217 }, 1218 { 1219 // there is not new args 1220 name: "addon6", 1221 legacyArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", 1222 args: nil, 1223 mergedArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", 1224 }, 1225 } 1226 1227 for _, tc := range testcases { 1228 t.Run("", func(t *testing.T) { 1229 if len(tc.legacyArgs) != 0 { 1230 secret := &corev1.Secret{ 1231 ObjectMeta: metav1.ObjectMeta{ 1232 Name: addonutil.Addon2SecName(tc.name), 1233 Namespace: types.DefaultKubeVelaNS, 1234 }, 1235 Data: map[string][]byte{ 1236 AddonParameterDataKey: []byte(tc.legacyArgs), 1237 }, 1238 } 1239 err := k8sClient.Create(ctx, secret) 1240 assert.NoError(t, err) 1241 } 1242 1243 addonArgs, err := MergeAddonInstallArgs(ctx, k8sClient, tc.name, tc.args) 1244 assert.NoError(t, err) 1245 args, err := json.Marshal(addonArgs) 1246 assert.NoError(t, err) 1247 assert.Equal(t, tc.mergedArgs, string(args), tc.name) 1248 }) 1249 } 1250 1251 } 1252 1253 func TestGenerateConflictError(t *testing.T) { 1254 confictAddon := map[string]string{ 1255 "helm": "definition: helm already exist and not belong to any addon \n", 1256 "kustomize": "definition: kustomize in this addon already exist in fluxcd \n", 1257 } 1258 err := produceDefConflictError(confictAddon) 1259 assert.Error(t, err) 1260 strings.Contains(err.Error(), "in this addon already exist in fluxcd") 1261 1262 assert.NoError(t, produceDefConflictError(map[string]string{})) 1263 } 1264 1265 // write a test for sortVersionsDescending 1266 func TestSortVersionsDescending(t *testing.T) { 1267 testCases := []struct { 1268 caseName string 1269 versions []string 1270 res []string 1271 }{ 1272 { 1273 caseName: "empty list", 1274 versions: []string{}, 1275 res: nil, 1276 }, 1277 { 1278 caseName: "one version", 1279 versions: []string{"1.2.3"}, 1280 res: []string{"1.2.3"}, 1281 }, 1282 { 1283 caseName: "multiple versions", 1284 versions: []string{"0.1.0", "1.2.3", "1.0.0", "1.1.0"}, 1285 res: []string{"1.2.3", "1.1.0", "1.0.0", "0.1.0"}, 1286 }, 1287 { 1288 caseName: "various SemVer formats", 1289 versions: []string{ 1290 "1.2.3", "1.2.3-rc.1", "1.2.3-rc.2", "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-1", "1.0.0+1", 1291 }, 1292 res: []string{"1.2.3", "1.2.3-rc.2", "1.2.3-rc.1", "1.0.0+1", "1.0.0-alpha.1", "1.0.0-alpha", "1.0.0-1"}, 1293 }, 1294 { 1295 caseName: "SemVer-ish versions", 1296 versions: []string{"v1.0.0", "1.1", "2", "1-2", "1+2"}, 1297 res: []string{"2.0.0", "1.1.0", "1.0.0", "1.0.0+2", "1.0.0-2"}, 1298 }, 1299 { 1300 caseName: "list with some non-SemVer-ish versions", 1301 versions: []string{"2.0.0", "1a", "b", "1,2", "1.0.0"}, 1302 res: []string{"2.0.0", "1.0.0"}, 1303 }, 1304 } 1305 for _, tc := range testCases { 1306 res := sortVersionsDescending(tc.versions) 1307 assert.Equal(t, tc.res, res, tc.caseName) 1308 } 1309 } 1310 1311 func TestValidateAddonDependencies(t *testing.T) { 1312 singletonMap := func(addonName string, addonVersions []string) itemInfoMap { 1313 res := make(itemInfoMap) 1314 res[addonName] = ItemInfo{Name: addonName, AvailableVersions: addonVersions} 1315 return res 1316 } 1317 1318 testCases := []struct { 1319 caseName string 1320 installedAddons itemInfoMap 1321 availableAddons itemInfoMap 1322 addon *InstallPackage 1323 err error 1324 }{ 1325 { 1326 caseName: "addon with no dependencies", 1327 1328 addon: &InstallPackage{}, 1329 err: nil, 1330 }, 1331 { 1332 caseName: "dependency with version, name matches available dependency, version available", 1333 1334 availableAddons: singletonMap("addon1", []string{"1.0.0", "1.2.3", "1.3.0", "2.0.0"}), 1335 addon: &InstallPackage{ 1336 Meta: Meta{ 1337 Name: "addon2", 1338 Dependencies: []*Dependency{ 1339 { 1340 Name: "addon1", 1341 Version: ">=1.2.3, <2.0.0", 1342 }, 1343 }, 1344 }, 1345 }, 1346 err: nil, 1347 }, 1348 { 1349 caseName: "multiple validation errors", 1350 1351 addon: &InstallPackage{ 1352 Meta: Meta{ 1353 Name: "addon4", 1354 Dependencies: []*Dependency{ 1355 { 1356 Name: "addon1", 1357 Version: ">=1.2.3, <2.0.0", 1358 }, 1359 { 1360 Name: "addon2", 1361 Version: ">=1.2.3, <2.0.0", 1362 }, 1363 { 1364 Name: "addon3", 1365 Version: ">=1.2.3, <2.0.0", 1366 }, 1367 }, 1368 }, 1369 }, 1370 err: multierr.Combine( 1371 fmt.Errorf("addon addon4 has unresolvable dependency addon1: %w", errors.New("no available addon with name addon1")), 1372 fmt.Errorf("addon addon4 has unresolvable dependency addon2: %w", errors.New("no available addon with name addon2")), 1373 fmt.Errorf("addon addon4 has unresolvable dependency addon3: %w", errors.New("no available addon with name addon3")), 1374 ), 1375 }, 1376 } 1377 for _, tc := range testCases { 1378 err := validateAddonDependencies(tc.addon, tc.installedAddons, tc.availableAddons) 1379 assert.Equal(t, tc.err, err, tc.caseName) 1380 } 1381 } 1382 1383 func TestCalculateDependencyVersionToInstall(t *testing.T) { 1384 singletonMap := func(addonName string, addonVersions []string) itemInfoMap { 1385 res := make(itemInfoMap) 1386 res[addonName] = ItemInfo{Name: addonName, AvailableVersions: addonVersions} 1387 return res 1388 } 1389 1390 testCases := []struct { 1391 caseName string 1392 dep Dependency 1393 installedAddons itemInfoMap 1394 availableAddons itemInfoMap 1395 res string 1396 err error 1397 }{ 1398 { 1399 caseName: "dependency without name", 1400 1401 err: errors.New("dependency name cannot be empty"), 1402 }, 1403 { 1404 caseName: "dependency without version, name matches available dependency", 1405 1406 dep: Dependency{Name: "addon1"}, 1407 availableAddons: singletonMap("addon1", []string{"1.0.0", "1.2.3", "1.3.0", "2.0.0"}), 1408 res: "2.0.0", 1409 }, 1410 { 1411 caseName: "dependency without version, name matches installed dependency", 1412 1413 dep: Dependency{Name: "addon1"}, 1414 installedAddons: singletonMap("addon1", []string{"1.2.3"}), 1415 res: "1.2.3", 1416 }, 1417 { 1418 caseName: "dependency with version, name matches available dependency, version available", 1419 1420 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1421 availableAddons: singletonMap("addon1", []string{"1.0.0", "1.2.3", "1.3.0", "2.0.0"}), 1422 res: "1.3.0", 1423 }, 1424 { 1425 caseName: "dependency with version, name does not match available dependency", 1426 1427 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1428 availableAddons: singletonMap("addon2", []string{"1.0.0", "1.2.3", "1.3.0", "2.0.0"}), 1429 err: errors.New("no available addon with name addon1"), 1430 }, 1431 { 1432 caseName: "dependency with version, name matches available dependency, version not available", 1433 1434 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1435 availableAddons: singletonMap("addon1", []string{"1.0.0", "1.2.0", "2.0.0"}), 1436 err: errors.New("no available addon with name addon1 and version '>=1.2.3, <2.0.0', available versions [1.0.0 1.2.0 2.0.0]"), 1437 }, 1438 { 1439 caseName: "dependency with version, name matches installed dependency", 1440 1441 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1442 installedAddons: singletonMap("addon1", []string{"1.2.3"}), 1443 res: "1.2.3", 1444 }, 1445 { 1446 caseName: "dependency with version, name matches installed dependency, version mismatch", 1447 1448 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1449 installedAddons: singletonMap("addon1", []string{"1.2.0"}), 1450 err: errors.New("addon addon1 version '>=1.2.3, <2.0.0' does not match installed version '1.2.0'"), 1451 }, 1452 { 1453 caseName: "dependency with version, name matches installed and available dependency", 1454 1455 dep: Dependency{Name: "addon1", Version: ">=1.2.3, <2.0.0"}, 1456 installedAddons: singletonMap("addon1", []string{"1.2.3"}), 1457 availableAddons: singletonMap("addon1", []string{"1.0.0", "1.2.3", "1.3.0", "2.0.0"}), 1458 res: "1.2.3", 1459 }, 1460 } 1461 for _, tc := range testCases { 1462 res, err := calculateDependencyVersionToInstall(tc.dep, tc.installedAddons, tc.availableAddons) 1463 assert.Equal(t, tc.res, res, tc.caseName) 1464 assert.Equal(t, tc.err, err, tc.caseName) 1465 } 1466 } 1467 1468 func TestListAvailableAddons(t *testing.T) { 1469 registries := []ItemInfoLister{ 1470 &AddonInfoListerMock{ 1471 expectedData: itemInfoMap{ 1472 "addon1": { 1473 Name: "addon1", 1474 AvailableVersions: []string{"1.0.0"}, 1475 }, 1476 "addon2": { 1477 Name: "addon2", 1478 AvailableVersions: []string{"2.0.0"}, 1479 }, 1480 }, 1481 }, 1482 &AddonInfoListerMock{ 1483 expectedData: itemInfoMap{ 1484 "addon1": { 1485 Name: "addon1", 1486 AvailableVersions: []string{"1.2.0", "1.1.0"}, 1487 }, 1488 "addon3": { 1489 Name: "addon3", 1490 AvailableVersions: []string{"3.0.0"}, 1491 }, 1492 }, 1493 }, 1494 } 1495 res, err := listAvailableAddons(registries) 1496 1497 assert.NoError(t, err) 1498 expected := itemInfoMap{ 1499 // addon1 versions are merged 1500 "addon1": { 1501 Name: "addon1", 1502 AvailableVersions: []string{"1.2.0", "1.1.0", "1.0.0"}, 1503 }, 1504 "addon2": { 1505 Name: "addon2", 1506 AvailableVersions: []string{"2.0.0"}, 1507 }, 1508 "addon3": { 1509 Name: "addon3", 1510 AvailableVersions: []string{"3.0.0"}, 1511 }, 1512 } 1513 assert.Equal(t, expected, res) 1514 } 1515 1516 type AddonInfoListerMock struct { 1517 expectedData itemInfoMap 1518 expectedErr error 1519 } 1520 1521 func (a *AddonInfoListerMock) ListAddonInfo() (map[string]ItemInfo, error) { 1522 return a.expectedData, a.expectedErr 1523 } 1524 1525 func TestListInstalledAddons(t *testing.T) { 1526 // Create some KubeVela addons 1527 k8sClient := fake.NewClientBuilder().Build() 1528 k8sClient.Create(context.Background(), &v1beta1.Application{ 1529 ObjectMeta: metav1.ObjectMeta{ 1530 Name: "addon-addon1", 1531 Namespace: types.DefaultKubeVelaNS, 1532 Labels: map[string]string{ 1533 oam.LabelAddonName: "addon1", 1534 oam.LabelAddonVersion: "1.0.0", 1535 }, 1536 }, 1537 }) 1538 k8sClient.Create(context.Background(), &v1beta1.Application{ 1539 ObjectMeta: metav1.ObjectMeta{ 1540 Name: "addon-addon2", 1541 Namespace: types.DefaultKubeVelaNS, 1542 Labels: map[string]string{ 1543 oam.LabelAddonName: "addon2", 1544 oam.LabelAddonVersion: "2.0.0", 1545 }, 1546 }, 1547 }) 1548 // create an app that's not an addon 1549 k8sClient.Create(context.Background(), &v1beta1.Application{ 1550 ObjectMeta: metav1.ObjectMeta{ 1551 Name: "app1", 1552 Namespace: types.DefaultKubeVelaNS, 1553 }, 1554 }) 1555 1556 res, err := listInstalledAddons(context.Background(), k8sClient) 1557 1558 assert.NoError(t, err) 1559 expected := itemInfoMap{ 1560 "addon1": { 1561 Name: "addon1", 1562 AvailableVersions: []string{"1.0.0"}, 1563 }, 1564 "addon2": { 1565 Name: "addon2", 1566 AvailableVersions: []string{"2.0.0"}, 1567 }, 1568 } 1569 assert.Equal(t, expected, res) 1570 }