github.com/oam-dev/kubevela@v1.9.11/pkg/appfile/template_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 appfile
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"cuelang.org/go/cue/cuecontext"
    24  	"github.com/crossplane/crossplane-runtime/pkg/test"
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/stretchr/testify/assert"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	ktypes "k8s.io/apimachinery/pkg/types"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    33  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    34  	"github.com/oam-dev/kubevela/apis/types"
    35  	oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
    36  )
    37  
    38  func TestLoadComponentTemplate(t *testing.T) {
    39  	cueTemplate := `
    40        context: {
    41           name: "test"
    42        }
    43        output: {
    44        	apiVersion: "apps/v1"
    45        	kind:       "Deployment"
    46        	spec: {
    47        		selector: matchLabels: {
    48        			"app.oam.dev/component": context.name
    49        		}
    50        
    51        		template: {
    52        			metadata: labels: {
    53        				"app.oam.dev/component": context.name
    54        			}
    55        
    56        			spec: {
    57        				containers: [{
    58        					name:  context.name
    59        					image: parameter.image
    60        
    61        					if parameter["cmd"] != _|_ {
    62        						command: parameter.cmd
    63        					}
    64        				}]
    65        			}
    66        		}
    67        
    68        		selector:
    69        			matchLabels:
    70        				"app.oam.dev/component": context.name
    71        	}
    72        }
    73        
    74        parameter: {
    75        	// +usage=Which image would you like to use for your service
    76        	// +short=i
    77        	image: string
    78        
    79        	cmd?: [...string]
    80        }
    81        `
    82  
    83  	var componentDefintion = `
    84  apiVersion: core.oam.dev/v1beta1
    85  kind: ComponentDefinition
    86  metadata:
    87    name: worker
    88    namespace: default
    89    annotations:
    90      definition.oam.dev/description: "Long-running scalable backend worker without network endpoint"
    91  spec:
    92    workload:
    93      definition:
    94        apiVersion: apps/v1
    95        kind: Deployment
    96    extension:
    97      template: |
    98  ` + cueTemplate
    99  
   100  	// Create mock client
   101  	tclient := test.MockClient{
   102  		MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj client.Object) error {
   103  			switch o := obj.(type) {
   104  			case *v1beta1.ComponentDefinition:
   105  				cd, err := oamutil.UnMarshalStringToComponentDefinition(componentDefintion)
   106  				if err != nil {
   107  					return err
   108  				}
   109  				*o = *cd
   110  			}
   111  			return nil
   112  		},
   113  	}
   114  
   115  	temp, err := LoadTemplate(context.TODO(), &tclient, "worker", types.TypeComponentDefinition)
   116  
   117  	if err != nil {
   118  		t.Error(err)
   119  		return
   120  	}
   121  	inst := cuecontext.New().CompileString(temp.TemplateStr)
   122  	instDest := cuecontext.New().CompileString(cueTemplate)
   123  	s1, _ := inst.Value().String()
   124  	s2, _ := instDest.Value().String()
   125  	if s1 != s2 {
   126  		t.Errorf("parsered template is not correct")
   127  	}
   128  }
   129  
   130  func TestLoadTraitTemplate(t *testing.T) {
   131  	cueTemplate := `
   132          parameter: {
   133          	domain: string
   134          	http: [string]: int
   135          }
   136          context: {
   137          	name: "test"
   138          }
   139          // trait template can have multiple outputs in one trait
   140          outputs: service: {
   141          	apiVersion: "v1"
   142          	kind:       "Service"
   143          	metadata:
   144          		name: context.name
   145          	spec: {
   146          		selector:
   147          			"app.oam.dev/component": context.name
   148          		ports: [
   149          			for k, v in parameter.http {
   150          				port:       v
   151          				targetPort: v
   152          			},
   153          		]
   154          	}
   155          }
   156  
   157          outputs: ingress: {
   158          	apiVersion: "networking.k8s.io/v1beta1"
   159          	kind:       "Ingress"
   160          	metadata:
   161          		name: context.name
   162          	spec: {
   163          		rules: [{
   164          			host: parameter.domain
   165          			http: {
   166          				paths: [
   167          					for k, v in parameter.http {
   168          						path: k
   169          						backend: {
   170          							serviceName: context.name
   171          							servicePort: v
   172          						}
   173          					},
   174          				]
   175          			}
   176          		}]
   177          	}
   178          }
   179        `
   180  
   181  	var traitDefintion = `
   182  apiVersion: core.oam.dev/v1beta1
   183  kind: TraitDefinition
   184  metadata:
   185    annotations:
   186      definition.oam.dev/description: "Configures K8s ingress and service to enable web traffic for your service.
   187      Please use route trait in cap center for advanced usage."
   188    name: ingress
   189    namespace: default
   190  spec:
   191    status:
   192      customStatus: |-
   193        if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 {
   194        	message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip
   195        }
   196        if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 {
   197        	message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
   198        }
   199      healthPolicy: |
   200        isHealth: len(context.outputs.service.spec.clusterIP) > 0
   201    appliesToWorkloads:
   202      - deployments.apps
   203    schematic:
   204      cue:
   205        template: |
   206  ` + cueTemplate
   207  
   208  	// Create mock client
   209  	tclient := test.MockClient{
   210  		MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj client.Object) error {
   211  			switch o := obj.(type) {
   212  			case *v1beta1.TraitDefinition:
   213  				wd, err := oamutil.UnMarshalStringToTraitDefinition(traitDefintion)
   214  				if err != nil {
   215  					return err
   216  				}
   217  				*o = *wd
   218  			}
   219  			return nil
   220  		},
   221  	}
   222  
   223  	temp, err := LoadTemplate(context.TODO(), &tclient, "ingress", types.TypeTrait)
   224  
   225  	if err != nil {
   226  		t.Error(err)
   227  		return
   228  	}
   229  	inst := cuecontext.New().CompileString(temp.TemplateStr)
   230  	instDest := cuecontext.New().CompileString(cueTemplate)
   231  	s1, _ := inst.Value().String()
   232  	s2, _ := instDest.Value().String()
   233  	if s1 != s2 {
   234  		t.Errorf("parsered template is not correct")
   235  	}
   236  }
   237  
   238  func TestLoadSchematicToTemplate(t *testing.T) {
   239  	testCases := map[string]struct {
   240  		schematic *common.Schematic
   241  		status    *common.Status
   242  		ext       *runtime.RawExtension
   243  		want      *Template
   244  	}{
   245  		"only tmp": {
   246  			schematic: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
   247  			want: &Template{
   248  				TemplateStr:        "t1",
   249  				CapabilityCategory: types.CUECategory,
   250  			},
   251  		},
   252  		"no tmp,but has extension": {
   253  			ext: &runtime.RawExtension{Raw: []byte(`{"template":"t1"}`)},
   254  			want: &Template{
   255  				TemplateStr:        "t1",
   256  				CapabilityCategory: types.CUECategory,
   257  			},
   258  		},
   259  		"no tmp,but has extension without temp": {
   260  			ext: &runtime.RawExtension{Raw: []byte(`{"template":{"t1":"t2"}}`)},
   261  			want: &Template{
   262  				TemplateStr:        "",
   263  				CapabilityCategory: types.CUECategory,
   264  			},
   265  		},
   266  		"tmp with status": {
   267  			schematic: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
   268  			status: &common.Status{
   269  				CustomStatus: "s1",
   270  				HealthPolicy: "h1",
   271  			},
   272  			want: &Template{
   273  				TemplateStr:        "t1",
   274  				CustomStatus:       "s1",
   275  				Health:             "h1",
   276  				CapabilityCategory: types.CUECategory,
   277  			},
   278  		},
   279  		"no tmp only status": {
   280  			status: &common.Status{
   281  				CustomStatus: "s1",
   282  				HealthPolicy: "h1",
   283  			},
   284  			want: &Template{
   285  				CustomStatus: "s1",
   286  				Health:       "h1",
   287  			},
   288  		},
   289  		"terraform schematic": {
   290  			schematic: &common.Schematic{Terraform: &common.Terraform{}},
   291  			want: &Template{
   292  				CapabilityCategory: types.TerraformCategory,
   293  				Terraform:          &common.Terraform{},
   294  			},
   295  		},
   296  	}
   297  	for reason, casei := range testCases {
   298  		gtmp := &Template{}
   299  		err := loadSchematicToTemplate(gtmp, casei.status, casei.schematic, casei.ext)
   300  		assert.NoError(t, err, reason)
   301  		assert.Equal(t, casei.want, gtmp, reason)
   302  	}
   303  }
   304  
   305  func TestDryRunTemplateLoader(t *testing.T) {
   306  	compDefStr := `
   307  apiVersion: core.oam.dev/v1beta1
   308  kind: ComponentDefinition
   309  metadata:
   310    name: myworker
   311  spec:
   312    status:
   313      customStatus: testCustomStatus
   314      healthPolicy: testHealthPolicy 
   315    workload:
   316      definition:
   317        apiVersion: apps/v1
   318        kind: Deployment
   319    schematic:
   320      cue:
   321        template: testCUE `
   322  
   323  	traitDefStr := `
   324  apiVersion: core.oam.dev/v1beta1
   325  kind: TraitDefinition
   326  metadata:
   327    name: myingress
   328  spec:
   329    status:
   330      customStatus: testCustomStatus
   331      healthPolicy: testHealthPolicy 
   332    appliesToWorkloads:
   333      - deployments.apps
   334    schematic:
   335      cue:
   336        template: testCUE `
   337  
   338  	compDef, _ := oamutil.UnMarshalStringToComponentDefinition(compDefStr)
   339  	traitDef, _ := oamutil.UnMarshalStringToTraitDefinition(traitDefStr)
   340  	unstrctCompDef, _ := oamutil.Object2Unstructured(compDef)
   341  	unstrctTraitDef, _ := oamutil.Object2Unstructured(traitDef)
   342  
   343  	expectedCompTmpl := &Template{
   344  		TemplateStr:        "testCUE",
   345  		Health:             "testHealthPolicy",
   346  		CustomStatus:       "testCustomStatus",
   347  		CapabilityCategory: types.CUECategory,
   348  		Reference: common.WorkloadTypeDescriptor{
   349  			Definition: common.WorkloadGVK{
   350  				APIVersion: "apps/v1",
   351  				Kind:       "Deployment",
   352  			},
   353  		},
   354  		ComponentDefinition: compDef,
   355  	}
   356  
   357  	expectedTraitTmpl := &Template{
   358  		TemplateStr:        "testCUE",
   359  		Health:             "testHealthPolicy",
   360  		CustomStatus:       "testCustomStatus",
   361  		CapabilityCategory: types.CUECategory,
   362  		TraitDefinition:    traitDef,
   363  	}
   364  
   365  	dryRunLoadTemplate := DryRunTemplateLoader([]*unstructured.Unstructured{unstrctCompDef, unstrctTraitDef})
   366  	compTmpl, err := dryRunLoadTemplate(nil, nil, "myworker", types.TypeComponentDefinition)
   367  	if err != nil {
   368  		t.Error("failed load template of component defintion", err)
   369  	}
   370  	if diff := cmp.Diff(expectedCompTmpl, compTmpl); diff != "" {
   371  		t.Fatal("failed load template of component defintion", diff)
   372  	}
   373  
   374  	traitTmpl, err := dryRunLoadTemplate(nil, nil, "myingress", types.TypeTrait)
   375  	if err != nil {
   376  		t.Error("failed load template of component defintion", err)
   377  	}
   378  	if diff := cmp.Diff(expectedTraitTmpl, traitTmpl); diff != "" {
   379  		t.Fatal("failed load template of trait definition ", diff)
   380  	}
   381  }