/* 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 ( "fmt" "os" "path/filepath" "reflect" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" ) var _ = Describe("Test definition check", func() { var compDef v1beta1.ComponentDefinition var traitDef v1beta1.TraitDefinition var wfStepDef v1beta1.WorkflowStepDefinition BeforeEach(func() { compDef = v1beta1.ComponentDefinition{} traitDef = v1beta1.TraitDefinition{} wfStepDef = v1beta1.WorkflowStepDefinition{} Expect(yaml.Unmarshal([]byte(compDefYaml), &compDef)).Should(BeNil()) Expect(k8sClient.Create(ctx, &compDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) Expect(yaml.Unmarshal([]byte(traitDefYaml), &traitDef)).Should(BeNil()) Expect(k8sClient.Create(ctx, &traitDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) Expect(yaml.Unmarshal([]byte(wfStepDefYaml), &wfStepDef)).Should(BeNil()) Expect(k8sClient.Create(ctx, &wfStepDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) }) It("Test pass def to app annotation", func() { c := v1beta1.ComponentDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "ComponentDefinition"}} c.SetName("my-comp") t := v1beta1.TraitDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "TraitDefinition"}} t.SetName("my-trait") w := v1beta1.WorkflowStepDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "WorkflowStepDefinition"}} w.SetName("my-wfstep") var defs []*unstructured.Unstructured cDef, err := util.Object2Unstructured(c) Expect(err).Should(BeNil()) defs = append(defs, cDef) tDef, err := util.Object2Unstructured(t) defs = append(defs, tDef) Expect(err).Should(BeNil()) wDef, err := util.Object2Unstructured(w) Expect(err).Should(BeNil()) defs = append(defs, wDef) addonApp := v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "addon-app", Namespace: velatypes.DefaultKubeVelaNS}} err = passDefInAppAnnotation(defs, &addonApp) Expect(err).Should(BeNil()) anno := addonApp.GetAnnotations() Expect(len(anno)).Should(BeEquivalentTo(3)) Expect(anno[compDefAnnotation]).Should(BeEquivalentTo("my-comp")) Expect(anno[traitDefAnnotation]).Should(BeEquivalentTo("my-trait")) Expect(anno[workflowStepDefAnnotation]).Should(BeEquivalentTo("my-wfstep")) }) It("Test checkAddonHasBeenUsed func", func() { addonApp := v1beta1.Application{} Expect(yaml.Unmarshal([]byte(addonAppYaml), &addonApp)).Should(BeNil()) app1 := v1beta1.Application{} Expect(yaml.Unmarshal([]byte(testApp1Yaml), &app1)).Should(BeNil()) Expect(k8sClient.Create(ctx, &app1)).Should(BeNil()) app2 := v1beta1.Application{} Expect(yaml.Unmarshal([]byte(testApp2Yaml), &app2)).Should(BeNil()) Expect(k8sClient.Create(ctx, &app2)).Should(BeNil()) Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-ns"}})) app3 := v1beta1.Application{} Expect(yaml.Unmarshal([]byte(testApp3Yaml), &app3)).Should(BeNil()) Expect(k8sClient.Create(ctx, &app3)).Should(BeNil()) app4 := v1beta1.Application{} Expect(yaml.Unmarshal([]byte(testApp4Yaml), &app4)).Should(BeNil()) Expect(k8sClient.Create(ctx, &app4)).Should(BeNil()) usedApps, err := checkAddonHasBeenUsed(ctx, k8sClient, "my-addon", addonApp, cfg) Expect(err).Should(BeNil()) Expect(len(usedApps)).Should(BeEquivalentTo(4)) }) }) func TestMerge2Map(t *testing.T) { res := make(map[string]bool) merge2DefMap(compDefAnnotation, "my-comp1,my-comp2", res) merge2DefMap(traitDefAnnotation, "my-trait1,my-trait2", res) merge2DefMap(workflowStepDefAnnotation, "my-wfStep1,my-wfStep2", res) assert.Equal(t, 6, len(res)) } func TestUsingAddonInfo(t *testing.T) { apps := []v1beta1.Application{ {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-3"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-3", Name: "app-3"}}, } res := appsDependsOnAddonErrInfo(apps) assert.Contains(t, res, "and other 1 more applications. Please delete all of them before removing.") apps = []v1beta1.Application{ {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-3"}}, } res = appsDependsOnAddonErrInfo(apps) assert.Contains(t, res, "Please delete all of them before removing.") apps = []v1beta1.Application{ {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}}, } res = appsDependsOnAddonErrInfo(apps) assert.Contains(t, res, "this addon is being used by: namespace-1/app-1 applications. Please delete all of them before removing.") apps = []v1beta1.Application{ {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}}, {ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}}, } res = appsDependsOnAddonErrInfo(apps) assert.Contains(t, res, ". Please delete all of them before removing.") } func TestIsAddonDir(t *testing.T) { var isAddonDir bool var err error var meta *Meta var metaYaml []byte // Non-existent dir isAddonDir, err = IsAddonDir("non-existent-dir") assert.Equal(t, isAddonDir, false) assert.Error(t, err) // Not a directory (a file) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "local", "metadata.yaml")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "not a directory") // No metadata.yaml isAddonDir, err = IsAddonDir(".") assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "exists in directory") // Empty metadata.yaml err = os.MkdirAll(filepath.Join("testdata", "testaddon"), 0700) assert.NoError(t, err) defer func() { os.RemoveAll(filepath.Join("testdata", "testaddon")) }() err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), []byte{}, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "missing") // Empty addon name meta = &Meta{} metaYaml, err = yaml.Marshal(meta) assert.NoError(t, err) err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "addon name is empty") // Empty addon version meta = &Meta{ Name: "name", } metaYaml, err = yaml.Marshal(meta) assert.NoError(t, err) err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "addon version is empty") // No metadata.yaml meta = &Meta{ Name: "name", Version: "1.0.0", } metaYaml, err = yaml.Marshal(meta) assert.NoError(t, err) err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "exists in directory") // Empty template.yaml err = os.WriteFile(filepath.Join("testdata", "testaddon", TemplateFileName), []byte{}, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), "missing") // Empty template.cue err = os.WriteFile(filepath.Join("testdata", "testaddon", AppTemplateCueFileName), []byte{}, 0644) assert.NoError(t, err) isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon")) assert.Equal(t, isAddonDir, false) assert.Contains(t, err.Error(), renderOutputCuePath) // Pass all checks cmd := InitCmd{ Path: filepath.Join("testdata", "testaddon2"), AddonName: "testaddon2", } err = cmd.CreateScaffold() assert.NoError(t, err) defer func() { _ = os.RemoveAll(filepath.Join("testdata", "testaddon2")) }() isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon2")) assert.Equal(t, isAddonDir, true) assert.NoError(t, err) } func TestMakeChart(t *testing.T) { var err error // Not a addon dir err = MakeChartCompatible(".", true) assert.Contains(t, err.Error(), "not an addon dir") // Valid addon dir cmd := InitCmd{ Path: filepath.Join("testdata", "testaddon"), AddonName: "testaddon", } err = cmd.CreateScaffold() assert.NoError(t, err) defer func() { _ = os.RemoveAll(filepath.Join("testdata", "testaddon")) }() err = MakeChartCompatible(filepath.Join("testdata", "testaddon"), true) assert.NoError(t, err) isChartDir, err := chartutil.IsChartDir(filepath.Join("testdata", "testaddon")) assert.NoError(t, err) assert.Equal(t, isChartDir, true) // Already a chart dir err = MakeChartCompatible(filepath.Join("testdata", "testaddon"), false) assert.NoError(t, err) isChartDir, err = chartutil.IsChartDir(filepath.Join("testdata", "testaddon")) assert.NoError(t, err) assert.Equal(t, isChartDir, true) } func TestCheckObjectBindingComponent(t *testing.T) { existingBindingDef := unstructured.Unstructured{} existingBindingDef.SetAnnotations(map[string]string{oam.AnnotationAddonDefinitionBondCompKey: "kustomize"}) emptyAnnoDef := unstructured.Unstructured{} emptyAnnoDef.SetAnnotations(map[string]string{"test": "onlyForTest"}) legacyAnnoDef := unstructured.Unstructured{} legacyAnnoDef.SetAnnotations(map[string]string{oam.AnnotationIgnoreWithoutCompKey: "kustomize"}) testCases := map[string]struct { object unstructured.Unstructured app v1beta1.Application res bool }{ "bindingExist": {object: existingBindingDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}}, res: true}, "NotExisting": {object: existingBindingDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "helm"}}}}, res: false}, "NoBidingAnnotation": {object: emptyAnnoDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}}, res: true}, "EmptyApp": {object: existingBindingDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{}}}, res: false}, "LegacyApp": {object: legacyAnnoDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}}, res: true, }, "LegacyAppWithoutComp": {object: legacyAnnoDef, app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{}}}}, res: false, }, } for _, s := range testCases { result := checkBondComponentExist(s.object, s.app) assert.Equal(t, result, s.res) } } func TestFilterDependencyRegistries(t *testing.T) { testCases := []struct { registries []Registry index int res []Registry origin []Registry }{ { registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, index: 0, res: []Registry{{Name: "r2"}, {Name: "r3"}}, origin: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, }, { registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, index: 1, res: []Registry{{Name: "r1"}, {Name: "r3"}}, origin: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, }, { registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, index: 2, res: []Registry{{Name: "r1"}, {Name: "r2"}}, origin: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, }, { registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, index: 3, res: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, origin: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, }, { registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, index: -1, res: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, origin: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}}, }, { registries: []Registry{}, index: 0, res: []Registry{}, origin: []Registry{}, }, } for _, testCase := range testCases { res := FilterDependencyRegistries(testCase.index, testCase.registries) assert.Equal(t, res, testCase.res) assert.Equal(t, testCase.registries, testCase.origin) } } func TestCheckAddonPackageValid(t *testing.T) { testCases := []struct { testCase Meta err error }{{ testCase: Meta{}, err: fmt.Errorf("the addon package doesn't have `metadata.yaml`"), }, { testCase: Meta{Version: "v1.4.0"}, err: fmt.Errorf("`matadata.yaml` must define the name of addon"), }, { testCase: Meta{Name: "test-addon"}, err: fmt.Errorf("`matadata.yaml` must define the version of addon"), }, { testCase: Meta{Name: "test-addon", Version: "1.4.5"}, err: nil, }, } for _, testCase := range testCases { err := validateAddonPackage(&InstallPackage{Meta: testCase.testCase}) assert.Equal(t, reflect.DeepEqual(err, testCase.err), true) } } func TestIsRegistryFuncs(t *testing.T) { t.Run("IsLocalRegistry", func(t *testing.T) { assert.True(t, IsLocalRegistry(Registry{Name: "local"})) assert.False(t, IsLocalRegistry(Registry{Name: "KubeVela"})) }) t.Run("IsVersionRegistry", func(t *testing.T) { assert.True(t, IsVersionRegistry(Registry{Helm: &HelmSource{}})) assert.False(t, IsVersionRegistry(Registry{Git: &GitAddonSource{}})) assert.False(t, IsVersionRegistry(Registry{})) }) } func TestInstallOptions(t *testing.T) { t.Run("SkipValidateVersion", func(t *testing.T) { installer := &Installer{} SkipValidateVersion(installer) assert.True(t, installer.skipVersionValidate) }) t.Run("DryRunAddon", func(t *testing.T) { installer := &Installer{} DryRunAddon(installer) assert.True(t, installer.dryRun) }) t.Run("OverrideDefinitions", func(t *testing.T) { installer := &Installer{} OverrideDefinitions(installer) assert.True(t, installer.overrideDefs) }) } func TestProduceDefConflictError(t *testing.T) { t.Run("no conflicts", func(t *testing.T) { err := produceDefConflictError(map[string]string{}) assert.NoError(t, err) }) t.Run("with conflicts", func(t *testing.T) { conflicts := map[string]string{ "def1": "error message 1", "def2": "error message 2", } err := produceDefConflictError(conflicts) assert.Error(t, err) assert.Contains(t, err.Error(), "error message 1") assert.Contains(t, err.Error(), "error message 2") assert.Contains(t, err.Error(), "--override-definitions") }) } func TestGenerateChartMetadata(t *testing.T) { addonDir := t.TempDir() // Corrected YAML content with no leading whitespace metaFileContent := `name: my-addon version: 1.2.3 description: my addon description icon: http://my-icon.com url: http://my-home.com tags: - tag1 - tag2 system: vela: ">=1.5.0" kubernetes: "1.20.0" ` err := os.WriteFile(filepath.Join(addonDir, MetadataFileName), []byte(metaFileContent), 0644) assert.NoError(t, err) chartMeta, err := generateChartMetadata(addonDir) assert.NoError(t, err) assert.NotNil(t, chartMeta) assert.Equal(t, "my-addon", chartMeta.Name) assert.Equal(t, "1.2.3", chartMeta.Version) assert.Equal(t, "my addon description", chartMeta.Description) assert.Equal(t, "library", chartMeta.Type) assert.Equal(t, chart.APIVersionV2, chartMeta.APIVersion) assert.Equal(t, "http://my-icon.com", chartMeta.Icon) assert.Equal(t, "http://my-home.com", chartMeta.Home) assert.Equal(t, []string{"tag1", "tag2"}, chartMeta.Keywords) assert.Equal(t, ">=1.5.0", chartMeta.Annotations[velaSystemRequirement]) assert.Equal(t, "1.20.0", chartMeta.Annotations[kubernetesSystemRequirement]) assert.Equal(t, "my-addon", chartMeta.Annotations[addonSystemRequirement]) } const ( compDefYaml = ` apiVersion: core.oam.dev/v1beta1 kind: ComponentDefinition metadata: name: my-comp namespace: vela-system ` traitDefYaml = ` apiVersion: core.oam.dev/v1beta1 kind: TraitDefinition metadata: name: my-trait namespace: vela-system ` wfStepDefYaml = ` apiVersion: core.oam.dev/v1beta1 kind: WorkflowStepDefinition metadata: name: my-wfstep namespace: vela-system ` ) const ( addonAppYaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: labels: addons.oam.dev/name: myaddon addons.oam.dev/registry: KubeVela annotations: addon.oam.dev/componentDefinitions: "my-comp" addon.oam.dev/traitDefinitions: "my-trait" addon.oam.dev/workflowStepDefinitions: "my-wfstep" addon.oam.dev/policyDefinitions: "my-policy" name: addon-myaddon namespace: vela-system spec: ` testApp1Yaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: labels: name: app-1 namespace: default spec: components: - name: comp1 type: my-comp traits: - type: my-trait ` testApp2Yaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: labels: name: app-2 namespace: default spec: components: - name: comp2 type: webservice traits: - type: my-trait ` testApp3Yaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: app-3 namespace: test-ns spec: components: - name: podinfo type: webservice workflow: steps: - type: my-wfstep name: deploy ` testApp4Yaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: app-4 namespace: test-ns spec: components: - name: podinfo type: webservice policies: - type: my-policy name: topology ` registryCmYaml = ` apiVersion: v1 data: registries: '{ "KubeVela":{ "name": "KubeVela", "oss": { "end_point": "TEST_SERVER_URL", "bucket": "", "path": "" } } }' kind: ConfigMap metadata: name: vela-addon-registry namespace: vela-system ` addonDisableTestAppYaml = ` apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: addon-test-disable-addon namespace: vela-system labels: addons.oam.dev/name: test-disable-addon addons.oam.dev/registry: KubeVela spec: components: - name: podinfo type: webservice ` )