github.com/oam-dev/kubevela@v1.9.11/pkg/addon/utils_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  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"testing"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/stretchr/testify/assert"
    29  	"helm.sh/helm/v3/pkg/chartutil"
    30  	v1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    36  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    37  	velatypes "github.com/oam-dev/kubevela/apis/types"
    38  	"github.com/oam-dev/kubevela/pkg/oam"
    39  	"github.com/oam-dev/kubevela/pkg/oam/util"
    40  )
    41  
    42  var _ = Describe("Test definition check", func() {
    43  	var compDef v1beta1.ComponentDefinition
    44  	var traitDef v1beta1.TraitDefinition
    45  	var wfStepDef v1beta1.WorkflowStepDefinition
    46  
    47  	BeforeEach(func() {
    48  		compDef = v1beta1.ComponentDefinition{}
    49  		traitDef = v1beta1.TraitDefinition{}
    50  		wfStepDef = v1beta1.WorkflowStepDefinition{}
    51  
    52  		Expect(yaml.Unmarshal([]byte(compDefYaml), &compDef)).Should(BeNil())
    53  		Expect(k8sClient.Create(ctx, &compDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
    54  
    55  		Expect(yaml.Unmarshal([]byte(traitDefYaml), &traitDef)).Should(BeNil())
    56  		Expect(k8sClient.Create(ctx, &traitDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
    57  
    58  		Expect(yaml.Unmarshal([]byte(wfStepDefYaml), &wfStepDef)).Should(BeNil())
    59  		Expect(k8sClient.Create(ctx, &wfStepDef)).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
    60  	})
    61  
    62  	It("Test pass def to app annotation", func() {
    63  		c := v1beta1.ComponentDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "ComponentDefinition"}}
    64  		c.SetName("my-comp")
    65  
    66  		t := v1beta1.TraitDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "TraitDefinition"}}
    67  		t.SetName("my-trait")
    68  
    69  		w := v1beta1.WorkflowStepDefinition{TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "WorkflowStepDefinition"}}
    70  		w.SetName("my-wfstep")
    71  
    72  		var defs []*unstructured.Unstructured
    73  		cDef, err := util.Object2Unstructured(c)
    74  		Expect(err).Should(BeNil())
    75  		defs = append(defs, cDef)
    76  		tDef, err := util.Object2Unstructured(t)
    77  		defs = append(defs, tDef)
    78  		Expect(err).Should(BeNil())
    79  		wDef, err := util.Object2Unstructured(w)
    80  		Expect(err).Should(BeNil())
    81  		defs = append(defs, wDef)
    82  
    83  		addonApp := v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "addon-app", Namespace: velatypes.DefaultKubeVelaNS}}
    84  		err = passDefInAppAnnotation(defs, &addonApp)
    85  		Expect(err).Should(BeNil())
    86  
    87  		anno := addonApp.GetAnnotations()
    88  		Expect(len(anno)).Should(BeEquivalentTo(3))
    89  		Expect(anno[compDefAnnotation]).Should(BeEquivalentTo("my-comp"))
    90  		Expect(anno[traitDefAnnotation]).Should(BeEquivalentTo("my-trait"))
    91  		Expect(anno[workflowStepDefAnnotation]).Should(BeEquivalentTo("my-wfstep"))
    92  	})
    93  
    94  	It("Test checkAddonHasBeenUsed func", func() {
    95  		addonApp := v1beta1.Application{}
    96  		Expect(yaml.Unmarshal([]byte(addonAppYaml), &addonApp)).Should(BeNil())
    97  
    98  		app1 := v1beta1.Application{}
    99  		Expect(yaml.Unmarshal([]byte(testApp1Yaml), &app1)).Should(BeNil())
   100  		Expect(k8sClient.Create(ctx, &app1)).Should(BeNil())
   101  
   102  		app2 := v1beta1.Application{}
   103  		Expect(yaml.Unmarshal([]byte(testApp2Yaml), &app2)).Should(BeNil())
   104  		Expect(k8sClient.Create(ctx, &app2)).Should(BeNil())
   105  
   106  		Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-ns"}}))
   107  		app3 := v1beta1.Application{}
   108  		Expect(yaml.Unmarshal([]byte(testApp3Yaml), &app3)).Should(BeNil())
   109  		Expect(k8sClient.Create(ctx, &app3)).Should(BeNil())
   110  
   111  		app4 := v1beta1.Application{}
   112  		Expect(yaml.Unmarshal([]byte(testApp4Yaml), &app4)).Should(BeNil())
   113  		Expect(k8sClient.Create(ctx, &app4)).Should(BeNil())
   114  
   115  		usedApps, err := checkAddonHasBeenUsed(ctx, k8sClient, "my-addon", addonApp, cfg)
   116  		Expect(err).Should(BeNil())
   117  		Expect(len(usedApps)).Should(BeEquivalentTo(4))
   118  	})
   119  })
   120  
   121  func TestMerge2Map(t *testing.T) {
   122  	res := make(map[string]bool)
   123  	merge2DefMap(compDefAnnotation, "my-comp1,my-comp2", res)
   124  	merge2DefMap(traitDefAnnotation, "my-trait1,my-trait2", res)
   125  	merge2DefMap(workflowStepDefAnnotation, "my-wfStep1,my-wfStep2", res)
   126  	assert.Equal(t, 6, len(res))
   127  }
   128  
   129  func TestUsingAddonInfo(t *testing.T) {
   130  	apps := []v1beta1.Application{
   131  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}},
   132  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}},
   133  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-3"}},
   134  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-3", Name: "app-3"}},
   135  	}
   136  	res := appsDependsOnAddonErrInfo(apps)
   137  	assert.Contains(t, res, "and other 1 more applications. Please delete all of them before removing.")
   138  
   139  	apps = []v1beta1.Application{
   140  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}},
   141  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}},
   142  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-3"}},
   143  	}
   144  	res = appsDependsOnAddonErrInfo(apps)
   145  	assert.Contains(t, res, "Please delete all of them before removing.")
   146  
   147  	apps = []v1beta1.Application{
   148  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}},
   149  	}
   150  	res = appsDependsOnAddonErrInfo(apps)
   151  	assert.Contains(t, res, "this addon is being used by: namespace-1/app-1 applications. Please delete all of them before removing.")
   152  
   153  	apps = []v1beta1.Application{
   154  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-1", Name: "app-1"}},
   155  		{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace-2", Name: "app-2"}},
   156  	}
   157  	res = appsDependsOnAddonErrInfo(apps)
   158  	assert.Contains(t, res, ". Please delete all of them before removing.")
   159  }
   160  
   161  func TestIsAddonDir(t *testing.T) {
   162  	var isAddonDir bool
   163  	var err error
   164  	var meta *Meta
   165  	var metaYaml []byte
   166  
   167  	// Non-existent dir
   168  	isAddonDir, err = IsAddonDir("non-existent-dir")
   169  	assert.Equal(t, isAddonDir, false)
   170  	assert.Error(t, err)
   171  
   172  	// Not a directory (a file)
   173  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "local", "metadata.yaml"))
   174  	assert.Equal(t, isAddonDir, false)
   175  	assert.Contains(t, err.Error(), "not a directory")
   176  
   177  	// No metadata.yaml
   178  	isAddonDir, err = IsAddonDir(".")
   179  	assert.Equal(t, isAddonDir, false)
   180  	assert.Contains(t, err.Error(), "exists in directory")
   181  
   182  	// Empty metadata.yaml
   183  	err = os.MkdirAll(filepath.Join("testdata", "testaddon"), 0700)
   184  	assert.NoError(t, err)
   185  	defer func() {
   186  		os.RemoveAll(filepath.Join("testdata", "testaddon"))
   187  	}()
   188  	err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), []byte{}, 0644)
   189  	assert.NoError(t, err)
   190  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   191  	assert.Equal(t, isAddonDir, false)
   192  	assert.Contains(t, err.Error(), "missing")
   193  
   194  	// Empty addon name
   195  	meta = &Meta{}
   196  	metaYaml, err = yaml.Marshal(meta)
   197  	assert.NoError(t, err)
   198  	err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644)
   199  	assert.NoError(t, err)
   200  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   201  	assert.Equal(t, isAddonDir, false)
   202  	assert.Contains(t, err.Error(), "addon name is empty")
   203  
   204  	// Empty addon version
   205  	meta = &Meta{
   206  		Name: "name",
   207  	}
   208  	metaYaml, err = yaml.Marshal(meta)
   209  	assert.NoError(t, err)
   210  	err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644)
   211  	assert.NoError(t, err)
   212  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   213  	assert.Equal(t, isAddonDir, false)
   214  	assert.Contains(t, err.Error(), "addon version is empty")
   215  
   216  	// No metadata.yaml
   217  	meta = &Meta{
   218  		Name:    "name",
   219  		Version: "1.0.0",
   220  	}
   221  	metaYaml, err = yaml.Marshal(meta)
   222  	assert.NoError(t, err)
   223  	err = os.WriteFile(filepath.Join("testdata", "testaddon", MetadataFileName), metaYaml, 0644)
   224  	assert.NoError(t, err)
   225  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   226  	assert.Equal(t, isAddonDir, false)
   227  	assert.Contains(t, err.Error(), "exists in directory")
   228  
   229  	// Empty template.yaml
   230  	err = os.WriteFile(filepath.Join("testdata", "testaddon", TemplateFileName), []byte{}, 0644)
   231  	assert.NoError(t, err)
   232  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   233  	assert.Equal(t, isAddonDir, false)
   234  	assert.Contains(t, err.Error(), "missing")
   235  
   236  	// Empty template.cue
   237  	err = os.WriteFile(filepath.Join("testdata", "testaddon", AppTemplateCueFileName), []byte{}, 0644)
   238  	assert.NoError(t, err)
   239  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon"))
   240  	assert.Equal(t, isAddonDir, false)
   241  	assert.Contains(t, err.Error(), renderOutputCuePath)
   242  
   243  	// Pass all checks
   244  	cmd := InitCmd{
   245  		Path:      filepath.Join("testdata", "testaddon2"),
   246  		AddonName: "testaddon2",
   247  	}
   248  	err = cmd.CreateScaffold()
   249  	assert.NoError(t, err)
   250  	defer func() {
   251  		_ = os.RemoveAll(filepath.Join("testdata", "testaddon2"))
   252  	}()
   253  	isAddonDir, err = IsAddonDir(filepath.Join("testdata", "testaddon2"))
   254  	assert.Equal(t, isAddonDir, true)
   255  	assert.NoError(t, err)
   256  }
   257  
   258  func TestMakeChart(t *testing.T) {
   259  	var err error
   260  
   261  	// Not a addon dir
   262  	err = MakeChartCompatible(".", true)
   263  	assert.Contains(t, err.Error(), "not an addon dir")
   264  
   265  	// Valid addon dir
   266  	cmd := InitCmd{
   267  		Path:      filepath.Join("testdata", "testaddon"),
   268  		AddonName: "testaddon",
   269  	}
   270  	err = cmd.CreateScaffold()
   271  	assert.NoError(t, err)
   272  	defer func() {
   273  		_ = os.RemoveAll(filepath.Join("testdata", "testaddon"))
   274  	}()
   275  	err = MakeChartCompatible(filepath.Join("testdata", "testaddon"), true)
   276  	assert.NoError(t, err)
   277  	isChartDir, err := chartutil.IsChartDir(filepath.Join("testdata", "testaddon"))
   278  	assert.NoError(t, err)
   279  	assert.Equal(t, isChartDir, true)
   280  
   281  	// Already a chart dir
   282  	err = MakeChartCompatible(filepath.Join("testdata", "testaddon"), false)
   283  	assert.NoError(t, err)
   284  	isChartDir, err = chartutil.IsChartDir(filepath.Join("testdata", "testaddon"))
   285  	assert.NoError(t, err)
   286  	assert.Equal(t, isChartDir, true)
   287  }
   288  
   289  func TestCheckObjectBindingComponent(t *testing.T) {
   290  	existingBindingDef := unstructured.Unstructured{}
   291  	existingBindingDef.SetAnnotations(map[string]string{oam.AnnotationAddonDefinitionBondCompKey: "kustomize"})
   292  
   293  	emptyAnnoDef := unstructured.Unstructured{}
   294  	emptyAnnoDef.SetAnnotations(map[string]string{"test": "onlyForTest"})
   295  
   296  	legacyAnnoDef := unstructured.Unstructured{}
   297  	legacyAnnoDef.SetAnnotations(map[string]string{oam.AnnotationIgnoreWithoutCompKey: "kustomize"})
   298  	testCases := map[string]struct {
   299  		object unstructured.Unstructured
   300  		app    v1beta1.Application
   301  		res    bool
   302  	}{
   303  		"bindingExist": {object: existingBindingDef,
   304  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}},
   305  			res: true},
   306  		"NotExisting": {object: existingBindingDef,
   307  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "helm"}}}},
   308  			res: false},
   309  		"NoBidingAnnotation": {object: emptyAnnoDef,
   310  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}},
   311  			res: true},
   312  		"EmptyApp": {object: existingBindingDef,
   313  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{}}},
   314  			res: false},
   315  		"LegacyApp": {object: legacyAnnoDef,
   316  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{Name: "kustomize"}}}},
   317  			res: true,
   318  		},
   319  		"LegacyAppWithoutComp": {object: legacyAnnoDef,
   320  			app: v1beta1.Application{Spec: v1beta1.ApplicationSpec{Components: []common.ApplicationComponent{{}}}},
   321  			res: false,
   322  		},
   323  	}
   324  	for _, s := range testCases {
   325  		result := checkBondComponentExist(s.object, s.app)
   326  		assert.Equal(t, result, s.res)
   327  	}
   328  }
   329  
   330  func TestFilterDependencyRegistries(t *testing.T) {
   331  	testCases := []struct {
   332  		registries []Registry
   333  		index      int
   334  		res        []Registry
   335  		origin     []Registry
   336  	}{
   337  		{
   338  			registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   339  			index:      0,
   340  			res:        []Registry{{Name: "r2"}, {Name: "r3"}},
   341  			origin:     []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   342  		},
   343  		{
   344  			registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   345  			index:      1,
   346  			res:        []Registry{{Name: "r1"}, {Name: "r3"}},
   347  			origin:     []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   348  		},
   349  		{
   350  			registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   351  			index:      2,
   352  			res:        []Registry{{Name: "r1"}, {Name: "r2"}},
   353  			origin:     []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   354  		},
   355  		{
   356  			registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   357  			index:      3,
   358  			res:        []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   359  			origin:     []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   360  		},
   361  		{
   362  			registries: []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   363  			index:      -1,
   364  			res:        []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   365  			origin:     []Registry{{Name: "r1"}, {Name: "r2"}, {Name: "r3"}},
   366  		},
   367  		{
   368  			registries: []Registry{},
   369  			index:      0,
   370  			res:        []Registry{},
   371  			origin:     []Registry{},
   372  		},
   373  	}
   374  	for _, testCase := range testCases {
   375  		res := FilterDependencyRegistries(testCase.index, testCase.registries)
   376  		assert.Equal(t, res, testCase.res)
   377  		assert.Equal(t, testCase.registries, testCase.origin)
   378  	}
   379  }
   380  
   381  func TestCheckAddonPackageValid(t *testing.T) {
   382  	testCases := []struct {
   383  		testCase Meta
   384  		err      error
   385  	}{{
   386  		testCase: Meta{},
   387  		err:      fmt.Errorf("the addon package doesn't have `metadata.yaml`"),
   388  	}, {
   389  		testCase: Meta{Version: "v1.4.0"},
   390  		err:      fmt.Errorf("`matadata.yaml` must define the name of addon"),
   391  	}, {
   392  		testCase: Meta{Name: "test-addon"},
   393  		err:      fmt.Errorf("`matadata.yaml` must define the version of addon"),
   394  	}, {
   395  		testCase: Meta{Name: "test-addon", Version: "1.4.5"},
   396  		err:      nil,
   397  	},
   398  	}
   399  	for _, testCase := range testCases {
   400  		err := validateAddonPackage(&InstallPackage{Meta: testCase.testCase})
   401  		assert.Equal(t, reflect.DeepEqual(err, testCase.err), true)
   402  	}
   403  }
   404  
   405  const (
   406  	compDefYaml = `
   407  apiVersion: core.oam.dev/v1beta1
   408  kind: ComponentDefinition
   409  metadata:
   410     name: my-comp
   411     namespace: vela-system
   412  `
   413  	traitDefYaml = `
   414  apiVersion: core.oam.dev/v1beta1
   415  kind: TraitDefinition
   416  metadata:
   417     name: my-trait
   418     namespace: vela-system
   419  `
   420  	wfStepDefYaml = `
   421  apiVersion: core.oam.dev/v1beta1
   422  kind: WorkflowStepDefinition
   423  metadata:
   424     name: my-wfstep
   425     namespace: vela-system
   426  `
   427  )
   428  
   429  const (
   430  	addonAppYaml = `
   431  apiVersion: core.oam.dev/v1beta1
   432  kind: Application
   433  metadata:
   434    labels:
   435      addons.oam.dev/name: myaddon
   436      addons.oam.dev/registry: KubeVela
   437    annotations:
   438      addon.oam.dev/componentDefinitions: "my-comp"
   439      addon.oam.dev/traitDefinitions: "my-trait"
   440      addon.oam.dev/workflowStepDefinitions: "my-wfstep"
   441      addon.oam.dev/policyDefinitions: "my-policy"
   442    name: addon-myaddon
   443    namespace: vela-system
   444  spec:
   445  `
   446  	testApp1Yaml = `
   447  apiVersion: core.oam.dev/v1beta1
   448  kind: Application
   449  metadata:
   450    labels:
   451    name: app-1
   452    namespace: default
   453  spec:
   454    components:
   455       - name: comp1
   456         type: my-comp
   457         traits:
   458         - type: my-trait
   459  `
   460  	testApp2Yaml = `
   461  apiVersion: core.oam.dev/v1beta1
   462  kind: Application
   463  metadata:
   464    labels:
   465    name: app-2
   466    namespace: default
   467  spec:
   468    components:
   469       - name: comp2
   470         type: webservice
   471         traits:
   472         - type: my-trait
   473  `
   474  	testApp3Yaml = `
   475  apiVersion: core.oam.dev/v1beta1
   476  kind: Application
   477  metadata:
   478    name: app-3
   479    namespace: test-ns
   480  spec:
   481    components:
   482      - name: podinfo
   483        type: webservice
   484  
   485    workflow:      
   486      steps:
   487      - type: my-wfstep
   488        name: deploy
   489  `
   490  	testApp4Yaml = `
   491  apiVersion: core.oam.dev/v1beta1
   492  kind: Application
   493  metadata:
   494    name: app-4
   495    namespace: test-ns
   496  spec:
   497    components:
   498      - name: podinfo
   499        type: webservice
   500  
   501    policies:
   502      - type: my-policy
   503        name: topology
   504  `
   505  
   506  	registryCmYaml = `
   507  apiVersion: v1
   508  data:
   509    registries: '{ "KubeVela":{ "name": "KubeVela", "oss": { "end_point": "TEST_SERVER_URL",
   510      "bucket": "", "path": "" } } }'
   511  kind: ConfigMap
   512  metadata:
   513    name: vela-addon-registry
   514    namespace: vela-system
   515  `
   516  	addonDisableTestAppYaml = `
   517  apiVersion: core.oam.dev/v1beta1
   518  kind: Application
   519  metadata:
   520    name: addon-test-disable-addon
   521    namespace: vela-system
   522    labels:
   523      addons.oam.dev/name: test-disable-addon
   524      addons.oam.dev/registry: KubeVela
   525  spec:
   526    components:
   527      - name: podinfo
   528        type: webservice
   529  `
   530  )