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  }