/* Copyright 2021 The KubeVela Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package addon import ( "context" "encoding/json" "encoding/xml" "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path" "path/filepath" "strings" "testing" "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-github/v32/github" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" clustercommon "github.com/oam-dev/cluster-gateway/pkg/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" version2 "github.com/oam-dev/kubevela/version" ) var paths = []string{ "example/metadata.yaml", "example/readme.md", "example/template.cue", "example/definitions/helm.yaml", "example/resources/configmap.cue", "example/parameter.cue", "example/resources/service/source-controller.yaml", "example-legacy/metadata.yaml", "example-legacy/readme.md", "example-legacy/template.yaml", "example-legacy/definitions/helm.yaml", "example-legacy/resources/configmap.cue", "example-legacy/resources/parameter.cue", "example-legacy/resources/service/source-controller.yaml", "terraform/metadata.yaml", "terraform-alibaba/metadata.yaml", "test-error-addon/metadata.yaml", "test-error-addon/resources/parameter.cue", "test-disable-addon/metadata.yaml", "test-disable-addon/definitions/compDef.yaml", "test-disable-addon/definitions/traitDef.cue", } var ossHandler http.HandlerFunc = func(rw http.ResponseWriter, req *http.Request) { queryPath := strings.TrimPrefix(req.URL.Path, "/") if strings.Contains(req.URL.RawQuery, "prefix") { prefix := req.URL.Query().Get("prefix") res := ListBucketResult{ Files: []File{}, Count: 0, } for _, p := range paths { if strings.HasPrefix(p, prefix) { // Size 100 is for mock a file res.Files = append(res.Files, File{Name: p, Size: 100}) res.Count += 1 } } data, err := xml.Marshal(res) if err != nil { rw.Write([]byte(err.Error())) } rw.Write(data) } else { found := false for _, p := range paths { if queryPath == p { file, err := os.ReadFile(path.Join("testdata", queryPath)) if err != nil { rw.Write([]byte(err.Error())) } found = true rw.Write(file) break } } if !found { rw.Write([]byte("not found")) } } } var helmHandler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) { switch { case strings.Contains(request.URL.Path, "index.yaml"): files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/index.yaml") if err != nil { _, _ = writer.Write([]byte(err.Error())) } writer.Write(files) case strings.Contains(request.URL.Path, "fluxcd-1.0.0.tgz"): files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-1.0.0.tgz") if err != nil { _, _ = writer.Write([]byte(err.Error())) } writer.Write(files) case strings.Contains(request.URL.Path, "fluxcd-2.0.0.tgz"): files, err := ioutil.ReadFile("./testdata/multiversion-helm-repo/fluxcd-2.0.0.tgz") if err != nil { _, _ = writer.Write([]byte(err.Error())) } writer.Write(files) } } var ctx = context.Background() func testReaderFunc(t *testing.T, reader AsyncReader) { registryMeta, err := reader.ListAddonMeta() assert.NoError(t, err) testAddonName := "example" var testAddonMeta SourceMeta for _, m := range registryMeta { if m.Name == testAddonName { testAddonMeta = m break } } assert.NoError(t, err) uiData, err := GetUIDataFromReader(reader, &testAddonMeta, UIMetaOptions) assert.NoError(t, err) assert.Equal(t, uiData.Name, testAddonName) assert.True(t, uiData.Parameters != "") assert.Equal(t, len(uiData.APISchema.Properties), 1) assert.Equal(t, uiData.APISchema.Properties["example"].Value.Description, "the example field") assert.True(t, len(uiData.Definitions) > 0) testAddonName = "example-legacy" for _, m := range registryMeta { if m.Name == testAddonName { testAddonMeta = m break } } assert.NoError(t, err) uiData, err = GetUIDataFromReader(reader, &testAddonMeta, UIMetaOptions) assert.NoError(t, err) assert.Equal(t, uiData.Name, testAddonName) assert.True(t, uiData.Parameters != "") assert.True(t, len(uiData.Definitions) > 0) // test get ui data rName := "KubeVela" uiDataList, err := ListAddonUIDataFromReader(reader, registryMeta, rName, UIMetaOptions) fmt.Println(err.Error()) assert.True(t, strings.Contains(err.Error(), "preference mark not allowed at this position")) assert.Equal(t, 5, len(uiDataList)) assert.Equal(t, uiDataList[0].RegistryName, rName) // test get install package installPkg, err := GetInstallPackageFromReader(reader, &testAddonMeta, uiData) assert.NoError(t, err) assert.NotNil(t, installPkg, "should get install package") assert.Equal(t, len(installPkg.CUETemplates), 1) } func TestGetAddonData(t *testing.T) { server := httptest.NewServer(ossHandler) defer server.Close() reader, err := NewAsyncReader(server.URL, "", "", "", "", ossType) assert.NoError(t, err) testReaderFunc(t, reader) } func TestRender(t *testing.T) { testcases := []struct { envs []ObservabilityEnvironment tmpl string expect string err error }{ { envs: []ObservabilityEnvironment{ { Cluster: "c1", }, { Cluster: "c2", }, }, tmpl: ObservabilityEnvBindingEnvTmpl, expect: ` - name: c1 placement: clusterSelector: name: c1 - name: c2 placement: clusterSelector: name: c2 `, err: nil, }, { envs: []ObservabilityEnvironment{ { Cluster: "c1", }, { Cluster: "c2", }, }, tmpl: ObservabilityWorkflow4EnvBindingTmpl, expect: ` - name: c1 type: deploy2env properties: policy: domain env: c1 parallel: true - name: c2 type: deploy2env properties: policy: domain env: c2 parallel: true `, err: nil, }, } for _, tc := range testcases { t.Run("", func(t *testing.T) { rendered, err := render(tc.envs, tc.tmpl) assert.Equal(t, tc.err, err) assert.Equal(t, tc.expect, rendered) }) } } func TestRenderApp(t *testing.T) { addon := baseAddon app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{}) assert.NoError(t, err, "render app fail") // definition should not be rendered assert.Equal(t, len(app.Spec.Components), 1) } func TestRenderAppWithNeedNamespace(t *testing.T) { addon := baseAddon addon.NeedNamespace = append(addon.NeedNamespace, types.DefaultKubeVelaNS, "test-ns2") app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{}) assert.NoError(t, err, "render app fail") assert.Equal(t, len(app.Spec.Components), 2) for _, c := range app.Spec.Components { assert.NotEqual(t, types.DefaultKubeVelaNS+"-namespace", c.Name, "namespace vela-system should not be rendered") } } func TestRenderDeploy2RuntimeAddon(t *testing.T) { addonDeployToRuntime := baseAddon addonDeployToRuntime.Meta.DeployTo = &DeployTo{ DisableControlPlane: false, RuntimeCluster: true, } defs, err := RenderDefinitions(&addonDeployToRuntime, nil) assert.NoError(t, err) assert.Equal(t, len(defs), 1) def := defs[0] assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1") assert.Equal(t, def.GetKind(), "TraitDefinition") app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{}) assert.NoError(t, err) policies := app.Spec.Policies assert.True(t, len(policies) == 1) assert.Equal(t, policies[0].Type, v1alpha1.TopologyPolicyType) } func TestRenderDefinitions(t *testing.T) { addonDeployToRuntime := baseAddon addonDeployToRuntime.Meta.DeployTo = &DeployTo{ DisableControlPlane: false, RuntimeCluster: false, } defs, err := RenderDefinitions(&addonDeployToRuntime, nil) assert.NoError(t, err) assert.Equal(t, len(defs), 1) def := defs[0] assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1") assert.Equal(t, def.GetKind(), "TraitDefinition") app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{}) assert.NoError(t, err) // addon which app work on no-runtime-cluster mode workflow is nil assert.Nil(t, app.Spec.Workflow) } func TestRenderViews(t *testing.T) { addonDeployToRuntime := viewAddon addonDeployToRuntime.Meta.DeployTo = &DeployTo{ DisableControlPlane: false, RuntimeCluster: false, } views, err := RenderViews(&addonDeployToRuntime) assert.NoError(t, err) assert.Equal(t, len(views), 2) view := views[0] assert.Equal(t, view.GetKind(), "ConfigMap") assert.Equal(t, view.GetAPIVersion(), "v1") assert.Equal(t, view.GetNamespace(), types.DefaultKubeVelaNS) assert.Equal(t, view.GetName(), "cloud-resource-view") view = views[1] assert.Equal(t, view.GetKind(), "ConfigMap") assert.Equal(t, view.GetAPIVersion(), "v1") assert.Equal(t, view.GetNamespace(), types.DefaultKubeVelaNS) assert.Equal(t, view.GetName(), "pod-view") } func TestRenderK8sObjects(t *testing.T) { addonMultiYaml := multiYamlAddon addonMultiYaml.Meta.DeployTo = &DeployTo{ DisableControlPlane: false, RuntimeCluster: false, } app, _, err := RenderApp(ctx, &addonMultiYaml, nil, map[string]interface{}{}) assert.NoError(t, err) assert.Equal(t, len(app.Spec.Components), 1) comp := app.Spec.Components[0] assert.Equal(t, comp.Type, "k8s-objects") } func TestGetAddonStatus(t *testing.T) { getFunc := test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object) error { switch key.Name { case "addon-disabled", "disabled": return kerrors.NewNotFound(schema.GroupResource{Group: "apiVersion: core.oam.dev/v1beta1", Resource: "app"}, key.Name) case "addon-suspend": o := obj.(*v1beta1.Application) app := &v1beta1.Application{} app.Status.Workflow = &common.WorkflowStatus{ Suspend: true, } *o = *app case "addon-enabled": o := obj.(*v1beta1.Application) app := &v1beta1.Application{} app.Status.Phase = common.ApplicationRunning *o = *app case "addon-disabling": o := obj.(*v1beta1.Application) app := &v1beta1.Application{} app.Status.Phase = common.ApplicationDeleting *o = *app case "addon-secret-enabled": o := obj.(*corev1.Secret) secret := &corev1.Secret{} secret.Data = map[string][]byte{ "some-key": []byte("some-value"), } *o = *secret case "addon-secret-disabling", "addon-secret-enabling": o := obj.(*corev1.Secret) secret := &corev1.Secret{} secret.Data = map[string][]byte{} *o = *secret default: o := obj.(*v1beta1.Application) app := &v1beta1.Application{} app.Status.Phase = common.ApplicationRendering *o = *app } return nil }) cli := test.MockClient{ MockGet: getFunc, } cases := []struct { name string expectStatus string expectedParameters map[string]interface{} }{ { name: "disabled", expectStatus: "disabled", }, { name: "suspend", expectStatus: "suspend", }, { name: "enabled", expectStatus: "enabled", }, { name: "disabling", expectStatus: "disabling", }, { name: "enabling", expectStatus: "enabling", }, } for _, s := range cases { addonStatus, err := GetAddonStatus(context.Background(), &cli, s.name) assert.NoError(t, err) assert.Equal(t, addonStatus.AddonPhase, s.expectStatus) } } func TestGetAddonStatus4Observability(t *testing.T) { ctx := context.Background() addonApplication := &v1beta1.Application{ ObjectMeta: metav1.ObjectMeta{ Name: "observability", Namespace: types.DefaultKubeVelaNS, }, Status: common.AppStatus{ Phase: common.ApplicationRunning, }, } addonSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: addonutil.Addon2SecName(ObservabilityAddon), Namespace: types.DefaultKubeVelaNS, }, Data: map[string][]byte{}, } addonService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: types.DefaultKubeVelaNS, Name: ObservabilityAddonEndpointComponent, }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { IP: "1.2.3.4", }, }, }, }, } clusterSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Labels: map[string]string{ clustercommon.LabelKeyClusterCredentialType: string(v1alpha12.CredentialTypeX509Certificate), }, }, Data: map[string][]byte{ "test-key": []byte("test-value"), }, } scheme := runtime.NewScheme() assert.NoError(t, v1beta1.AddToScheme(scheme)) assert.NoError(t, corev1.AddToScheme(scheme)) k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(addonApplication, addonSecret).Build() addonStatus, err := GetAddonStatus(context.Background(), k8sClient, ObservabilityAddon) assert.NoError(t, err) assert.Equal(t, addonStatus.AddonPhase, enabling) // Addon is not installed in multiple clusters k8sClient = fake.NewClientBuilder().WithScheme(scheme).WithObjects(addonApplication, addonSecret, addonService).Build() addonStatus, err = GetAddonStatus(context.Background(), k8sClient, ObservabilityAddon) assert.NoError(t, err) assert.Equal(t, addonStatus.AddonPhase, enabled) // Addon is installed in multiple clusters assert.NoError(t, k8sClient.Create(ctx, clusterSecret)) addonStatus, err = GetAddonStatus(context.Background(), k8sClient, ObservabilityAddon) assert.NoError(t, err) assert.Equal(t, addonStatus.AddonPhase, enabled) } func TestGetAddonVersionMeetSystemRequirement(t *testing.T) { server := httptest.NewServer(helmHandler) defer server.Close() i := &Installer{ r: &Registry{ Helm: &HelmSource{ URL: server.URL, }, }, } version := i.getAddonVersionMeetSystemRequirement("fluxcd-no-requirements") assert.Equal(t, version, "1.0.0") version = i.getAddonVersionMeetSystemRequirement("not-exist") assert.Equal(t, version, "") } var baseAddon = InstallPackage{ Meta: Meta{ Name: "test-render-cue-definition-addon", NeedNamespace: []string{"test-ns"}, DeployTo: &DeployTo{RuntimeCluster: true}, }, CUEDefinitions: []ElementFile{ { Data: testCueDef, Name: "test-def", }, }, } var multiYamlAddon = InstallPackage{ Meta: Meta{ Name: "test-render-multi-yaml-addon", }, YAMLTemplates: []ElementFile{ { Data: testYamlObject1, Name: "test-object-1", }, { Data: testYamlObject2, Name: "test-object-2", }, }, } var viewAddon = InstallPackage{ Meta: Meta{ Name: "test-render-view-addon", }, YAMLViews: []ElementFile{ { Data: testYAMLView, Name: "cloud-resource-view", }, }, CUEViews: []ElementFile{ { Data: testCUEView, Name: "pod-view", }, }, } var testCueDef = `annotations: { type: "trait" annotations: {} labels: { "ui-hidden": "true" } description: "Add annotations on K8s pod for your workload which follows the pod spec in path 'spec.template'." attributes: { podDisruptive: true appliesToWorkloads: ["*"] } } template: { patch: { metadata: { annotations: { for k, v in parameter { "\(k)": v } } } spec: template: metadata: annotations: { for k, v in parameter { "\(k)": v } } } parameter: [string]: string } ` var testYamlObject1 = ` apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 ` var testYamlObject2 = ` apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment-2 labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 ` var testYAMLView = ` apiVersion: "v1" kind: "ConfigMap" metadata: name: "cloud-resource-view" namespace: "vela-system" data: template: | import ( "vela/ql" ) parameter: { appName: string appNs: string } resources: ql.#ListResourcesInApp & { app: { name: parameter.appName namespace: parameter.appNs filter: { "apiVersion": "terraform.core.oam.dev/v1beta1" "kind": "Configuration" } withStatus: true } } status: { if resources.err == _|_ { "cloud-resources": [ for i, resource in resources.list { resource.object }] } if resources.err != _|_ { error: resources.err } } ` var testCUEView = ` import ( "vela/ql" ) parameter: { name: string namespace: string cluster: *"" | string } pod: ql.#Read & { value: { apiVersion: "v1" kind: "Pod" metadata: { name: parameter.name namespace: parameter.namespace } } cluster: parameter.cluster } eventList: ql.#SearchEvents & { value: { apiVersion: "v1" kind: "Pod" metadata: pod.value.metadata } cluster: parameter.cluster } podMetrics: ql.#Read & { cluster: parameter.cluster value: { apiVersion: "metrics.k8s.io/v1beta1" kind: "PodMetrics" metadata: { name: parameter.name namespace: parameter.namespace } } } status: { if pod.err == _|_ { containers: [ for container in pod.value.spec.containers { name: container.name image: container.image resources: { if container.resources.limits != _|_ { limits: container.resources.limits } if container.resources.requests != _|_ { requests: container.resources.requests } if podMetrics.err == _|_ { usage: {for containerUsage in podMetrics.value.containers { if containerUsage.name == container.name { cpu: containerUsage.usage.cpu memory: containerUsage.usage.memory } }} } } if pod.value.status.containerStatuses != _|_ { status: {for containerStatus in pod.value.status.containerStatuses if containerStatus.name == container.name { state: containerStatus.state restartCount: containerStatus.restartCount }} } }] if eventList.err == _|_ { events: eventList.list } } if pod.err != _|_ { error: pod.err } } ` func TestGetPatternFromItem(t *testing.T) { ossR, err := NewAsyncReader("http://ep.beijing", "some-bucket", "", "some-sub-path", "", ossType) assert.NoError(t, err) gitR, err := NewAsyncReader("https://github.com/oam-dev/catalog", "", "", "addons", "", gitType) assert.NoError(t, err) gitItemName := "parameter.cue" gitItemType := FileType gitItemPath := "addons/terraform/resources/parameter.cue" viewOSSR := localReader{ dir: "./testdata/test-view", name: "test-view", } viewPath := filepath.Join("./testdata/test-view/views/pod-view.cue", "pod-view.cue") testCases := []struct { caseName string item Item root string meetPattern string r AsyncReader }{ { caseName: "OSS case", item: OSSItem{ tp: FileType, path: "terraform/resources/parameter.cue", name: "parameter.cue", }, root: "terraform", meetPattern: "resources/parameter.cue", r: ossR, }, { caseName: "git case", item: &github.RepositoryContent{Name: &gitItemName, Type: &gitItemType, Path: &gitItemPath}, root: "terraform", meetPattern: "resources/parameter.cue", r: gitR, }, { caseName: "views case", item: OSSItem{ tp: FileType, path: viewPath, name: "pod-view.cue", }, root: "test-view", meetPattern: "views", r: viewOSSR, }, } for _, tc := range testCases { res := GetPatternFromItem(tc.item, tc.r, tc.root) assert.Equal(t, res, tc.meetPattern, tc.caseName) } } func TestGitLabReaderNotPanic(t *testing.T) { _, err := NewAsyncReader("https://gitlab.com/test/catalog", "", "", "addons", "", gitType) assert.EqualError(t, err, "git type repository only support github for now") } func TestCheckSemVer(t *testing.T) { testCases := []struct { actual string require string nilError bool res bool }{ { actual: "v1.2.1", require: "<=v1.2.1", res: true, }, { actual: "v1.2.1", require: ">v1.2.1", res: false, }, { actual: "v1.2.1", require: "<=v1.2.3", res: true, }, { actual: "v1.2", require: "<=v1.2.3", res: true, }, { actual: "v1.2.1", require: ">v1.2.3", res: false, }, { actual: "v1.2.1", require: "=v1.2.1", res: true, }, { actual: "1.2.1", require: "=v1.2.1", res: true, }, { actual: "1.2.1", require: "", res: true, }, { actual: "v1.2.2", require: "<=v1.2.3, >=v1.2.1", res: true, }, { actual: "v1.2.0", require: "v1.2.0, <=v1.2.3", res: true, }, { actual: "1.2.2", require: "v1.2.2", res: true, }, { actual: "1.2.02", require: "v1.2.2", res: true, }, { actual: "1.3.0-beta.1", require: ">=v1.3.0-alpha.1", res: true, }, { actual: "1.3.0-alpha.2", require: ">=v1.3.0-alpha.1", res: true, }, { actual: "1.2.3", require: ">=v1.3.0-alpha.1", res: false, }, { actual: "v1.4.0-alpha.3", require: ">=v1.3.0-beta.2", res: true, }, { actual: "v1.4.0-beta.1", require: ">=v1.3.0", res: true, }, { actual: "v1.4.0", require: ">=v1.3.0-beta.2", res: true, }, { actual: "1.2.4-beta.2", require: ">=v1.2.4-beta.3", res: false, }, { actual: "1.5.0-beta.2", require: ">=1.5.0", res: false, }, { actual: "1.5.0-alpha.2", require: ">=1.5.0", res: false, }, { actual: "1.5.0-rc.2", require: ">=1.5.0-beta.1", res: true, }, { actual: "1.5.0-rc.2", require: ">=1.5.0-rc.1", res: true, }, { actual: "1.5.0-rc.1", require: ">=1.5.0-alpha.1", res: true, }, { actual: "1.5.0-rc.2", require: ">=1.5.0", res: false, }, { actual: "1.5.0-rc.2", require: ">=1.4.0", res: true, }, } for _, testCase := range testCases { result, err := checkSemVer(testCase.actual, testCase.require) assert.NoError(t, err) assert.Equal(t, result, testCase.res) } } func TestCheckAddonVersionMeetRequired(t *testing.T) { k8sClient := &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { return nil }), MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error { robj := obj.(*appsv1.DeploymentList) list := &appsv1.DeploymentList{ Items: []appsv1.Deployment{ { ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ oam.LabelControllerName: oam.ApplicationControllerName, }, }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "vela-core:v1.2.5", }, }, }, }, }, }, }, } list.DeepCopyInto(robj) return nil }), } ctx := context.Background() assert.NoError(t, checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil)) version2.VelaVersion = "v1.2.3" if err := checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil); err == nil { assert.Error(t, fmt.Errorf("should meet error")) } version2.VelaVersion = "v1.2.4" assert.NoError(t, checkAddonVersionMeetRequired(ctx, &SystemRequirements{VelaVersion: ">=1.2.4"}, k8sClient, nil)) } var testUnmarshalToContent1 = ` { "type": "file", "encoding": "", "size": 651, "name": "metadata.yaml", "path": "example/metadata.yaml", "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" }` var testUnmarshalToContent2 = ` [ { "type": "dir", "name": "example", "path": "example" }, { "type": "dir", "name": "local", "path": "local" }, { "type": "dir", "name": "terraform", "path": "terraform" }, { "type": "dir", "name": "terraform-alibaba", "path": "terraform-alibaba" }, { "type": "dir", "name": "test-error-addon", "path": "test-error-addon" } ]` var testUnmarshalToContent3 = ` [ { "type": "dir", "name": "example", }, { "type": "dir", "name": "local", "path": "local" } ]` var testUnmarshalToContent4 = `` func TestUnmarshalToContent(t *testing.T) { _, _, err1 := unmarshalToContent([]byte(testUnmarshalToContent1)) assert.NoError(t, err1) _, _, err2 := unmarshalToContent([]byte(testUnmarshalToContent2)) assert.NoError(t, err2) _, _, err3 := unmarshalToContent([]byte(testUnmarshalToContent3)) assert.Error(t, err3, "unmarshalling failed for both file and directory content: invalid character '}' looking for beginnin") _, _, err4 := unmarshalToContent([]byte(testUnmarshalToContent4)) assert.Error(t, err4, "unmarshalling failed for both file and directory content: unexpected end of JSON input and unexpecte") } // Test readResFile, only accept .cue and .yaml/.yml func TestReadResFile(t *testing.T) { // setup test data testAddonName := "example" testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) reader := localReader{dir: testAddonDir, name: testAddonName} metas, err := reader.ListAddonMeta() testAddonMeta := metas[testAddonName] assert.NoError(t, err) // run test var addon = &InstallPackage{} ptItems := ClassifyItemByPattern(&testAddonMeta, reader) items := ptItems[ResourcesDirName] for _, it := range items { err := readResFile(addon, reader, reader.RelativePath(it)) assert.NoError(t, err) } // verify assert.True(t, len(addon.YAMLTemplates) == 1) } // Test readDefFile only accept .cue and .yaml/.yml func TestReadDefFile(t *testing.T) { // setup test data testAddonName := "example" testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) reader := localReader{dir: testAddonDir, name: testAddonName} metas, err := reader.ListAddonMeta() testAddonMeta := metas[testAddonName] assert.NoError(t, err) // run test var uiData = &UIData{} ptItems := ClassifyItemByPattern(&testAddonMeta, reader) items := ptItems[DefinitionsDirName] for _, it := range items { err := readDefFile(uiData, reader, reader.RelativePath(it)) if err != nil { assert.Error(t, fmt.Errorf("Something wrong.")) } } // verify assert.True(t, len(uiData.Definitions) == 1) } // Test readDefFile only accept .cue func TestReadViewFile(t *testing.T) { // setup test data testAddonName := "test-view" testAddonDir := fmt.Sprintf("./testdata/%s", testAddonName) reader := localReader{dir: testAddonDir, name: testAddonName} metas, err := reader.ListAddonMeta() testAddonMeta := metas[testAddonName] assert.NoError(t, err) // run test var addon = &InstallPackage{} ptItems := ClassifyItemByPattern(&testAddonMeta, reader) items := ptItems[ViewDirName] for _, it := range items { err := readViewFile(addon, reader, reader.RelativePath(it)) if err != nil { assert.NoError(t, err) } } notExistErr := readViewFile(addon, reader, "not-exist.cue") assert.Error(t, notExistErr) // verify assert.True(t, len(addon.CUEViews) == 1) assert.True(t, len(addon.YAMLViews) == 1) } func TestRenderCUETemplate(t *testing.T) { fileDate, err := os.ReadFile("./testdata/example/resources/configmap.cue") assert.NoError(t, err) addon := &InstallPackage{ Meta: Meta{ Version: "1.0.1", }, Parameters: "{\"example\": \"\"}", } component, err := renderCompAccordingCUETemplate(ElementFile{Data: string(fileDate), Name: "configmap.cue"}, addon, map[string]interface{}{ "example": "render", }) assert.NoError(t, err) assert.True(t, component.Type == "raw") var config = make(map[string]interface{}) err = json.Unmarshal(component.Properties.Raw, &config) assert.NoError(t, err) assert.True(t, component.Type == "raw") assert.True(t, config["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["version"] == "1.0.1") } func TestCheckEnableAddonErrorWhenMissMatch(t *testing.T) { version2.VelaVersion = "v1.3.0" i := InstallPackage{Meta: Meta{SystemRequirements: &SystemRequirements{VelaVersion: ">=1.4.0"}}} installer := &Installer{} err := installer.enableAddon(&i) assert.Equal(t, errors.As(err, &VersionUnMatchError{}), true) } func TestPackageAddon(t *testing.T) { pwd, _ := os.Getwd() validAddonDict := "./testdata/example-legacy" archiver, err := PackageAddon(validAddonDict) assert.NoError(t, err) assert.Equal(t, filepath.Join(pwd, "example-legacy-1.0.1.tgz"), archiver) // Remove generated package after tests defer func() { _ = os.RemoveAll(filepath.Join(pwd, "example-legacy-1.0.1.tgz")) }() invalidAddonDict := "./testdata" archiver, err = PackageAddon(invalidAddonDict) assert.NotNil(t, err) assert.Equal(t, "", archiver) invalidAddonMetadata := "./testdata/invalid-metadata" archiver, err = PackageAddon(invalidAddonMetadata) assert.NotNil(t, err) assert.Equal(t, "", archiver) } func TestGenerateAnnotation(t *testing.T) { meta := Meta{SystemRequirements: &SystemRequirements{ VelaVersion: ">1.4.0", KubernetesVersion: ">1.20.0", }} res := generateAnnotation(&meta) assert.Equal(t, res[velaSystemRequirement], ">1.4.0") assert.Equal(t, res[kubernetesSystemRequirement], ">1.20.0") meta = Meta{} meta.SystemRequirements = &SystemRequirements{KubernetesVersion: ">=1.20.1"} res = generateAnnotation(&meta) assert.Equal(t, res[velaSystemRequirement], "") assert.Equal(t, res[kubernetesSystemRequirement], ">=1.20.1") } func TestMergeAddonInstallArgs(t *testing.T) { k8sClient := fake.NewClientBuilder().Build() ctx := context.Background() testcases := []struct { name string legacyArgs string args map[string]interface{} mergedArgs string application string err error }{ { name: "addon1", legacyArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", args: map[string]interface{}{ "serviceType": "NodePort", }, mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", }, { name: "addon2", legacyArgs: "{\"clusters\":[\"\"]}", args: map[string]interface{}{ "repo": "hub.vela.com", "serviceType": "NodePort", "imagePullSecrets": []string{"test-hub"}, }, mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", }, { name: "addon3", legacyArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", args: map[string]interface{}{ "imagePullSecrets": []string{"test-hub-2"}, }, mergedArgs: "{\"clusters\":[\"\"],\"imagePullSecrets\":[\"test-hub-2\"],\"repo\":\"hub.vela.com\",\"serviceType\":\"NodePort\"}", }, { // merge nested parameters name: "addon4", legacyArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", args: map[string]interface{}{ "p1": map[string]interface{}{ "p12": "p12-v2", "p13": "p13-v1", }, }, mergedArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v2\",\"p13\":\"p13-v1\"}}", }, { // there is not legacyArgs name: "addon5", legacyArgs: "", args: map[string]interface{}{ "p1": map[string]interface{}{ "p12": "p12-v2", "p13": "p13-v1", }, }, mergedArgs: "{\"p1\":{\"p12\":\"p12-v2\",\"p13\":\"p13-v1\"}}", }, { // there is not new args name: "addon6", legacyArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", args: nil, mergedArgs: "{\"clusters\":[\"\"],\"p1\":{\"p11\":\"p11-v1\",\"p12\":\"p12-v1\"}}", }, } for _, tc := range testcases { t.Run("", func(t *testing.T) { if len(tc.legacyArgs) != 0 { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: addonutil.Addon2SecName(tc.name), Namespace: types.DefaultKubeVelaNS, }, Data: map[string][]byte{ AddonParameterDataKey: []byte(tc.legacyArgs), }, } err := k8sClient.Create(ctx, secret) assert.NoError(t, err) } addonArgs, err := MergeAddonInstallArgs(ctx, k8sClient, tc.name, tc.args) assert.NoError(t, err) args, err := json.Marshal(addonArgs) assert.NoError(t, err) assert.Equal(t, tc.mergedArgs, string(args), tc.name) }) } } func TestGenerateConflictError(t *testing.T) { confictAddon := map[string]string{ "helm": "definition: helm already exist and not belong to any addon \n", "kustomize": "definition: kustomize in this addon already exist in fluxcd \n", } err := produceDefConflictError(confictAddon) assert.Error(t, err) strings.Contains(err.Error(), "in this addon already exist in fluxcd") assert.NoError(t, produceDefConflictError(map[string]string{})) }